Last active
January 4, 2018 00:06
-
-
Save zrzka/c273d6602b95275a431c1f1023c62261 to your computer and use it in GitHub Desktop.
Pythonista & auto layout
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
#!python3 | |
import ui | |
from objc_util import ObjCInstance, ObjCClass, on_main_thread | |
from enum import Enum | |
from functools import partial | |
from collections import defaultdict | |
_LayoutConstraint = ObjCClass('NSLayoutConstraint') | |
class LayoutRelation(int, Enum): | |
lessThanOrEqual = -1 | |
equal = 0 | |
greaterThanOrEqual = 1 | |
class LayoutAttribute(int, Enum): | |
notAnAttribute = 0 | |
left = 1 | |
right = 2 | |
top = 3 | |
bottom = 4 | |
leading = 5 | |
trailing = 6 | |
width = 7 | |
height = 8 | |
centerX = 9 | |
centerY = 10 | |
baseline = 11 | |
lastBaseline = 12 | |
firstBaseline = 13 | |
leftMargin = 14 | |
rightMargin = 15 | |
topMargin = 16 | |
bottomMargin = 17 | |
leadingMargin = 18 | |
trailingMargin = 19 | |
centerXWithinMargins = 20 | |
centerYWithinMargins = 21 | |
class LayoutConstraintOrientation(int, Enum): | |
horizontal = 0 | |
vertical = 1 | |
class LayoutPriority(float, Enum): | |
required = 1000 | |
defaultHight = 750 | |
defaultLow = 250 | |
fittingSizeLevel = 50 | |
class _LayoutBaseAttribute: | |
_relations = { | |
"min": LayoutRelation.greaterThanOrEqual, | |
"max": LayoutRelation.lessThanOrEqual, | |
"equal": LayoutRelation.equal | |
} | |
def __init__(self, view, attribute, other=None, other_attribute=LayoutAttribute.notAnAttribute): | |
assert(isinstance(view, LayoutView)) | |
assert(isinstance(attribute, LayoutAttribute)) | |
self._view = view | |
self._attribute = attribute | |
self._constraints = {} | |
if other: | |
assert(isinstance(other, ui.View)) | |
assert(isinstance(other_attribute, LayoutAttribute)) | |
self._other = other | |
self._other_attribute = other_attribute | |
else: | |
self._other = None | |
self._other_attribute = LayoutAttribute.notAnAttribute | |
@property | |
def view(self): | |
return self._view | |
@property | |
def attribute(self): | |
return self._attribute | |
@property | |
def other(self): | |
return self._other | |
@property | |
def other_attribute(self): | |
return self._other_attribute | |
@property | |
def superview(self): | |
return self._view.superview | |
@on_main_thread | |
def remove_constraint(self, constraint): | |
ObjCInstance(self.superview).removeConstraint_(constraint) | |
@on_main_thread | |
def add_constraint(self, constraint): | |
ObjCInstance(self.superview).addConstraint_(constraint) | |
def constraint(relation, value, priority): | |
raise NotImplementedError | |
def __setattr__(self, name, value): | |
if name in self._relations.keys(): | |
constraint = self._constraints.get(name, None) | |
if constraint: | |
self.remove_constraint(constraint) | |
if value is None: | |
return | |
if isinstance(value, tuple): | |
priority = value[1] | |
value = value[0] | |
else: | |
priority = LayoutPriority.required | |
constraint = self.constraint(self._relations[name], value, priority) | |
self._constraints[name] = constraint | |
self.add_constraint(constraint) | |
else: | |
super().__setattr__(name, value) | |
class _LayoutConstantAttribute(_LayoutBaseAttribute): | |
def __init__(self, view, attribute, other=None, other_attribute=LayoutAttribute.notAnAttribute): | |
super().__init__(view, attribute, other, other_attribute) | |
def constraint(self, relation, value, priority): | |
constraint = _LayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( | |
self.view, int(self.attribute), int(relation), self.other, int(self.other_attribute), 1.0, value | |
) | |
constraint.setPriority_(priority) | |
return constraint | |
class _LayoutMultiplierAttribute(_LayoutBaseAttribute): | |
def __init__(self, view, attribute, other=None, other_attribute=LayoutAttribute.notAnAttribute): | |
super().__init__(view, attribute, other, other_attribute) | |
def constraint(self, relation, value, priority): | |
constraint = _LayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( | |
self.view, int(self.attribute), int(relation), self.other, int(self.other_attribute), value, 0 | |
) | |
constraint.setPriority_(priority) | |
return constraint | |
class Layout: | |
_definitions = { | |
"width": (_LayoutConstantAttribute, LayoutAttribute.width, LayoutAttribute.notAnAttribute), | |
"height": (_LayoutConstantAttribute, LayoutAttribute.height, LayoutAttribute.notAnAttribute), | |
"align_center_x_to": (_LayoutConstantAttribute, LayoutAttribute.centerX, LayoutAttribute.centerX), | |
"align_center_y_to": (_LayoutConstantAttribute, LayoutAttribute.centerY, LayoutAttribute.centerY), | |
"align_leading_to": (_LayoutConstantAttribute, LayoutAttribute.leading, LayoutAttribute.leading), | |
"align_trailing_to": (_LayoutConstantAttribute, LayoutAttribute.trailing, LayoutAttribute.trailing), | |
"align_top_to": (_LayoutConstantAttribute, LayoutAttribute.top, LayoutAttribute.top), | |
"align_bottom_to": (_LayoutConstantAttribute, LayoutAttribute.bottom, LayoutAttribute.bottom), | |
"align_left_to": (_LayoutConstantAttribute, LayoutAttribute.left, LayoutAttribute.left), | |
"align_right_to": (_LayoutConstantAttribute, LayoutAttribute.right, LayoutAttribute.right), | |
"align_baseline_to": (_LayoutConstantAttribute, LayoutAttribute.baseline, LayoutAttribute.baseline), | |
"align_center_x_with_superview": (_LayoutConstantAttribute, LayoutAttribute.centerX, LayoutAttribute.centerX), | |
"align_center_y_with_superview": (_LayoutConstantAttribute, LayoutAttribute.centerY, LayoutAttribute.centerY), | |
"align_leading_with_superview": (_LayoutConstantAttribute, LayoutAttribute.leading, LayoutAttribute.leading), | |
"align_trailing_with_superview": (_LayoutConstantAttribute, LayoutAttribute.trailing, LayoutAttribute.trailing), | |
"align_top_with_superview": (_LayoutConstantAttribute, LayoutAttribute.top, LayoutAttribute.top), | |
"align_bottom_with_superview": (_LayoutConstantAttribute, LayoutAttribute.bottom, LayoutAttribute.bottom), | |
"align_left_with_superview": (_LayoutConstantAttribute, LayoutAttribute.left, LayoutAttribute.left), | |
"align_right_with_superview": (_LayoutConstantAttribute, LayoutAttribute.right, LayoutAttribute.right), | |
"relative_superview_width": (_LayoutMultiplierAttribute, LayoutAttribute.width, LayoutAttribute.width), | |
"relative_superview_height": (_LayoutMultiplierAttribute, LayoutAttribute.height, LayoutAttribute.height), | |
"relative_width_to": (_LayoutMultiplierAttribute, LayoutAttribute.width, LayoutAttribute.width), | |
"relative_height_to": (_LayoutMultiplierAttribute, LayoutAttribute.height, LayoutAttribute.height), | |
"left_offset_to": (_LayoutConstantAttribute, LayoutAttribute.left, LayoutAttribute.right), | |
"right_offset_to": (_LayoutConstantAttribute, LayoutAttribute.right, LayoutAttribute.left), | |
"top_offset_to": (_LayoutConstantAttribute, LayoutAttribute.top, LayoutAttribute.bottom), | |
"bottom_offset_to": (_LayoutConstantAttribute, LayoutAttribute.bottom, LayoutAttribute.top), | |
"leading_offset_to": (_LayoutConstantAttribute, LayoutAttribute.leading, LayoutAttribute.trailing), | |
"trailing_offset_to": (_LayoutConstantAttribute, LayoutAttribute.trailing, LayoutAttribute.leading) | |
} | |
def __init__(self, view): | |
assert(isinstance(view, LayoutView)) | |
self._view = view | |
self._attributes = {} | |
def _create_attribute(self, cls, attribute, other_attribute, other): | |
assert(isinstance(attribute, LayoutAttribute)) | |
if other: | |
assert(isinstance(other_attribute, LayoutAttribute)) | |
assert(isinstance(other, ui.View)) | |
name = '{}{}{}'.format(int(attribute), id(other), int(other_attribute)) | |
layout_attribute = self._attributes.get(name, None) | |
if layout_attribute: | |
return layout_attribute | |
layout_attribute = cls(self._view, attribute, other, other_attribute) | |
self._attributes[name] = layout_attribute | |
return layout_attribute | |
def _attribute(self, name, definition): | |
cls = definition[0] | |
attribute = definition[1] | |
other_attribute = definition[2] | |
assert(isinstance(attribute, LayoutAttribute)) | |
assert(isinstance(other_attribute, LayoutAttribute)) | |
if 'superview' in name: | |
return self._create_attribute(cls, attribute, other_attribute, self._view.superview) | |
elif name.endswith('_to'): | |
return partial(self._create_attribute, cls, attribute, other_attribute) | |
else: | |
assert(other_attribute is LayoutAttribute.notAnAttribute) | |
return self._create_attribute(cls, attribute, other_attribute, None) | |
def __getattr__(self, name): | |
if name in self._definitions: | |
return self._attribute(name, self._definitions[name]) | |
return super().__getattr__(name) | |
class LayoutView(ui.View): | |
def __init__(self, view): | |
assert(isinstance(view, ui.View)) | |
self._view = view | |
self.add_subview(self._view) | |
self._view_objc = ObjCInstance(self._view) | |
self._objc = ObjCInstance(self) | |
self._view_objc.setTranslatesAutoresizingMaskIntoConstraints_(False) | |
self._objc.setTranslatesAutoresizingMaskIntoConstraints_(False) | |
attributes = [LayoutAttribute.left, LayoutAttribute.right, LayoutAttribute.top, LayoutAttribute.bottom] | |
for attribute in attributes: | |
constraint = _LayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( | |
self._view_objc, int(attribute), int(LayoutRelation.equal), self._objc, int(attribute), 1.0, 0 | |
) | |
self._objc.addConstraint_(constraint) | |
self._layout = None | |
@property | |
def layout(self): | |
if not self._layout: | |
self._layout = Layout(self) | |
return self._layout | |
@property | |
def view(self): | |
return self._view | |
def table_like(): | |
view = ui.View(background_color='white') | |
name_label = LayoutView(ui.Label( | |
text='Name', font=('<system-bold>', 18), background_color=(0, 0, 0, 0.1) | |
)) | |
view.add_subview(name_label) | |
# 15 points from superview left edge | |
name_label.layout.align_left_with_superview.equal = 15 | |
# 15 points from superview top edge | |
name_label.layout.align_top_with_superview.equal = 15 | |
age_label = LayoutView(ui.Label( | |
text='Age', font=('<system-bold>', 18), | |
alignment=ui.ALIGN_RIGHT, background_color=(0, 0, 0, 0.1) | |
)) | |
view.add_subview(age_label) | |
# Align label baseline with name label | |
age_label.layout.align_baseline_to(name_label).equal = 0 | |
# Right side of age label is 15 points from superview right edge | |
age_label.layout.align_right_with_superview.equal = -15 | |
# Age label width is always 150 pixel | |
age_label.layout.width.equal = (150, LayoutPriority.required) | |
age_label.layout.leading_offset_to(name_label).equal = 8 | |
last_name_label = name_label | |
last_age_label = age_label | |
for i in range(1, 5): | |
name_label = LayoutView(ui.Label( | |
text='Dummy Name {}'.format(i), background_color=(0, 0, 0, 0.01) | |
)) | |
view.add_subview(name_label) | |
# Left egde aligned with left egde of last label | |
name_label.layout.align_leading_to(last_name_label).equal = 0 | |
# Top edge aligned with bottom edge of last label + 16 points | |
name_label.layout.top_offset_to(last_name_label).equal = 16 | |
# Width is 100% of last name label width | |
name_label.layout.relative_width_to(last_name_label).equal = 1.0 | |
age_label = LayoutView(ui.Label( | |
text=str(i * 5), alignment=ui.ALIGN_RIGHT, background_color=(0, 0, 0, 0.01) | |
)) | |
view.add_subview(age_label) | |
# Baseline aligned with name label | |
age_label.layout.align_baseline_to(name_label).equal = 0 | |
# Right edge aligned with last age label | |
age_label.layout.align_trailing_to(last_age_label).equal = 0 | |
# Width is 100% of width of last age label | |
age_label.layout.relative_width_to(last_age_label).equal = 1.0 | |
last_name_label = name_label | |
last_age_label = age_label | |
return view | |
def center_in_superview(): | |
view = ui.View(background_color='white') | |
label = LayoutView(ui.Label( | |
text='80% of width, height, centered', alignment=ui.ALIGN_CENTER, | |
background_color=(0, 0, 0, 0.1) | |
)) | |
view.add_subview(label) | |
# Align horizontal, vertical center with superview | |
label.layout.align_center_x_with_superview.equal = 0 | |
label.layout.align_center_y_with_superview.equal = 0 | |
# Width and height 80% of superview width and height | |
label.layout.relative_width_to(view).equal = 0.8 | |
label.layout.relative_height_to(view).equal = 0.8 | |
return view | |
def grid_like(): | |
view = ui.View(background_color='white') | |
rows = 3 | |
cols = 3 | |
spacing = 10 | |
cells = defaultdict(list) | |
for row in range(0, rows): | |
for col in range(0, cols): | |
cell = LayoutView(ui.Label( | |
text='R: {} C: {}'.format(row, col), | |
alignment=ui.ALIGN_CENTER, | |
background_color=(0, 0, 0, 0.1) | |
)) | |
view.add_subview(cell) | |
cells[row].append(cell) | |
if row == 0: | |
# Top cells, align to superview top | |
cell.layout.align_top_to(view).equal = spacing | |
if row == rows - 1: | |
# Bottom cells, align to superview bottom | |
cell.layout.align_bottom_to(view).equal = -spacing | |
if col == 0: | |
# Left column cells, align to superview left | |
cell.layout.align_left_to(view).equal = spacing | |
if col == cols - 1: | |
# Right column cells, align to superview right | |
cell.layout.align_right_to(view).equal = -spacing | |
if row > 0 or col > 0: | |
# Not top/left cell, make same width / height as top/left cell | |
cell.layout.relative_width_to(cells[0][0]).equal = 1.0 | |
cell.layout.relative_height_to(cells[0][0]).equal = 1.0 | |
if col > 0: | |
# Not first column, add spacing between cells | |
cell.layout.left_offset_to(cells[row][col - 1]).equal = spacing | |
if row > 0: | |
# Not first row, add spacing between cells | |
cell.layout.top_offset_to(cells[row - 1][col]).equal = spacing | |
return view | |
def breadcrumb(): | |
view = ui.View(background_color='white') | |
spacing = 8 | |
previous_label = None | |
for title in ('Documents', 'blackmamba', 'experimental', '__init__.py'): | |
label = LayoutView(ui.Label( | |
text=title, | |
background_color=(0, 0, 0, 0.1) | |
)) | |
view.add_subview(label) | |
if previous_label: | |
label.layout.align_baseline_to(previous_label).equal = 0 | |
label.layout.leading_offset_to(previous_label).equal = spacing | |
else: | |
label.layout.align_top_to(view).equal = spacing | |
label.layout.align_left_to(view).equal = spacing | |
previous_label = label | |
return view | |
def main(): | |
datasource = ui.ListDataSource([ | |
{ | |
'title': 'Table', | |
'view': table_like | |
}, | |
{ | |
'title': 'Center in superview', | |
'view': center_in_superview | |
}, | |
{ | |
'title': 'Grid', | |
'view': grid_like | |
}, | |
{ | |
'title': 'Breadcrumb', | |
'view': breadcrumb | |
} | |
]) | |
def did_select(ds): | |
nv = ds.tableview.navigation_view | |
tv_objc = ObjCInstance(ds.tableview) | |
index_path = tv_objc.indexPathForSelectedRow() | |
if index_path: | |
tv_objc.deselectRowAtIndexPath_animated_(index_path, True) | |
row = ds.selected_row | |
if row < 0: | |
return | |
item = ds.items[row] | |
view = item['view']() | |
view.name = item['title'] | |
nv.push_view(view) | |
datasource.action = did_select | |
window_size = ui.get_window_size() | |
view = ui.View(width=window_size[0] * 0.8, height=window_size[1] * 0.8) | |
tv = ui.TableView(frame=view.bounds, flex='WH') | |
tv.name = 'Auto Layout Demo' | |
tv.data_source = datasource | |
tv.delegate = datasource | |
nv = ui.NavigationView(tv, frame=view.bounds, flex='WH') | |
view.add_subview(nv) | |
view.present('sheet') | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment