Last active
July 4, 2023 17:13
-
-
Save sbbosco/cb44f88960c16a33704cc9f92980a113 to your computer and use it in GitHub Desktop.
Modified dialogs module for Pythonista 3.4
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import 'pythonista' | |
""" | |
Modified dialogs module for Pythonista 3.4 | |
fixes: | |
unstable form_dialog | |
adds date styles: 'auto', 'wheels', 'compact', 'inline' | |
example: dialogs.datetime_dialog(title='Date', style='inline') | |
add this file to 'site-packages' | |
""" | |
import ui | |
from collections.abc import Sequence | |
import datetime | |
from objc_util import on_main_thread, ObjCClass | |
try: | |
from console import alert, input_alert, password_alert, login_alert, hud_alert, open_in, quicklook | |
import Image | |
except ImportError: | |
pass | |
from _dialogs import share_text, share_url, share_image_data, pick_document | |
import sys | |
PY3 = sys.version_info[0] >= 3 | |
if PY3: | |
basestring = str | |
NSIndexPath = ObjCClass('NSIndexPath') | |
def share_image(img): | |
if isinstance(img, ui.Image): | |
img_data = img.to_png() | |
elif isinstance(img, Image.Image): | |
from io import BytesIO | |
b = BytesIO() | |
img.save(b, 'PNG') | |
img_data = b.getvalue() | |
else: | |
raise TypeError("Expected an image") | |
return share_image_data(img_data) | |
class _EditListDialogController (object): | |
def __init__(self, title, items, move_enabled, delete_enabled, done_button_title='Done'): | |
self.was_canceled = True | |
self.items = items | |
self.view = ui.TableView() | |
self.view.name = title | |
self.view.editing = True | |
done_button = ui.ButtonItem(title=done_button_title) | |
done_button.action = self.done_action | |
self.view.right_button_items = [done_button] | |
ds = ui.ListDataSource(items) | |
ds.move_enabled = move_enabled | |
ds.delete_enabled = delete_enabled | |
self.view.data_source = ds | |
self.view.delegate = ds | |
self.view.frame = (0, 0, 500, 500) | |
def done_action(self, sender): | |
self.was_canceled = False | |
self.view.close() | |
class _DateDialogController (object): | |
def __init__(self, mode=ui.DATE_PICKER_MODE_DATE, title='', done_button_title='Done', style=None): | |
self.was_canceled = True | |
self.container_view = ui.View(background_color='white') | |
@on_main_thread | |
def _init(style): | |
self.view = ui.DatePicker(style) | |
if style is None: | |
if mode == ui.DATE_PICKER_MODE_TIME: | |
style = 'wheels' | |
elif mode == ui.DATE_PICKER_MODE_DATE_AND_TIME: | |
style = 'inline' | |
elif mode == ui.DATE_PICKER_MODE_DATE: | |
style = 'inline' | |
elif mode == ui.DATE_PICKER_MODE_COUNTDOWN: | |
style = 'wheels' | |
if style == 'auto': | |
self.view.objc_instance.preferredDatePickerStyle = 0 | |
elif style == 'wheels': | |
self.view.objc_instance.preferredDatePickerStyle = 1 | |
elif style == 'compact': | |
self.view.objc_instance.preferredDatePickerStyle = 2 | |
elif style == 'inline': | |
if mode == ui.DATE_PICKER_MODE_COUNTDOWN: | |
self.view.objc_instance.preferredDatePickerStyle = 1 | |
else: | |
self.view.objc_instance.preferredDatePickerStyle = 3 | |
self.view.name = title | |
self.view.mode = mode | |
self.view.background_color = 'white' | |
self.view.frame = (0, 0, 500, 500) | |
self.view.flex = 'WH' | |
self.container_view.frame = self.view.frame | |
self.container_view.add_subview(self.view) | |
done_button = ui.ButtonItem(title=done_button_title) | |
done_button.action = self.done_action | |
self.container_view.right_button_items = [done_button] | |
_init(style) | |
def done_action(self, sender): | |
self.was_canceled = False | |
self.container_view.close() | |
class _TextDialogController (object): | |
def __init__(self, title='', text='', font=('<system>', 16), autocorrection=None, autocapitalization=ui.AUTOCAPITALIZE_SENTENCES, spellchecking=None, done_button_title='Done'): | |
self.text = None | |
self.view = ui.TextView() | |
self.view.text = text | |
self.view.font = font | |
self.view.frame = (0, 0, 500, 500) | |
self.view.name = title | |
self.view.autocapitalization_type = autocapitalization | |
self.view.autocorrection_type = autocorrection | |
self.view.spellchecking_type = spellchecking | |
done_button = ui.ButtonItem(title=done_button_title) | |
done_button.action = self.done_action | |
self.view.right_button_items = [done_button] | |
def done_action(self, sender): | |
ui.end_editing() | |
self.text = self.view.text | |
self.view.close() | |
class _FormContainerView (ui.View): | |
def __init__(self): | |
self.delegate = None | |
def keyboard_frame_will_change(self, f): | |
r = ui.convert_rect(f, to_view=self) | |
if r[3] > 0: | |
kbh = self.height - r[1] | |
else: | |
kbh = 0 | |
if self.delegate: | |
self.delegate.update_kb_height(kbh) | |
class _FormDialogController (object): | |
def __init__(self, title, sections, done_button_title='Done'): | |
self.was_canceled = True | |
self.shield_view = None | |
self.values = {} | |
self.container_view = _FormContainerView() | |
self.container_view.frame = (0, 0, 500, 550) | |
self.container_view.delegate = self | |
self.view = ui.TableView('grouped') | |
self.view.flex = 'WH' | |
self.container_view.add_subview(self.view) | |
self.container_view.name = title | |
self.view.frame = (0, 0, 500, 550) | |
self.view.data_source = self | |
self.view.delegate = self | |
self.cells = [] | |
self.sections = sections | |
class Cell: | |
def __init__(self): | |
self.key = None | |
self.title = '' | |
self.subtitle = '' | |
self.type = '' | |
self.tint_color = None | |
self.icon = None | |
self.content = None | |
self.accessory_type = None | |
self.value = None | |
for section in self.sections: | |
section_cells = [] | |
self.cells.append(section_cells) | |
items = section[1] | |
for i, item in enumerate(items): | |
##cell = ui.TableViewCell('value1') | |
cell = Cell() ## | |
cell.icon = item.get('icon', None) | |
tint_color = item.get('tint_color', None) | |
if tint_color: | |
cell.tint_color = tint_color | |
""" | |
if icon: | |
if isinstance(icon, basestring): | |
icon = ui.Image.named(icon) | |
if tint_color: | |
cell.image_view.image = icon.with_rendering_mode(ui.RENDERING_MODE_TEMPLATE) | |
else: | |
cell.image_view.image = icon | |
""" | |
title_color = item.get('title_color', None) | |
if title_color: | |
cell.text_label.text_color = title_color | |
t = item.get('type', None) | |
key = item.get('key', item.get('title', str(i))) | |
item['key'] = key | |
title = item.get('title', '') | |
cell.key = key ## | |
cell.title = title ## | |
cell.type = t ## | |
if t == 'switch': | |
value = item.get('value', False) | |
self.values[key] = value | |
cell.value = value | |
#cell.text_label.text = title | |
#cell.selectable = False | |
switch = ui.Switch() | |
#w, h = cell.content_view.width, cell.content_view.height | |
#switch.center = (w - switch.width/2 - 10, h/2) | |
switch.flex = 'TBL' | |
switch.value = value | |
switch.name = key | |
switch.action = self.switch_action | |
if tint_color: | |
switch.tint_color = tint_color | |
cell.content = switch | |
#cell.content_view.add_subview(switch) | |
elif t == 'text' or t == 'url' or t == 'email' or t == 'password' or t == 'number': | |
value = item.get('value', '') | |
self.values[key] = value | |
placeholder = item.get('placeholder', '') | |
""" | |
cell.selectable = False | |
cell.text_label.text = title | |
label_width = ui.measure_string(title, font=cell.text_label.font)[0] | |
if cell.image_view.image: | |
label_width += min(64, cell.image_view.image.size[0] + 16) | |
cell_width, cell_height = cell.content_view.width, cell.content_view.height | |
""" | |
tf = ui.TextField() | |
## tf_width = max(40, cell_width - label_width - 32) | |
## tf.frame = (cell_width - tf_width - 8, 1, tf_width, cell_height-2) | |
tf.bordered = False | |
tf.placeholder = placeholder | |
tf.flex = 'W' | |
tf.text = value | |
tf.text_color = '#337097' | |
if t == 'text': | |
tf.autocorrection_type = item.get('autocorrection', None) | |
tf.autocapitalization_type = item.get('autocapitalization', ui.AUTOCAPITALIZE_SENTENCES) | |
tf.spellchecking_type = item.get('spellchecking', None) | |
if t == 'url': | |
tf.keyboard_type = ui.KEYBOARD_URL | |
tf.autocapitalization_type = ui.AUTOCAPITALIZE_NONE | |
tf.autocorrection_type = False | |
tf.spellchecking_type = False | |
elif t == 'email': | |
tf.keyboard_type = ui.KEYBOARD_EMAIL | |
tf.autocapitalization_type = ui.AUTOCAPITALIZE_NONE | |
tf.autocorrection_type = False | |
tf.spellchecking_type = False | |
elif t == 'number': | |
tf.keyboard_type = ui.KEYBOARD_NUMBERS | |
tf.autocapitalization_type = ui.AUTOCAPITALIZE_NONE | |
tf.autocorrection_type = False | |
tf.spellchecking_type = False | |
elif t == 'password': | |
tf.secure = True | |
tf.clear_button_mode = 'while_editing' | |
tf.name = key | |
tf.delegate = self | |
cell.content = tf | |
## cell.content_view.add_subview(tf) | |
elif t == 'check': | |
value = item.get('value', False) | |
group = item.get('group', None) | |
cell.value = value | |
""" | |
if value: | |
cell.accessory_type = 'checkmark' | |
cell.text_label.text_color = cell.tint_color | |
""" | |
## cell.text_label.text = title | |
if group: | |
if value: | |
self.values[group] = key | |
else: | |
self.values[key] = value | |
elif t == 'date' or t == 'datetime' or t == 'time': | |
value = item.get('value', datetime.datetime.now()) | |
if type(value) == datetime.date: | |
value = datetime.datetime.combine(value, datetime.time()) | |
if type(value) == datetime.time: | |
value = datetime.datetime.combine(value, datetime.date.today()) | |
date_format = item.get('format', None) | |
if not date_format: | |
if t == 'date': | |
date_format = '%Y-%m-%d' | |
elif t == 'time': | |
date_format = '%H:%M' | |
else: | |
date_format = '%Y-%m-%d %H:%M' | |
item['format'] = date_format | |
date_style = item.get('style', None) | |
if not date_style: | |
if t == 'date': | |
date_style = 'inline' | |
elif t == 'time': | |
date_style = 'wheels' | |
else: | |
date_style = 'inline' | |
item['style'] = date_style | |
#cell.detail_text_label.text = value.strftime(date_format) | |
cell.subtitle = value.strftime(date_format) | |
self.values[key] = value | |
cell.value = value | |
#cell.text_label.text = title | |
else: | |
cell.selectable = False | |
cell.text_label.text = item.get('title', '') | |
section_cells.append(cell) | |
done_button = ui.ButtonItem(title=done_button_title) | |
done_button.action = self.done_action | |
self.container_view.right_button_items = [done_button] | |
def update_kb_height(self, h): | |
self.view.content_inset = (0, 0, h, 0) | |
self.view.scroll_indicator_insets = (0, 0, h, 0) | |
def tableview_number_of_sections(self, tv): | |
return len(self.cells) | |
def tableview_title_for_header(self, tv, section): | |
return self.sections[section][0] | |
def tableview_title_for_footer(self, tv, section): | |
s = self.sections[section] | |
if len(s) > 2: | |
return s[2] | |
return None | |
def tableview_number_of_rows(self, tv, section): | |
return len(self.cells[section]) | |
def tableview_did_select(self, tv, section, row): | |
sel_item = self.sections[section][1][row] | |
t = sel_item.get('type', None) | |
if t == 'check': | |
key = sel_item['key'] | |
tv.selected_row = -1 | |
group = sel_item.get('group', None) | |
#cell = self.cells[section][row] | |
index_path = NSIndexPath.indexPathForRow(row, inSection=section) | |
cell = self.view.objc_instance.cellForRowAtIndexPath_(index_path) | |
if group: | |
for i, s in enumerate(self.sections): | |
for j, item in enumerate(s[1]): | |
if item.get('type', None) == 'check' and item.get('group', None) == group and item is not sel_item: | |
self.cells[i][j].accessory_type = 'none' | |
self.cells[i][j].text_label.text_color = None | |
cell.accessory_type = 'checkmark' | |
cell.text_label.text_color = cell.tint_color | |
self.values[group] = key | |
else: | |
if cell.accessoryType() == 0: | |
cell.setAccessoryType_(3) | |
self.values[key] = True | |
else: | |
cell.setAccessoryType_(0) | |
self.values[key] = False | |
""" | |
if cell.accessory_type == 'checkmark': | |
cell.accessory_type = 'none' | |
cell.text_label.text_color = None | |
self.values[key] = False | |
else: | |
cell.accessory_type = 'checkmark' | |
self.values[key] = True | |
""" | |
elif t == 'date' or t == 'time' or t == 'datetime': | |
index_path = NSIndexPath.indexPathForRow(row, inSection=section) | |
tv.selected_row = -1 | |
self.selected_date_key = sel_item['key'] | |
self.selected_date_value = self.values.get(self.selected_date_key) | |
self.selected_date_cell = self.view.objc_instance.cellForRowAtIndexPath_(index_path) | |
self.selected_date_format = sel_item['format'] | |
style = sel_item['style'] | |
self.selected_date_type = t | |
if t == 'date': | |
mode = ui.DATE_PICKER_MODE_DATE | |
elif t == 'time': | |
mode = ui.DATE_PICKER_MODE_TIME | |
else: | |
mode = ui.DATE_PICKER_MODE_DATE_AND_TIME | |
self.show_datepicker(mode, style) | |
def show_datepicker(self, mode, style): | |
ui.end_editing() | |
self.shield_view = ui.View() | |
self.shield_view.flex = 'WH' | |
self.shield_view.frame = (0, 0, self.view.width, self.view.height) | |
self.dismiss_datepicker_button = ui.Button() | |
self.dismiss_datepicker_button.flex = 'WH' | |
self.dismiss_datepicker_button.frame = (0, 0, self.view.width, self.view.height) | |
self.dismiss_datepicker_button.background_color = (0, 0, 0, 0.5) | |
self.dismiss_datepicker_button.background_color = 'lightgray' | |
self.dismiss_datepicker_button.action = self.dismiss_datepicker | |
self.dismiss_datepicker_button.alpha = 0.0 | |
self.shield_view.add_subview(self.dismiss_datepicker_button) | |
self.date_picker = ui.DatePicker() | |
self.date_picker.date = self.selected_date_value | |
self.date_picker.background_color = 'white' | |
#self.date_picker.alpha = 1.0 | |
self.date_picker.mode = mode | |
# styles .auto = 0 .wheels = 1 .compact = 2 .inline = 3 | |
if style == 'auto': | |
self.date_picker.objc_instance.preferredDatePickerStyle = 0 | |
elif style == 'wheels': | |
self.date_picker.objc_instance.preferredDatePickerStyle = 1 | |
elif style == 'compact': | |
self.date_picker.objc_instance.preferredDatePickerStyle = 2 | |
elif style == 'inline': | |
self.date_picker.objc_instance.preferredDatePickerStyle = 3 | |
self.date_picker.frame = (0, self.shield_view.height - self.date_picker.height - 100, self.shield_view.width, self.date_picker.height + 100) | |
#print(self.date_picker.frame) | |
self.date_picker.flex = 'TW' | |
#self.date_picker.transform = ui.Transform.translation(0, self.date_picker.height) | |
self.shield_view.add_subview(self.date_picker) | |
self.container_view.add_subview(self.shield_view) | |
## self.date_picker.bring_to_front() ## | |
def fade_in(): | |
self.dismiss_datepicker_button.alpha = 1.0 | |
self.date_picker.transform = ui.Transform.translation(0, 0) | |
ui.animate(fade_in, 0.3) | |
def dismiss_datepicker(self, sender): | |
value = self.date_picker.date | |
self.selected_date_cell.detailTextLabel.text = None | |
self.values[self.selected_date_key] = value | |
if self.selected_date_type == 'date': | |
self.selected_date_cell.detailTextLabel().text = value.strftime(self.selected_date_format) | |
elif self.selected_date_type == 'time': | |
self.selected_date_cell.detailTextLabel().text = value.strftime(self.selected_date_format) | |
else: | |
self.selected_date_cell.detailTextLabel().text = value.strftime(self.selected_date_format) | |
self.selected_date_cell.detailTextLabel.text = None | |
self.values[self.selected_date_key] = value | |
def fade_out(): | |
self.dismiss_datepicker_button.alpha = 0.0 | |
self.date_picker.transform = ui.Transform.translation(0, self.date_picker.height) | |
def remove(): | |
self.container_view.remove_subview(self.shield_view) | |
self.shield_view = None | |
ui.animate(fade_out, 0.3, completion=remove) | |
def tableview_cell_for_row(self, tv, section, row): | |
ctype = self.cells[section][row].type | |
ctitle = self.cells[section][row].title | |
cvalue = self.cells[section][row].value | |
icon = self.cells[section][row].icon | |
tvcell = ui.TableViewCell('value1') | |
tvcell.text_label.text = ctitle | |
tvcell.text_label.text_color = self.cells[section][row].tint_color | |
#content = self.cells[section][row].content | |
if icon: | |
if isinstance(icon, basestring): | |
icon = ui.Image.named(icon) | |
if tint_color: | |
tvcell.image_view.image = icon.with_rendering_mode(ui.RENDERING_MODE_TEMPLATE) | |
else: | |
tvcell.image_view.image = icon | |
if ctype == 'text' or ctype == 'url' or ctype == 'email' or ctype == 'password' or ctype == 'number': | |
#print('text') | |
tvcell.selectable = False | |
label_width = ui.measure_string(ctitle, font=tvcell.text_label.font)[0] + 16 | |
if tvcell.image_view.image: | |
label_width += min(64, tvcell.image_view.image.size[0] + 16) | |
cell_width, cell_height = tvcell.content_view.width, tvcell.content_view.height | |
#print(cell_width, cell_height) | |
tf = self.cells[section][row].content | |
tf_width = max(40, cell_width - label_width - 32) | |
tf.frame = (cell_width - tf_width - 8, 1, tf_width, cell_height-2) | |
tvcell.content_view.add_subview(tf) | |
elif ctype == 'check': | |
if cvalue: | |
tvcell.accessory_type = 'checkmark' | |
tvcell.text_label.text_color = tvcell.tint_color | |
elif ctype == 'switch': | |
switch = self.cells[section][row].content | |
w, h = tvcell.content_view.width, tvcell.content_view.height | |
switch.center = (w - switch.width/2 - 10, h/2) | |
tvcell.content_view.add_subview(switch) | |
elif ctype == 'date' or ctype == 'time' or ctype == 'datetime': | |
tvcell.detail_text_label.text = self.cells[section][row].subtitle | |
return tvcell | |
#return self.cells[section][row] | |
def textfield_did_change(self, tf): | |
self.values[tf.name] = tf.text | |
def switch_action(self, sender): | |
self.values[sender.name] = sender.value | |
def done_action(self, sender): | |
if self.shield_view: | |
self.dismiss_datepicker(None) | |
else: | |
ui.end_editing() | |
self.was_canceled = False | |
self.container_view.close() | |
class _ListDialogController (object): | |
def __init__(self, title, items, multiple=False, done_button_title='Done'): | |
self.items = items | |
self.selected_item = None | |
self.view = ui.TableView() | |
self.view.name = title | |
self.view.allows_multiple_selection = multiple | |
if multiple: | |
done_button = ui.ButtonItem(title=done_button_title) | |
done_button.action = self.done_action | |
self.view.right_button_items = [done_button] | |
ds = ui.ListDataSource(items) | |
ds.action = self.row_selected | |
ds.delete_enabled = False | |
self.view.data_source = ds | |
self.view.delegate = ds | |
self.view.frame = (0, 0, 500, 500) | |
def done_action(self, sender): | |
selected = [] | |
for row in self.view.selected_rows: | |
selected.append(self.items[row[1]]) | |
self.selected_item = selected | |
self.view.close() | |
def row_selected(self, ds): | |
if not self.view.allows_multiple_selection: | |
self.selected_item = self.items[ds.selected_row] | |
self.view.close() | |
def list_dialog(title='', items=None, multiple=False, done_button_title='Done'): | |
if not items: | |
items = [] | |
if not isinstance(title, basestring): | |
raise TypeError('title must be a string') | |
if not isinstance(items, Sequence): | |
raise TypeError('items must be a sequence') | |
c = _ListDialogController(title, items, multiple, done_button_title=done_button_title) | |
c.view.present('sheet') | |
c.view.wait_modal() | |
return c.selected_item | |
def edit_list_dialog(title='', items=None, move=True, delete=True, done_button_title='Done'): | |
if not items: | |
items = [] | |
if not isinstance(title, basestring): | |
raise TypeError('title must be a string') | |
if not isinstance(items, Sequence): | |
raise TypeError('items must be a sequence') | |
c = _EditListDialogController(title, items, move, delete, done_button_title=done_button_title) | |
c.view.present('sheet') | |
c.view.wait_modal() | |
if c.was_canceled: | |
return None | |
return list(c.view.data_source.items) | |
def form_dialog(title='', fields=None, sections=None, done_button_title='Done'): | |
if not sections and not fields: | |
raise ValueError('sections or fields are required') | |
if not sections: | |
sections = [('', fields)] | |
if not isinstance(title, basestring): | |
raise TypeError('title must be a string') | |
for section in sections: | |
if not isinstance(section, Sequence): | |
raise TypeError('Sections must be sequences (title, fields)') | |
if len(section) < 2: | |
raise TypeError('Sections must have 2 or 3 items (title, fields[, footer]') | |
if not isinstance(section[0], basestring): | |
raise TypeError('Section titles must be strings') | |
if not isinstance(section[1], Sequence): | |
raise TypeError('Expected a sequence of field dicts') | |
for field in section[1]: | |
if not isinstance(field, dict): | |
raise TypeError('fields must be dicts') | |
c = _FormDialogController(title, sections, done_button_title=done_button_title) | |
c.container_view.present('sheet') | |
c.container_view.wait_modal() | |
# Get rid of the view to avoid a retain cycle: | |
c.container_view = None | |
if c.was_canceled: | |
return None | |
return c.values | |
def text_dialog(title='', text='', font=('<system>', 16), autocorrection=None, autocapitalization=ui.AUTOCAPITALIZE_SENTENCES, spellchecking=None, done_button_title='Done'): | |
c = _TextDialogController(title=title, text=text, font=font, autocorrection=autocorrection, autocapitalization=autocapitalization, spellchecking=spellchecking, done_button_title=done_button_title) | |
c.view.present('sheet') | |
c.view.begin_editing() | |
c.view.wait_modal() | |
return c.text | |
def date_dialog(title='', done_button_title='Done', style=None): | |
c = _DateDialogController(title=title, | |
done_button_title=done_button_title, style=style) | |
c.container_view.present('sheet') | |
c.container_view.wait_modal() | |
if c.was_canceled: | |
return None | |
return c.view.date.date() | |
def time_dialog(title='', done_button_title='Done', style=None): | |
c = _DateDialogController(mode=ui.DATE_PICKER_MODE_TIME, title=title, | |
done_button_title=done_button_title, style=style) | |
c.container_view.present('sheet') | |
c.container_view.wait_modal() | |
if c.was_canceled: | |
return None | |
return c.view.date.time() | |
def datetime_dialog(title='', done_button_title='Done', style=None): | |
c = _DateDialogController(mode=ui.DATE_PICKER_MODE_DATE_AND_TIME, title=title, | |
done_button_title=done_button_title, style=style) | |
c.container_view.present('sheet') | |
c.container_view.wait_modal() | |
if c.was_canceled: | |
return None | |
return c.view.date | |
def duration_dialog(title='', done_button_title='Done', style=None): | |
c = _DateDialogController(mode=ui.DATE_PICKER_MODE_COUNTDOWN, title=title, | |
done_button_title=done_button_title, style=style) | |
c.container_view.present('sheet') | |
c.container_view.wait_modal() | |
if c.was_canceled: | |
return None | |
return c.view.countdown_duration |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment