├── .gitignore ├── LICENSE ├── README.md ├── TASKLIST.md ├── demo.py ├── test.py └── ui2 ├── NewPath.py ├── __init__.py ├── animate.py ├── delay.py ├── kb_shortcuts.py ├── path_helpers.py ├── screen.py ├── shapes.py ├── statusbar.py ├── subclassing ├── __init__.py ├── proxies.py └── proxytypes.py ├── transition.py ├── ui_io.py ├── utils ├── __init__.py └── tables.py └── view_classes ├── BlurView.py ├── CameraView.py ├── MapView.py ├── PathView.py ├── ProgressPathView.py ├── TableView.py └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Luke Deen Taylor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ui2 2 | Builds on Pythonista's UI module to provide extra functionality 3 | 4 | ## Features 5 | Some of the most notable features of `ui2` are as follows: 6 | - Transitioning between views 7 | - Progress bars, which can take any shape representable with a `ui.Path`, including lines, circles, and rectangles 8 | - An entirely new system for delays. This introduces a new system for tracking delays with IDs, and allows for multiple "delay managers" to make things less global. 9 | - Classes for controlling animation, which include support for different easings, and chaining several animations to run on sequence. 10 | - Functions for scaling and stretching `ui.Path` objects, as well as a convenience `ui2.PathView` class which displays a path auch that as the `PathView` changes size, it intelligently stretches its path to match. 11 | 12 | `ui2` includes a `demo.py` script which makes it easy to view and run examples for each major feature of the module. 13 | 14 | Lastly, `ui2` is expanding fast! Check back frequently for new features. 15 | 16 | ## Design goals 17 | `ui2` aims to make it as easy as possible to swap out `ui` for `ui2` and begin adding features. It follows that one of the aims of `ui2` is to maintain compatibility with `ui`, so that switching to `ui2` won't break your existing code. -------------------------------------------------------------------------------- /TASKLIST.md: -------------------------------------------------------------------------------- 1 | #Task List 2 | A list of things I'd like to add, or things that are planned 3 | 4 | # `ui.View` classes 5 | - [x] `PathView` class for easily displaying shapes in a UI 6 | - Perhaps the existing polygon classes should inherit from this? 7 | - [x] Progress bar 8 | - Should have styling options 9 | - [x] NewPath class 10 | - [x] Basics 11 | - [x] Scaling and stretching the whole path - implemented via `Pathview` 12 | - [ ] `AccordionView` class - See [this](http://materializecss.com/collapsible). 13 | - [ ] **Side menu class** 14 | - This will wrap an existing view, adding a menu that can slide out from the side. There should be no visible change to the view. 15 | - It will have methods for showing and hiding, the menu, etc. 16 | - [ ] Pull to refresh 17 | - [x] `BlurView` 18 | - [x] `MapView` 19 | 20 | 21 | # New features for UIs 22 | - [x] Transitions 23 | - [ ] Make transitions work on subviews 24 | - [x] Chain transitions together 25 | - Animations 26 | - [x] iOS included easings 27 | - [ ] ~~Custom easings~~ (may be impossible) 28 | - [x] Chained animations to be executed in sequence 29 | - [ ] Repeated animations 30 | - [x] New `ui2.delay` interface 31 | - [x] Decorators for delay: 32 | ```python 33 | @ui2.delayed(1) 34 | def hi(): 35 | print("Hello") 36 | ``` 37 | - [x] Named delays which can be cancelled individually 38 | - [ ] Gestures 39 | - [x] Easy keyboard shortcuts 40 | 41 | # Misc 42 | - [ ] Create `.pyui` files from in-memory `ui.View` objects - Individual components can be created with their subviews, but not the top-level metadata. 43 | - [ ] Test suite 44 | -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui2 3 | 4 | 5 | def _f(*args, **kwargs): 6 | print('Hello') 7 | 8 | 9 | # DEMOS ----------------------------------------------------------------------- 10 | 11 | 12 | def demo_PathView(): 13 | # SETUP 14 | p = ui2.get_regular_polygon_path(6, center=(50, 50), radius=50) 15 | pv = ui2.PathView(p) 16 | pv.x = 150 17 | pv.y = 150 18 | 19 | # ANIMATION FUNCTIONS 20 | def scaleWidth(): 21 | pv.width = 200 22 | 23 | def scaleHeight(): 24 | pv.height = 200 25 | 26 | def scaleBoth(): 27 | pv.width = 300 28 | pv.height = 300 29 | 30 | def scaleBack(): 31 | pv.x, pv.y = 0, 0 32 | pv.width, pv.height = 50, 50 33 | 34 | # BASIC USAGE 35 | v = ui.View(background_color="white") 36 | v.width = v.height = 500 37 | v.add_subview(pv) 38 | v.present("sheet", hide_title_bar=True) 39 | # PERFORM THE ANIMATIONS 40 | ui.animate(scaleWidth, 1) 41 | ui.delay(lambda: ui.animate(scaleHeight, 1), 1) 42 | ui.delay(lambda: ui.animate(scaleBoth, 1), 2) 43 | ui.delay(lambda: ui.animate(scaleBack, 1), 3) 44 | 45 | 46 | def demo_Polygon(): 47 | for i in range(3, 9): # Triangle to octagon 48 | p = ui2.get_regular_polygon_path(i, center=(50, 50), radius=50) 49 | ui2.pathutil.get_path_image(p).show() 50 | 51 | 52 | def demo_ProgressPathView(): 53 | import math 54 | import random 55 | 56 | p = ui.Path() 57 | p.move_to(20, 20) 58 | p.line_to(480, 20) 59 | p.line_to(480, 250) 60 | p.add_arc(250, 250, 230, 0, math.radians(110)) 61 | p.add_curve(50, 450, 20, 250, 480, 250) 62 | p.close() # This makes the end look nicer 63 | 64 | ppv = ui2.ProgressPathView(p) 65 | 66 | view = ui.View(background_color="white") 67 | view.add_subview(ppv) 68 | view.width = view.height = ppv.width = ppv.height = 500 69 | view.present("sheet", hide_title_bar=True) 70 | 71 | def advance(): 72 | """Advance by a random amount and repeat.""" 73 | pg = ppv.progress + random.random() / 20 74 | if pg < 1: 75 | ppv.progress = pg 76 | ui.delay(advance, random.random() / 2) 77 | else: 78 | ppv.progress = 1 79 | 80 | advance() 81 | 82 | 83 | def demo_Animation(): 84 | v = ui.View(frame=(0, 0, 500, 500), background_color="red") 85 | b = ui.View(frame=(100, 200, 100, 100), background_color="white") 86 | v.add_subview(b) 87 | 88 | def a(): 89 | b.x = 300 90 | 91 | v.present("sheet", hide_title_bar=True) 92 | 93 | ui2.animate(a, 0.25, 0.25, _f) 94 | 95 | 96 | def demo_ChainedAnimation(): 97 | v = ui.View(frame=(0, 0, 500, 500), background_color="red") 98 | b = ui.View(frame=(100, 200, 100, 100), background_color="white") 99 | v.add_subview(b) 100 | 101 | def animation_a(): 102 | b.x = 300 103 | 104 | def animation_b(): 105 | b.x = 100 106 | 107 | a_anim = ui2.Animation(animation_a, 1, easing=ui2.ANIMATE_EASE_IN) 108 | b_anim = ui2.Animation(animation_b, 1, easing=ui2.ANIMATE_EASE_OUT) 109 | 110 | v.present("sheet", hide_title_bar=True) 111 | 112 | chain = ui2.ChainedAnimation(a_anim, b_anim, a_anim, b_anim, completion=_f) 113 | chain.play() 114 | 115 | 116 | def demo_Transition(): 117 | v1 = ui.View(frame=(0, 0, 500, 500), background_color="red") 118 | v2 = ui.View(background_color="blue") 119 | v1.present('sheet', hide_title_bar=True) 120 | ui2.transition(v1, v2, ui2.TRANSITION_CURL_UP, 1.5, _f) 121 | 122 | 123 | def demo_ChainedTransition(): 124 | v1 = ui.View(frame=(0, 0, 500, 500), background_color="pink") 125 | v1.add_subview(ui.Button(frame=(100, 100, 300, 20))) 126 | v1.subviews[0].title = "Hello! I'm a button" 127 | v1.add_subview(ui.Slider(frame=(100, 300, 100, 20))) 128 | v2 = ui.View(background_color="lightblue") 129 | v2.add_subview(ui.ImageView(frame=(100, 100, 300, 300))) 130 | v2.subviews[0].image = ui.Image.named('test:Peppers') 131 | v3 = ui.View(background_color="lightgreen") 132 | v3.add_subview(ui.Switch(frame=(100, 100, 20, 10))) 133 | v3.subviews[0].value = True 134 | 135 | t1 = ui2.Transition(v1, v2, ui2.TRANSITION_CURL_UP, 1.5) 136 | t2 = ui2.Transition(v2, v3, ui2.TRANSITION_FLIP_FROM_LEFT, 1) 137 | t3 = ui2.Transition(v3, v1, ui2.TRANSITION_CROSS_DISSOLVE, 1) 138 | 139 | v1.present("sheet", hide_title_bar=True) 140 | 141 | ui2.delay(ui2.ChainedTransition(t1, t2, t3).play, 1) 142 | 143 | 144 | def demo_BlurView(): 145 | a = ui.View(frame=(0, 0, 500, 500)) 146 | a.add_subview(ui.ImageView(frame=(0, 0, 500, 500))) 147 | a.subviews[0].image = ui.Image.named('test:Peppers') 148 | a.add_subview(ui2.BlurView()) 149 | a.subviews[1].frame = (100, 100, 100, 100) 150 | a.present('sheet', hide_title_bar=True) 151 | 152 | toggle = ui2.Animation(a.subviews[1].toggle_brightness, 0.5) 153 | def movea(): 154 | a.subviews[1].x = 300 155 | def moveb(): 156 | a.subviews[1].y = 300 157 | def movec(): 158 | a.subviews[1].x = 100 159 | def moved(): 160 | a.subviews[1].y = 100 161 | movea = ui2.Animation(movea, 1) 162 | moveb = ui2.Animation(moveb, 1) 163 | movec = ui2.Animation(movec, 1) 164 | moved = ui2.Animation(moved, 1) 165 | ui2.ChainedAnimation(movea, moveb, movec, moved, toggle, 166 | movea, moveb, movec, moved).play() 167 | 168 | 169 | def demo_Delays(): 170 | print("* Starting *") 171 | print() 172 | print("Delays in ui2.delay_manager:") 173 | print(ui2.delay_manager) 174 | print() 175 | 176 | @ui2.delayed_by(2, id="Hello") 177 | def func(): 178 | print("* Finished *") 179 | print() 180 | 181 | # On a different manager, this won't show up in results! 182 | @ui2.delayed_by(2.1, manager=ui2.DelayManager()) 183 | def print_at_end(): 184 | print("Delays in ui2.delay_manager:") 185 | print(ui2.delay_manager) 186 | 187 | print("* Started *") 188 | print() 189 | print("Delays in ui2.delay_manager:") 190 | print(ui2.delay_manager) 191 | print() 192 | 193 | 194 | def demo_Status_Bar(): 195 | ui2.statusbar.color = 0 196 | ui2.statusbar.background_color = "#ff8" 197 | print("Look at the status bar!") 198 | ui2.delay(ui2.statusbar.reset, 15) 199 | 200 | 201 | def demo_Screen(): 202 | print(ui2.screen) 203 | 204 | 205 | def demo_CameraView(): 206 | from ui2.view_classes.CameraView import CameraView 207 | a = CameraView() 208 | a.background_color = "#fff" 209 | a.start() 210 | a.present() 211 | a.will_close = lambda: a.stop() 212 | 213 | 214 | def demo_Subclassing(): 215 | class SuperSlider(ui2.subclassable(ui.Slider)): 216 | def set_halfway(self): 217 | self.value = 0.5 218 | 219 | ss = SuperSlider(frame=(0, 0, 200, 40)) 220 | print("ss, a \"subclass\" of ui.Slider: {}".format(ss)) 221 | print("The subclassible proxy used forwards atttributes:") 222 | print(" ss.frame = {}".format(ss.frame)) 223 | print("Methods from the subclass are preserved, though:") 224 | ss.present("sheet") 225 | print(" ss.set_halfway()") 226 | ss.set_halfway() 227 | ui2.delay(ss.close, 0.5) 228 | 229 | 230 | def demo_MapView(): 231 | a = ui2.MapView(frame=(0, 0, 500, 500)) 232 | a.present("sheet") 233 | 234 | @ui2.delayed_by(0.5) 235 | def move_a(): 236 | a.bounds = -30, -30, 30, 30 237 | 238 | @ui2.delayed_by(2) 239 | def move_b(): 240 | a.center = 50, 50 241 | 242 | 243 | # DEMO RUNNER ----------------------------------------------------------------- 244 | 245 | 246 | if __name__ == "__main__": 247 | import dialogs 248 | 249 | prefix = "demo_" 250 | 251 | # Generate list of demos 252 | functions = [k for k in globals().keys() if k.startswith(prefix)] 253 | 254 | # Let user pick one 255 | demo = dialogs.list_dialog( 256 | "Choose a demo", 257 | sorted([fn.replace(prefix, "").replace("_", " ") for fn in functions], 258 | key=lambda x: x.lower()) 259 | ) 260 | 261 | # Run the demo 262 | if demo is not None: 263 | globals()[prefix + demo.replace(" ", "_")]() 264 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | # Testing libraries 2 | import unittest 3 | try: 4 | import coverage 5 | has_coverage = True 6 | except ImportError: 7 | has_coverage = False 8 | 9 | # Modules used in testing 10 | import ui 11 | import ui2 12 | import ui2.ui_io 13 | 14 | import os 15 | 16 | LOCALDIR = os.path.abspath(os.path.dirname(__file__)) 17 | 18 | class TestDump(unittest.TestCase): 19 | """ Test ui2.dump_view to ensure the preservation of all attributes """ 20 | # SETUP 21 | path = "test.pyui" 22 | 23 | def tearDown(self): 24 | """ Called after each test to remove the pyui file created """ 25 | if os.path.exists(self.path): 26 | os.remove(self.path) 27 | 28 | # MAIN TESTS 29 | 30 | def test_Generic(self): 31 | """ Test all generic attributes of any ui.View class """ 32 | a = ui.View() 33 | # Store attributes 34 | a.background_color = "#ff00ff" 35 | # Encode + decode 36 | b = ui._view_from_dict(ui2.ui_io._view_to_dict(a), globals(), locals()) 37 | # Check that type and attributes were preserved 38 | self.assertIsInstance(b, type(a)) 39 | self.assertEqual(a.background_color, b.background_color) 40 | 41 | def test_Button(self): 42 | a = ui.Button() 43 | # Store attributes 44 | a.title = "Hey, it's a thing!" 45 | # Encode + decode 46 | b = ui._view_from_dict(ui2.ui_io._view_to_dict(a), globals(), locals()) 47 | # Check that type and attributes were preserved 48 | self.assertIsInstance(b, type(a)) 49 | self.assertEqual(a.title, b.title) 50 | 51 | if __name__ == "__main__": 52 | if has_coverage: 53 | cov = coverage.Coverage(source=["ui2"]) 54 | cov.start() 55 | 56 | unittest.main(exit=False) 57 | 58 | if has_coverage: 59 | print("\n") 60 | cov.stop() 61 | cov.report() 62 | else: 63 | print("Install the 'coverage' module with StaSh to see more detailed " 64 | "reports!") 65 | 66 | -------------------------------------------------------------------------------- /ui2/NewPath.py: -------------------------------------------------------------------------------- 1 | import objc_util 2 | import ui 3 | 4 | 5 | # We can't inherit :( 6 | class Path(): 7 | """ A magical wrapper around ui.Path that allows you to track where the 8 | path goes. Tracks all the points in the path, as well as the exact 9 | parameters used to create each component of the path """ 10 | def __init__(self): 11 | self.p = ui.Path() 12 | 13 | # Stores all arcs, curves, and lines in the path. They are stored as 14 | # [start_point, function_args, end_point] 15 | self.components = [] 16 | # Keeps track of the current position internally. Only updated manually 17 | # and is therefore used to track old values 18 | self._position = (0, 0) 19 | 20 | # Copy docstring 21 | self.__doc__ == ui.Path.__doc__ 22 | 23 | # Wrapper methods 24 | 25 | def move_to(self, x, y): 26 | # Not stored in components, since it isn't part of the path. Can be 27 | # easily inferred from gaps between one ending position and the next 28 | # initial position, however 29 | self.p.move_to(x, y) 30 | self._position = x, y 31 | 32 | def line_to(self, x, y): 33 | self.p.line_to(x, y) 34 | self.components.append( 35 | [self._position, # Initial position 36 | (x, y), # Arguments passed 37 | self.position, # Ending position 38 | "line_to"] # Method used 39 | ) 40 | self._position = self.position 41 | 42 | def add_arc(self, center_x, center_y, radius, start_angle, end_angle): 43 | self.p.add_arc(center_x, center_y, radius, start_angle, end_angle) 44 | self.components.append( 45 | [self._position, # Initial 46 | (center_x, center_y, radius, start_angle, end_angle), # Args 47 | self.position, # Ending 48 | "add_arc"] # Method 49 | ) 50 | self._position = self.position 51 | 52 | def add_curve(self, end_x, end_y, cp1_x, cp1_y, cp2_x, cp2_y): 53 | self.p.add_curve(end_x, end_y, cp1_x, cp1_y, cp2_x, cp2_y) 54 | self.components.append( 55 | [self._position, # Initial position 56 | (end_x, end_y, cp1_x, cp1_y, cp2_x, cp2_y), # Arguments passed 57 | self.position, # Ending position 58 | "add_curve"] # Method used 59 | ) 60 | self._position = self.position 61 | 62 | def add_quad_curve(self, end_x, end_y, cp_x, cp_y): 63 | self.p.add_quad_curve(end_x, end_y, cp_x, cp_y) 64 | self.components.append( 65 | [self._position # Initial position 66 | (end_x, end_y, cp_x, cp_y), # Arguments passed 67 | self.position, # Ending position 68 | "add_quad_curve"] # Method used 69 | ) 70 | 71 | def append_path(other_path): 72 | raise NotImplementedError() 73 | 74 | def close(self): 75 | self.p.close() 76 | self.components.append( 77 | [self._position, # Initial position 78 | (), # Aruments passed 79 | self.position, # Ending position 80 | "close"] # Method used 81 | ) 82 | 83 | # Extended API (besides what's in __init__) 84 | 85 | @property 86 | def position(self): 87 | """ Current position """ 88 | pos = objc_util.ObjCInstance(self.p).currentPoint() 89 | return pos.x, pos.y 90 | 91 | @property 92 | def points(self): 93 | return [c[0] for c in self.components] 94 | 95 | @property 96 | def is_closed(self): 97 | return self.components[-1][2] == self.components[0][0] 98 | 99 | # Allow transparant access to the ui.path items 100 | 101 | def __getattr__(self, key): 102 | return getattr(self.p, key) 103 | 104 | if __name__ == "__main__": 105 | with ui.ImageContext(100, 100) as ctx: 106 | a = Path() 107 | # Tests 108 | a.move_to(10, 10) 109 | a.line_to(20, 20) 110 | a.line_to(20, 50) 111 | a.line_to(50, 10) 112 | print(a.points) 113 | a.close() 114 | a.fill() 115 | ctx.get_image().show() 116 | -------------------------------------------------------------------------------- /ui2/__init__.py: -------------------------------------------------------------------------------- 1 | """ A module that builds on Pythonista's `ui` module """ 2 | 3 | # Dump ui into namespace so that `import ui2` can be used interchangably with 4 | # `import ui` 5 | from ui import * 6 | 7 | # Load subpackages 8 | from ui2.shapes import * 9 | from ui2.ui_io import * 10 | import ui2.path_helpers as pathutil 11 | from ui2.statusbar import statusbar 12 | from ui2.screen import screen 13 | from ui2.subclassing import subclassable 14 | 15 | # Load replacements for ui functions 16 | from ui2.animate import * 17 | from ui2.transition import * 18 | from ui2.delay import * 19 | 20 | # Load view classes 21 | from ui2.view_classes.PathView import PathView 22 | from ui2.view_classes.ProgressPathView import ProgressPathView 23 | from ui2.view_classes.BlurView import BlurView 24 | from ui2.view_classes.MapView import MapView 25 | 26 | # Load keyboard shortcuts 27 | from ui2.kb_shortcuts import bind 28 | -------------------------------------------------------------------------------- /ui2/animate.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | 3 | from objc_util import * 4 | 5 | # Constants representing easings. See http://apple.co/29FOF5i 6 | 7 | ANIMATE_EASE_IN = 1 << 16 8 | ANIMATE_EASE_OUT = 2 << 16 9 | ANIMATE_EASE_IN_OUT = 0 << 16 10 | ANIMATE_EASE_NONE = ANIMATE_LINEAR = 3 << 16 11 | 12 | 13 | class Animation(object): 14 | """Represents an animation to one or more properties of an object.""" 15 | def __init__(self, animation, duration=0.25, delay=0.0, completion=None, 16 | easing=ANIMATE_EASE_IN_OUT): 17 | self.animation = animation 18 | self.duration = duration 19 | self.delay = delay 20 | self.completion = completion 21 | self.easing = easing 22 | 23 | self._completion = None # Used internally when a callback is needed 24 | 25 | def play(self): 26 | """Perform the animation.""" 27 | # If any callbacks are set, we set up a block 28 | funcs = (self.completion, self._completion) # Possible callbacks 29 | if any(funcs): 30 | def c(cmd, success): 31 | """A completion function wrapping one or more callbacks.""" 32 | for func in funcs: 33 | if func: # Only call the registered ones 34 | func(success) 35 | # Lets the function be garbage collected when it's safe 36 | release_global(ObjCInstance(cmd)) 37 | oncomplete = ObjCBlock(c, argtypes=[c_void_p, c_bool]) 38 | # This prevents the oncomplete function from being garbage 39 | # collected as soon as the play() function exits 40 | retain_global(oncomplete) 41 | else: 42 | oncomplete = None 43 | 44 | UIView.animateWithDuration_delay_options_animations_completion_( 45 | self.duration, 46 | self.delay, 47 | self.easing, 48 | ObjCBlock(self.animation), 49 | oncomplete 50 | ) 51 | 52 | 53 | class ChainedAnimationComponent(Animation): 54 | def __init__(self, animation_obj, next_animation): 55 | """A single step in a chain of animations.""" 56 | self.animation_obj = animation_obj 57 | self.next_animation = next_animation 58 | 59 | self.duration = self.animation_obj.duration 60 | self.delay = self.animation_obj.delay 61 | self.animation = self.animation_obj.animation 62 | self.easing = self.animation_obj.easing 63 | 64 | self._completion = None 65 | 66 | def completion(self, success): 67 | # If it has a completion function already, run it first. 68 | if self.animation_obj.completion is not None: 69 | self.animation_obj.completion(success) 70 | # Then play the next animation if we're not at the end of the chain 71 | if self.next_animation is not None: 72 | self.next_animation.play() 73 | 74 | 75 | class ChainedAnimation(object): 76 | """Represents a series of several animations to be played in sequence.""" 77 | def __init__(self, *animations, completion=None): 78 | self.completion = completion 79 | 80 | anims = [] 81 | for i, a in reversed(list(enumerate(animations))): 82 | 83 | if i == len(animations) - 1: 84 | # This is the last element in the chain (first in iteration), 85 | # so it has no successor. We can use the old Animation object. 86 | anims.append(copy(a)) 87 | else: 88 | anims.append(ChainedAnimationComponent(copy(a), anims[-1])) 89 | 90 | self.anims = anims[::-1] 91 | 92 | # Register the completion event on the final component 93 | 94 | if self.completion is not None: 95 | self.anims[-1]._completion = self.completion 96 | 97 | def play(self): 98 | """Perform the animations.""" 99 | self.anims[0].play() 100 | 101 | 102 | def animate(animation, *args, **kwargs): 103 | """A drop-in replacement for ui.animate. 104 | 105 | This adds support for different easings. 106 | """ 107 | Animation(animation, *args, **kwargs).play() 108 | -------------------------------------------------------------------------------- /ui2/delay.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import uuid 3 | 4 | from ui2.utils.tables import make_table 5 | 6 | 7 | class DelayManager(object): 8 | """A global manager for all delays.""" 9 | def __init__(self): 10 | self._delays = {} 11 | 12 | def register(self, delay_obj): 13 | """Add a Delay object to the index.""" 14 | if delay_obj.id in self._delays: 15 | print(self._delays) 16 | raise ValueError("Delay IDs must be unique!") 17 | else: 18 | self._delays[delay_obj.id] = delay_obj 19 | 20 | def cancel(self, id): 21 | """Cancel a delay by id.""" 22 | # Allow access by a Delay object 23 | self._delays[id].cancel() 24 | 25 | def cancel_all(self): 26 | """Cancel all delays registered to this manager.""" 27 | for id in self.delays: 28 | self.cancel(id) 29 | 30 | # Access the index 31 | 32 | def get(self, id): 33 | """Get a Delay object from its id.""" 34 | return self._delays[id] 35 | 36 | @property 37 | def delays(self): 38 | return list(self._delays.keys()) 39 | 40 | @property 41 | def description(self): 42 | ds = self._delays.values() 43 | return {d.id: d.description for d in ds} 44 | 45 | def __repr__(self): 46 | data = list(self.description.values()) 47 | # Rename keys 48 | for d in data: 49 | d["ID"] = d.pop("id") 50 | d["Delay"] = d.pop("delay") 51 | d["Function"] = d.pop("function").__name__ 52 | return make_table(data, ["ID", "Delay", "Function"]) 53 | 54 | def __str__(self): 55 | return self.__repr__() 56 | 57 | # A global manager to be used as a default 58 | delay_manager = DelayManager() 59 | 60 | 61 | class Delay(object): 62 | def __init__(self, func, seconds, id=None, manager=delay_manager): 63 | self.function = func 64 | self.seconds = seconds 65 | self.id = id or str(uuid.uuid4()) 66 | self.manager = manager 67 | 68 | def func(): 69 | self.function() 70 | self._deregister() 71 | 72 | self.timer = threading.Timer(self.seconds, func) 73 | 74 | self.manager.register(self) 75 | 76 | def start(self): 77 | self.timer.start() 78 | 79 | def cancel(self): 80 | """Stop the delay and remove it from its manager.""" 81 | self.timer.cancel() 82 | self.manager._delays.pop(self.id) 83 | 84 | def _deregister(self): 85 | self.manager._delays.pop(self.id) 86 | self.manager = None 87 | 88 | # Access data 89 | @property 90 | def description(self): 91 | return { 92 | "id": self.id, 93 | "delay": self.seconds, 94 | "function": self.function 95 | } 96 | 97 | def __repr__(self): 98 | return "ui2.Delay ({})".format(self.id) 99 | 100 | def __str__(self): 101 | return self.id 102 | 103 | 104 | def delay(func, seconds, id=None, manager=delay_manager): 105 | """Call a function after a given delay.""" 106 | delay = Delay(func, seconds, id, manager) 107 | delay.start() 108 | return delay.id 109 | 110 | 111 | def delayed_by(seconds, *args, **kwargs): 112 | """Delay in decorator form.""" 113 | return lambda function: delay(function, seconds, *args, **kwargs) 114 | -------------------------------------------------------------------------------- /ui2/kb_shortcuts.py: -------------------------------------------------------------------------------- 1 | """An easy interface for adding keyboard shortcuts using decorators.""" 2 | 3 | import ctypes 4 | import functools 5 | import operator 6 | import sys 7 | import uuid 8 | 9 | import objc_util 10 | 11 | 12 | _app = objc_util.UIApplication.sharedApplication() 13 | _controller = _app.keyWindow().rootViewController() 14 | 15 | 16 | # Modifiers for special keys 17 | _modifiers = { 18 | "shift": 1 << 17, 19 | "control": 1 << 18, "ctrl": 1 << 18, 20 | "option": 1 << 19, "alt": 1 << 19, 21 | "command": 1 << 20, "cmd": 1 << 20 22 | } 23 | # Input strings for special keys 24 | _special_keys = { 25 | "up": "UIKeyInputUpArrow", 26 | "down": "UIKeyInputDownArrow", 27 | "left": "UIKeyInputLeftArrow", 28 | "right": "UIKeyInputRightArrow", 29 | "escape": "UIKeyInputEscape", "esc": "UIKeyInputEscape" 30 | } 31 | 32 | 33 | # HELPER METHODS 34 | 35 | def _add_method(cls, func): 36 | # void, object, selector 37 | type_encoding = "v@:" 38 | sel_name = str(uuid.uuid4()) 39 | sel = objc_util.sel(sel_name) 40 | class_ptr = objc_util.object_getClass(cls.ptr) 41 | 42 | # ----------------- Modified from objc_util.add_method ------------------ # 43 | parsed_types = objc_util.parse_types(type_encoding) 44 | restype, argtypes, _ = parsed_types 45 | imp = ctypes.CFUNCTYPE(restype, *argtypes)(func) 46 | objc_util.retain_global(imp) 47 | if isinstance(type_encoding, str): 48 | type_encoding = type_encoding.encode('ascii') 49 | objc_util.class_addMethod(class_ptr, sel, imp, type_encoding) 50 | # ----------------------------------------------------------------------- # 51 | return sel 52 | 53 | 54 | def _tokenize_shortcut_string(shortcut): 55 | """Split a plaintext string representing a keyboard shortcut into each 56 | individual key in the shortcut. 57 | 58 | Valid separator characters are any combination of " ", "+", "-", and ",". 59 | """ 60 | # Tokenize the string 61 | out = [shortcut] 62 | for separator in (" ", "-", "+", ","): 63 | new = [] 64 | for piece in out: 65 | new.extend(piece.split(separator)) 66 | out = new[:] 67 | tokens = [i.strip().lower() for i in out if i.strip().lower()] 68 | # Sort the tokens to place modifiers first 69 | return sorted(tokens, key=lambda tok: tok not in _modifiers) 70 | 71 | 72 | def _validate_tokens(tokens): 73 | """Raise appropriate errors for ridiculous key commands. 74 | 75 | This will throw descriptive errors for keyboard keyboard shortcuts like: 76 | - Cmd + Shift + P + I 77 | - Ctrl + Elephant 78 | - Ctrl + Cmd + Shift 79 | """ 80 | exceptions = tuple(_modifiers) + tuple(_special_keys) 81 | # Disallow muultiple non-modifier keys 82 | non_modifier_tokens = [tok for tok in tokens if tok not in _modifiers] 83 | if len(non_modifier_tokens) > 1: 84 | raise ValueError( 85 | "Only one non-modifier key is allowed in a shortcut" 86 | ) 87 | if len(non_modifier_tokens) < 1: 88 | raise ValueError( 89 | "At least one non-modifier key is required in a shortcut" 90 | ) 91 | 92 | # Disallow invalid key names 93 | for tok in tokens: 94 | if len(tok) > 1 and tok not in exceptions: 95 | raise ValueError( 96 | "{} is not a valid keyboard key".format(tok) 97 | ) 98 | 99 | 100 | # TRACKING OF COMMANDS 101 | _registered_commands = {} 102 | 103 | 104 | # REGISTERING 105 | 106 | 107 | def _add_shortcut(shortcut, function, title=None): 108 | """Bind a function to a keyboard shortcut.""" 109 | # Wrap function to accept and ignore arguments 110 | def wrapper(*args, **kwargs): 111 | function() 112 | 113 | # Parse shortcut 114 | tokens = _tokenize_shortcut_string(shortcut) 115 | _validate_tokens(tokens) 116 | modifiers = tokens[:-1] 117 | inp = tokens[-1] 118 | 119 | # Process components 120 | mod_bitmask = functools.reduce( 121 | operator.ior, 122 | [_modifiers[mod] for mod in modifiers], 123 | 0 124 | ) 125 | if inp in _special_keys: 126 | inp = _special_keys[inp] 127 | 128 | # Make the command 129 | sel = _add_method(_controller, wrapper) 130 | 131 | kc = objc_util.ObjCClass("UIKeyCommand") 132 | if title is not None: 133 | c = kc.keyCommandWithInput_modifierFlags_action_discoverabilityTitle_( 134 | inp, 135 | mod_bitmask, 136 | sel, 137 | title 138 | ) 139 | else: 140 | c = kc.keyCommandWithInput_modifierFlags_action_( 141 | inp, 142 | mod_bitmask, 143 | sel 144 | ) 145 | 146 | _registered_commands[frozenset(tokens)] = cp 147 | _controller.addKeyCommand_(c) 148 | 149 | 150 | # MAIN INTERFACE 151 | 152 | 153 | def bind(shortcut, title=None): 154 | """A decorator for binding keyboard shortcuts. 155 | 156 | Example: 157 | 158 | >>> @bind(Command + T) 159 | >>> def test_func(): 160 | ... print("Hello!") 161 | 162 | The shortcut definition syntax is designed to be flexible, so the following 163 | shortcut names are all equivalent: 164 | - Command + Shift + Escape 165 | - cmd-shift-esc 166 | - CMD SHIFT ESCAPE 167 | - command, shift, esc 168 | 169 | A few non-alphanumeric keys are supported with special names: 170 | - up 171 | - down 172 | - left 173 | - right 174 | - escape / esc 175 | """ 176 | return functools.partial(_add_shortcut, shortcut, title=title) 177 | 178 | 179 | if __name__ == "__main__": 180 | import console 181 | @bind("Command Shift Escape", "Say Hi") 182 | def hi(): 183 | console.alert("Hello") 184 | -------------------------------------------------------------------------------- /ui2/path_helpers.py: -------------------------------------------------------------------------------- 1 | import ui 2 | import objc_util 3 | 4 | 5 | def get_path_image(path): 6 | """ Get an image of a path """ 7 | bounds = path.bounds 8 | with ui.ImageContext(bounds.max_x, bounds.max_y) as ctx: 9 | path.fill() 10 | return ctx.get_image() 11 | 12 | 13 | def copy_path(path): 14 | """ Make a copy of a ui.Path and return it. Preserves all data. """ 15 | new = ui.Path() 16 | new.append_path(path) 17 | # Copy over the attributes 18 | new.line_cap_style = path.line_cap_style 19 | new.line_join_style = path.line_join_style 20 | new.line_width = path.line_width 21 | 22 | return new 23 | 24 | 25 | def scale_path(path, scale): 26 | """ Stretch or scale a path. Pass either a scale or a tuple of scales """ 27 | if not hasattr(scale, "__iter__"): 28 | scale = (scale, scale) 29 | sx, sy = scale 30 | 31 | newpath = copy_path(path) 32 | # Construct an affine transformation matrix 33 | transform = objc_util.CGAffineTransform(sx, 0, 0, sy, 0, 0) 34 | # Apply it to the path 35 | objcpath = objc_util.ObjCInstance(newpath) 36 | objcpath.applyTransform_(transform) 37 | return newpath 38 | 39 | 40 | if __name__ == "__main__": 41 | a = ui.Path() 42 | a.line_to(25, 0) 43 | a.line_to(25, 25) 44 | a.close() 45 | 46 | b = scale_path(a, 2) 47 | 48 | get_path_image(a).show() # Note that the original is not mutated 49 | get_path_image(b).show() 50 | -------------------------------------------------------------------------------- /ui2/screen.py: -------------------------------------------------------------------------------- 1 | """Class for screen sizes and orientation.""" 2 | import objc_util 3 | import ui 4 | 5 | 6 | app = objc_util.UIApplication.sharedApplication() 7 | UIScreen = objc_util.ObjCClass("UIScreen") 8 | 9 | orientation_codes = { 10 | 1: "bottom", 11 | 2: "top", 12 | 3: "left", 13 | 4: "right" 14 | } 15 | 16 | 17 | class _ScreenOrientation(object): 18 | """Represents a device orientation state.""" 19 | def __init__(self, orientation): 20 | self.code = orientation 21 | 22 | def __str__(self): 23 | return orientation_codes[self.code] 24 | 25 | def __int__(self): 26 | return self.code 27 | 28 | def __float__(self): 29 | return float(self.code) 30 | 31 | def __repr__(self): 32 | return repr(self.code) 33 | 34 | @property 35 | def portrait(self): 36 | return self.code in (1, 3) 37 | 38 | @property 39 | def landscape(self): 40 | return self.code in (2, 4) 41 | 42 | 43 | class Screen(object): 44 | """An interface to access characteristics of the device's screen.""" 45 | @property 46 | def size(self): 47 | """Get screen size.""" 48 | return ui.get_screen_size() 49 | 50 | @property 51 | def width(self): 52 | """The width of the screen.""" 53 | return self.size[0] 54 | 55 | @property 56 | def height(self): 57 | """The height of the screen.""" 58 | return self.size[1] 59 | 60 | @property 61 | def min(self): 62 | return min(self.size) 63 | 64 | @property 65 | def max(self): 66 | return max(self.size) 67 | 68 | def __iter__(self): 69 | """This allows the min and max functions to work on this class.""" 70 | return iter(self.size) 71 | 72 | @property 73 | def orientation(self): 74 | """Get a numerical value representing the screen's orientation.""" 75 | # This doesn't use UIDevice because my approach is simpler, and 76 | # accounts for rotation lock automatically. 77 | return _ScreenOrientation(app.statusBarOrientation()) 78 | 79 | @property 80 | def portrait(self): 81 | return self.orientation.portrait 82 | 83 | @property 84 | def landscape(self): 85 | return self.orientation.landscape 86 | 87 | @property 88 | def is_retina(self): 89 | return UIScreen.mainScreen().scale() == 2.0 90 | 91 | def __repr__(self): 92 | a = "Retina screen" if self.is_retina else "Screen" 93 | b = str(self.size) 94 | c = "with the {} side down".format(self.orientation) 95 | return " ".join((a, b, c)) 96 | 97 | screen = Screen() 98 | -------------------------------------------------------------------------------- /ui2/shapes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Easily draw different shapes and polygons using Pythonista's UI module. 3 | """ 4 | 5 | from math import sin, cos, pi 6 | 7 | import ui 8 | 9 | 10 | # HELPER METHODS 11 | 12 | def _polar2cart(radius, theta): 13 | """ Convert polar coordinates to cartesian coordinates """ 14 | theta = theta / (180 / pi) # Degrees to radians 15 | return (radius * cos(theta), radius * sin(theta)) 16 | 17 | 18 | # MAIN METHODS 19 | 20 | def get_polygon_path(points): 21 | """ Get a ui.Path object that connects a list of points with straight lines 22 | to form a closed figure """ 23 | p = ui.Path() 24 | # Move to first point 25 | p.move_to(*points[0]) 26 | # Begin path, drawing line to the rest 27 | for point in points[1:]: 28 | p.line_to(*point) 29 | # close the shape 30 | p.close() 31 | 32 | return p 33 | 34 | 35 | def get_regular_polygon_points(n, center=(20, 20), radius=20, rotation=0): 36 | """ Get the points to form a regular polygon given the number of sides, the 37 | center, the radius. The polygon will be drawn so that the base is flat 38 | horizontal, unless `rotation` is specified, in which case the polygon will 39 | be rotated by `rotation` degrees clockwise from the original """ 40 | if n < 3: 41 | raise ValueError("A polygon must have at least 3 points") 42 | # Adjust for flat bottom, then add 90 because 0 is straight right in the 43 | # standard polar coordinate system 44 | rotation += 360.0 / n / 2 + 90 45 | # The polar coordinate values of theta at which the points go 46 | degree_intervals = [360.0 / n * i + rotation for i in range(n)] 47 | # The cartesian coordinates where the points go 48 | points = [_polar2cart(radius, theta) for theta in degree_intervals] 49 | points = [(center[0] + p[0], center[1] + p[1]) for p in points] 50 | 51 | return points 52 | 53 | 54 | def get_regular_polygon_path(*args, **kwargs): 55 | """ Get a ui.Path for a regular polygon. See 'get_regular_polygon_points' 56 | for description of arguments """ 57 | return get_polygon_path(get_regular_polygon_points(*args, **kwargs)) 58 | 59 | 60 | def draw_shape_from_dict(): 61 | """ Draw a shape from a dict, as it is stored in .pyui2 files """ 62 | raise NotImplementedError() 63 | -------------------------------------------------------------------------------- /ui2/statusbar.py: -------------------------------------------------------------------------------- 1 | from objc_util import * 2 | from ui import parse_color 3 | 4 | app = UIApplication.sharedApplication() 5 | rootvc = app.keyWindow().rootViewController() 6 | 7 | class StatusBar(object): 8 | """The system status bar.""" 9 | 10 | status = app.statusBar() 11 | 12 | # Color 13 | 14 | @property 15 | def color(self): 16 | """Get the color of the status bar. 17 | 18 | 0 is black, 1 is white. 19 | """ 20 | return rootvc.statusBarStyle() 21 | 22 | @color.setter 23 | def color(self, color): 24 | """Change the text color of the status bar.""" 25 | if color not in (0, 1): 26 | raise ValueError("Color must be 0 (black) or 1 (white)") 27 | rootvc.setStatusBarStyle_(color) 28 | rootvc.setNeedsStatusBarAppearanceUpdate() 29 | 30 | # Background color 31 | 32 | @property 33 | def background_color(self): 34 | if self.status.backgroundColor() == None: 35 | return (0.0, 0.0, 0.0, 0.0) 36 | else: 37 | color = self.status.backgroundColor() 38 | return color.red(), color.green(), color.blue(), color.alpha() 39 | 40 | @background_color.setter 41 | def background_color(self, color): 42 | rgba = parse_color(color) 43 | self.status.setBackgroundColor_( 44 | UIColor.colorWithRed_green_blue_alpha_(*rgba) 45 | ) 46 | 47 | # Visibility 48 | 49 | def _set_visibility(self, should_show): 50 | """Hide or show the status bar.""" 51 | self.status.hidden = not should_show 52 | 53 | @property 54 | def is_visible(self): 55 | return not self.status.isHidden( ) 56 | 57 | def hide(self): 58 | """Hide the status bar.""" 59 | self._set_visibility(False) 60 | 61 | def show(self): 62 | """Show the status bar.""" 63 | self._set_visibility(True) 64 | 65 | def toggle(self): 66 | """Toggle the status bar.""" 67 | self._set_visibility(False if self.is_visible else True) 68 | 69 | # Other 70 | 71 | def reset(self): 72 | self.background_color = "clear" 73 | self.color = 1 74 | self.show() 75 | 76 | # The only instance ever needed. There's no point in having multiple. StatusBar 77 | # is only a class so that "@property"s can be used. 78 | statusbar = StatusBar() 79 | -------------------------------------------------------------------------------- /ui2/subclassing/__init__.py: -------------------------------------------------------------------------------- 1 | """A submodule for helping to "subclass" ui.View objects.""" 2 | 3 | import objc_util 4 | import ui 5 | from ui2.subclassing import proxies 6 | 7 | 8 | class ViewClassProxy(proxies.TypeProxy, ui.View): 9 | """A complete ui.View proxy, almost indistinguishible from the view it 10 | wraps. 11 | 12 | This essentially works by encapsulating the uninheritable view in a 13 | container view. The container is its own view, but it's transparent, and 14 | all access to this object is delegated to the wrapped view. So although the 15 | ViewClassProxy is its own view with its own properties, thse attributes 16 | can't be easily accessed, all access is forwarded to the contained view. 17 | 18 | For example, trying to access a ViewClassProxy's 'frame' will transparently 19 | return the contained view's frame instead, and setting it will leave the 20 | ViewClassProxy's frame untouched while instead modifying the contained 21 | view's frame. 22 | """ 23 | 24 | def __init__(self, view_type, *args, **kwargs): 25 | super().__init__(view_type, *args, **kwargs) 26 | # Add subject as a subview 27 | object.__getattribute__(self, "add_subview")(self.__subject__) 28 | # Turn off clipping of subviews so that the wrapped view is visible. 29 | ptr = object.__getattribute__(self, "_objc_ptr") 30 | 31 | class ObjCDummy(): 32 | """Used to trick ObjCInstance into working on this proxy class.""" 33 | _objc_ptr = ptr 34 | objc = objc_util.ObjCInstance(ObjCDummy()) 35 | 36 | 37 | class ViewClassWrapper(ViewClassProxy, proxies.AbstractWrapper): 38 | """A complete ui.View proxy, almost indistinguishible from the view it 39 | wraps. See ViewClassProxy for details.""" 40 | pass 41 | 42 | 43 | def subclassable(view_type): 44 | """Return an inheritable version of an uninheritable ui.View class.""" 45 | class ViewWrapper(ViewClassWrapper): 46 | def __init__(self, *args, **kwargs): 47 | super().__init__(view_type, *args, **kwargs) 48 | return ViewWrapper 49 | -------------------------------------------------------------------------------- /ui2/subclassing/proxies.py: -------------------------------------------------------------------------------- 1 | """Platform-indpendent extensions to ProxyTypes.""" 2 | from ui2.subclassing.proxytypes import AbstractProxy, AbstractWrapper 3 | 4 | 5 | class TypeProxy(AbstractProxy): 6 | """Delegates all operations to an instance of another class.""" 7 | def __init__(self, obj_type, *args, **kwargs): 8 | if not isinstance(obj_type, type): 9 | raise ValueError("argument obj_type must be a class") 10 | self.__subject__ = obj_type(*args, **kwargs) 11 | 12 | 13 | class TypeWrapper(TypeProxy, AbstractWrapper): 14 | """Consumes a class, allowing extra methods to be added.""" 15 | pass 16 | -------------------------------------------------------------------------------- /ui2/subclassing/proxytypes.py: -------------------------------------------------------------------------------- 1 | """A modified copy of ProxyTypes 0.9 (https://pypi.io/project/ProxyTypes/).""" 2 | 3 | """ 4 | ========== NOTICE OF MODIFICATION ========== 5 | 6 | This version HAS BEEN MODIFIED from the original 'proxies.py' file by Luke Deen 7 | Taylor. The original file was published on July 20, 2006. 8 | 9 | Modifications made on July 18, 2016: 10 | - Rewriting for compliance with the PEP 8 style guide 11 | - Supporting for Python 3 12 | - Movinging from the old format syntax (%) to the newer .format() syntax. 13 | Modifications made on July 19, 2016: 14 | - Removing CallbackProxy, LazyProxy, CallbackWrapper, and LazyWrapper 15 | - Removing use of __slots__ because of conflicts 16 | - Renaming this file from proxies.py to proxytypes.py 17 | 18 | Overall, these modifications serve as a clean-up and removal of classes I don't 19 | need, rather than a change to the functionality or structure of the code that 20 | remains after my removals. 21 | 22 | =========== ORIGINAL AUTHORSHIP AND LICENSING ========== 23 | 24 | ProxyTypes was originally written by Phillip J. Eby, and is ZPL licensed. 25 | 26 | The ZPL is as follows: 27 | 28 | Zope Public License (ZPL) Version 2.0 29 | ----------------------------------------------- 30 | 31 | This software is Copyright (c) Zope Corporation (tm) and 32 | Contributors. All rights reserved. 33 | 34 | This license has been certified as open source. It has also 35 | been designated as GPL compatible by the Free Software 36 | Foundation (FSF). 37 | 38 | Redistribution and use in source and binary forms, with or 39 | without modification, are permitted provided that the 40 | following conditions are met: 41 | 42 | 1. Redistributions in source code must retain the above 43 | copyright notice, this list of conditions, and the following 44 | disclaimer. 45 | 46 | 2. Redistributions in binary form must reproduce the above 47 | copyright notice, this list of conditions, and the following 48 | disclaimer in the documentation and/or other materials 49 | provided with the distribution. 50 | 51 | 3. The name Zope Corporation (tm) must not be used to 52 | endorse or promote products derived from this software 53 | without prior written permission from Zope Corporation. 54 | 55 | 4. The right to distribute this software or to use it for 56 | any purpose does not give you the right to use Servicemarks 57 | (sm) or Trademarks (tm) of Zope Corporation. Use of them is 58 | covered in a separate agreement (see 59 | http://www.zope.com/Marks). 60 | 61 | 5. If any files are modified, you must cause the modified 62 | files to carry prominent notices stating that you changed 63 | the files and the date of any change. 64 | 65 | Disclaimer 66 | 67 | THIS SOFTWARE IS PROVIDED BY ZOPE CORPORATION ``AS IS'' 68 | AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT 69 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 70 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 71 | NO EVENT SHALL ZOPE CORPORATION OR ITS CONTRIBUTORS BE 72 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 73 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 74 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 75 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 76 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 77 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 78 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 79 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 80 | DAMAGE. 81 | 82 | 83 | This software consists of contributions made by Zope 84 | Corporation and many individuals on behalf of Zope 85 | Corporation. Specific attributions are listed in the 86 | accompanying credits file. 87 | """ 88 | 89 | 90 | class AbstractProxy(object): 91 | """Delegates all operations (except ``.__subject__``) to another object.""" 92 | 93 | # Delegate getting, setting, and deleting attributes 94 | 95 | def __getattribute__(self, attr, oga=object.__getattribute__): 96 | subject = oga(self, "__subject__") 97 | if attr == "__subject__": 98 | return subject 99 | return getattr(subject, attr) 100 | 101 | def __setattr__(self, attr, val, osa=object.__setattr__): 102 | if attr == "__subject__": 103 | osa(self, attr, val) 104 | else: 105 | setattr(self.__subject__, attr, val) 106 | 107 | def __delattr__(self, attr, oda=object.__delattr__): 108 | if attr == "__subject__": 109 | oda(self, attr) 110 | else: 111 | delattr(self.__subject__, attr) 112 | 113 | # Delegate the getting, setting, and deleting of items with [] 114 | 115 | def __getitem__(self, arg): 116 | return self.__subject__[arg] 117 | 118 | def __setitem__(self, arg, val): 119 | self.__subject__[arg] = val 120 | 121 | def __delitem__(self, arg): 122 | del self.__subject__[arg] 123 | 124 | # Delegate the getting, setting, and deleting of slices with [] 125 | 126 | def __getslice__(self, i, j): 127 | return self.__subject__[i:j] 128 | 129 | def __setslice__(self, i, j, val): 130 | self.__subject__[i:j] = val 131 | 132 | def __delslice__(self, i, j): 133 | del self.__subject__[i:j] 134 | 135 | # Delegate calling 136 | 137 | def __call__(self, *args, **kwargs): 138 | return self.__subject__(*args, **kwargs) 139 | 140 | # Delegate true/false testing 141 | 142 | def __nonzero__(self): 143 | return bool(self.__subject__) 144 | 145 | # Delegate the 'in' operator 146 | 147 | def __contains__(self, ob): 148 | return ob in self.__subject__ 149 | 150 | # Delegate magic methods with no arguments 151 | 152 | for name in ("repr", "str", "hash", "len", "abs", "complex", "int", "long", 153 | "float", "iter", "oct", "hex"): 154 | exec(("def __{}__(self):" 155 | " return {}(self.__subject__)").format(name, name)) 156 | 157 | for name in "cmp", "coerce", "divmod": 158 | exec(("def __{}__(self, ob):" 159 | " return {}(self.__subject__, ob)").format(name, name)) 160 | 161 | # Delegate comparison operators 162 | 163 | for name, operator in [ 164 | ("lt", "<"), ("gt", ">"), ("le", "<="), ("ge", ">="), 165 | ("eq", "=="), ("ne", "!=") 166 | ]: 167 | exec(("def __{}__(self, ob):" 168 | " return self.__subject__ {} ob").format(name, operator)) 169 | 170 | # Delegate unary operators 171 | 172 | for name, op in [("neg", "-"), ("pos", "+"), ("invert", "~")]: 173 | exec(("def __{}__(self):" 174 | " return {} self.__subject__").format(name, op)) 175 | 176 | # Delegate arithmetic, bitwise, and shift operators 177 | 178 | for name, op in [ 179 | ("or", "|"), ("and", "&"), ("xor", "^"), # Bitwise operators 180 | ("lshift", "<<"), ("rshift", ">>"), # Shift operators 181 | ("add", "+"), ("sub", "-"), ("mul", "*"), ("div", "/"), # Arithmetic 182 | ("mod", "%"), ("truediv", "/"), ("floordiv", "//") # Weird arithmetic 183 | ]: 184 | exec("\n".join([ 185 | "def __{0}__(self, ob):", 186 | " return self.__subject__ {1} ob", 187 | 188 | "def __r{0}__(self, ob):", 189 | " return ob {1} self.__subject__", 190 | 191 | "def __i{0}__(self, ob):", 192 | " self.__subject__ {1}= ob", 193 | " return self" 194 | ]).format(name, op)) 195 | 196 | del name, op 197 | 198 | # Oddball signatures 199 | 200 | def __rdivmod__(self, ob): 201 | return divmod(ob, self.__subject__) 202 | 203 | def __pow__(self, *args): 204 | return pow(self.__subject__, *args) 205 | 206 | def __ipow__(self, ob): 207 | self.__subject__ **= ob 208 | return self 209 | 210 | def __rpow__(self, ob): 211 | return pow(ob, self.__subject__) 212 | 213 | 214 | class ObjectProxy(AbstractProxy): 215 | """Proxy for a specific object.""" 216 | 217 | def __init__(self, subject): 218 | self.__subject__ = subject 219 | 220 | 221 | class AbstractWrapper(AbstractProxy): 222 | """Mixin to allow extra behaviors and attributes on proxy instance.""" 223 | def __getattribute__(self, attr, oga=object.__getattribute__): 224 | if attr.startswith("__"): 225 | subject = oga(self, "__subject__") 226 | if attr == "__subject__": 227 | return subject 228 | return getattr(subject, attr) 229 | return oga(self, attr) 230 | 231 | def __getattr__(self, attr, oga=object.__getattribute__): 232 | return getattr(oga(self, "__subject__"), attr) 233 | 234 | def __setattr__(self, attr, val, osa=object.__setattr__): 235 | if ( 236 | attr == "__subject__" or 237 | hasattr(type(self), attr) and not 238 | attr.startswith("__") 239 | ): 240 | osa(self, attr, val) 241 | else: 242 | setattr(self.__subject__, attr, val) 243 | 244 | def __delattr__(self, attr, oda=object.__delattr__): 245 | if ( 246 | attr == "__subject__" or 247 | hasattr(type(self), attr) and not attr.startswith("__") 248 | ): 249 | oda(self, attr) 250 | else: 251 | delattr(self.__subject__, attr) 252 | 253 | 254 | class ObjectWrapper(ObjectProxy, AbstractWrapper): 255 | pass 256 | -------------------------------------------------------------------------------- /ui2/transition.py: -------------------------------------------------------------------------------- 1 | from objc_util import * 2 | 3 | # Constants 4 | TRANSITION_NONE = 0 << 20 5 | TRANSITION_FLIP_FROM_TOP = 6 << 20 6 | TRANSITION_FLIP_FROM_RIGHT = 2 << 20 7 | TRANSITION_FLIP_FROM_BOTTOM = 7 << 20 8 | TRANSITION_FLIP_FROM_LEFT = 1 << 20 9 | TRANSITION_CURL_UP = 3 << 20 10 | TRANSITION_CURL_DOWN = 4 << 20 11 | TRANSITION_CROSS_DISSOLVE = 5 << 20 12 | 13 | 14 | class Transition(object): 15 | """Represents a transition between two views.""" 16 | def __init__(self, view1, view2, effect=TRANSITION_FLIP_FROM_RIGHT, 17 | duration=0.25, completion=None): 18 | self.view1 = view1 19 | self.view2 = view2 20 | self.effect = effect 21 | self.duration = duration 22 | self.completion = completion 23 | 24 | self._completion = None 25 | 26 | def play(self): 27 | """Perform the transition.""" 28 | # Make frames match, it's way cleaner. 29 | self.view2.frame = self.view1.frame 30 | 31 | # If any callbacks are set, we set up a block 32 | funcs = (self.completion, self._completion) # Possible callbacks 33 | if any(funcs): 34 | def c(cmd, success): 35 | """A completion function wrapping one or more callbacks.""" 36 | for func in funcs: 37 | if func: # Only call the registered ones 38 | func(success) 39 | # Lets the function be garbage collected when it's safe 40 | release_global(ObjCInstance(cmd)) 41 | oncomplete = ObjCBlock(c, argtypes=[c_void_p, c_bool]) 42 | # This prevents the oncomplete function from being garbage 43 | # collected as soon as the play() function exits 44 | retain_global(oncomplete) 45 | else: 46 | oncomplete = None 47 | 48 | # Perform the transition 49 | UIView.transitionFromView_toView_duration_options_completion_( 50 | self.view1, self.view2, self.duration, self.effect, oncomplete 51 | ) 52 | 53 | 54 | class ChainedTransitionComponent(Transition): 55 | def __init__(self, transition_obj, next_transition): 56 | """A single step in a chain of transitions.""" 57 | self.transition_obj = transition_obj 58 | self.next_transition = next_transition 59 | 60 | self.view1 = self.transition_obj.view1 61 | self.view2 = self.transition_obj.view2 62 | self.effect = self.transition_obj.effect 63 | self.duration = self.transition_obj.duration 64 | 65 | self._completion = None 66 | 67 | def completion(self, success): 68 | # If it has a completion function already, run it first. 69 | if self.transition_obj.completion is not None: 70 | self.transition_obj.completion(success) 71 | # Then play the next transition if we're not at the end of the chain 72 | if self.next_transition is not None: 73 | self.next_transition.play() 74 | 75 | 76 | class ChainedTransition(object): 77 | """Represents a series of several transitions to be played in sequence.""" 78 | def __init__(self, *transitions, completion=None): 79 | self.completion = completion 80 | 81 | transits = [] 82 | for i, a in reversed(list(enumerate(transitions))): 83 | if i == len(transitions) - 1: 84 | # This is the last element in the chain (first in iteration), 85 | # so it has no successor. We can use the old Animation object. 86 | transits.append(a) 87 | else: 88 | transits.append(ChainedTransitionComponent(a, transits[-1])) 89 | 90 | self.transitions = transits[::-1] 91 | 92 | # Register the completion event on the final component 93 | 94 | if self.completion is not None: 95 | self.transitions[-1]._completion = self.completion 96 | 97 | def play(self): 98 | """Perform the transitions.""" 99 | self.transitions[0].play() 100 | 101 | 102 | def transition(*args, **kwargs): 103 | """Transition from one view to another.""" 104 | Transition(*args, **kwargs).play() 105 | -------------------------------------------------------------------------------- /ui2/ui_io.py: -------------------------------------------------------------------------------- 1 | """ Utilities for writing and reading custom info from .pyui files """ 2 | 3 | 4 | import json 5 | import uuid 6 | 7 | 8 | def _json_get(inp): 9 | """ Get a Python object (list or dict) regardless of whether data is passed 10 | as a JSON string, a file path, or is already a python object. 11 | 12 | Returns the parsed data, as well as "native" if the data was already a 13 | Python object, "str" if the data was passed as a JSON string, or the path 14 | if the data passed was a file path """ 15 | 16 | if not (isinstance(inp, dict) or isinstance(inp, list)): # Python object 17 | try: # JSON string 18 | data = json.loads(inp) 19 | dataformat = "str" 20 | except json.JSONDecodeError: # JSON filepath 21 | # Store the filename in the dataformat variable if dataformat is a 22 | # file, because it's just one fewer variable to keep track of 23 | dataformat = inp 24 | with open(inp, encoding="utf-8") as f: 25 | data = json.load(f) 26 | else: 27 | dataformat = "native" 28 | 29 | return data, dataformat 30 | 31 | 32 | def _embed_custom_attributes(inp, data): 33 | """ ui2 stores metadata by embedding a `ui2` key in the `attributes` dict 34 | of the base view. This function automates the storing of said "custom 35 | attributes." Pass a JSON string, a dict, or the path to a pyui file """ 36 | 37 | # Load dict, storing the format of `.pyui` used (JSON filename, 38 | # JSON string, or Python object) 39 | pyui, dataformat = _json_get(inp) 40 | 41 | pyui[0]["attributes"]["ui2"] = data 42 | 43 | if dataformat == "native": 44 | return pyui 45 | elif dataformat == "str": 46 | return json.dumps(pyui, indent=2) 47 | else: 48 | with open(dataformat, "w", encoding="utf-8") as f: 49 | json.dump(pyui, f, indent=2) 50 | 51 | 52 | def _get_custom_attributes(pyui): 53 | pyui, dataformat = _json_get(pyui) 54 | return pyui[0]["attributes"]["ui2"] 55 | 56 | 57 | def _view_to_dict(view): 58 | """ The magical solution to store ui.View instances as pyui files """ 59 | # The base attributes shared across all views 60 | out = { 61 | "selected": False, 62 | # This is a scary amount of curly braces, but Python format syntax 63 | # requires double curly braces for one to appear in the result. 64 | "frame": "{{{{{}, {}}}, {{{}, {}}}}}".format(int(view.frame.min_x), 65 | int(view.frame.min_y), 66 | int(view.frame.width), 67 | int(view.frame.height)), 68 | "class": view.__class__.__name__, 69 | "nodes": [_view_to_dict(sv) for sv in view.subviews], 70 | "attributes": {} 71 | } 72 | 73 | # Add the strange attributes. Some of these are duplicate properties, and 74 | # some are never used at all as far as I can tell (like 'uuid') I'm 75 | # including them to be safe, though. 76 | out["attributes"]["uuid"] = str(uuid.uuid4()) 77 | out["attributes"]["class"] = view.__class__.__name__ 78 | out["attributes"]["frame"] = out["frame"] 79 | 80 | # Add the easy attributes 81 | attrs = ["custom_class", "root_view_name", "background_color", 82 | "title_color", "title_bar_color", "flex", "alpha", "name", 83 | "tint_color", "border_width", "border_color", "corner_radius", 84 | "font_name", "font_size", "alignment", "number_of_lines", 85 | "text_color", "text", "placeholder", "autocorrection_type", 86 | "spellchecking_type", "secure", "editable", "image_name", 87 | "font_bold", "action", "continuous", "value", "segments", "title", 88 | "scales_to_fit", "row_height", "editing", "data_source_items", 89 | "data_source_action", "data_source_edit_action", 90 | "data_source_accessory_action", "data_source_font_size", 91 | "data_source_move_enabled", "data_source_number_of_lines", "mode", 92 | "content_width", "content_height", ("image", "image_name")] 93 | # Tuples are used to indicate when an attribute has a different name in the 94 | # pyui file than it does in an actual object. We convert everything to a 95 | # tuple for convenience. 96 | attrs = [a if isinstance(a, tuple) else (a,) * 2 for a in attrs] 97 | 98 | # Edge cases 99 | if hasattr(view, "background_color"): 100 | out["attributes"]["background_color"] = "RGBA({},{},{},{})".format( 101 | *view.background_color 102 | ) 103 | 104 | # This is mostly robust, though there are a few uncovered edge cases 105 | for attr_name in attrs: 106 | if hasattr(view, attr_name[0]): 107 | attr = getattr(view, attr_name[0]) 108 | if not isinstance(attr, (int, float, bool, str, type(None))): 109 | attr = str(attr) 110 | if (attr_name[0] not in out["attributes"] and 111 | attr not in (None, "")): 112 | out["attributes"][attr_name[1]] = attr 113 | 114 | return out 115 | 116 | 117 | def dump_view(view, path): 118 | """ The reverse of `ui.load_view()` """ 119 | with open(path, "w") as f: 120 | json.dump(_view_to_dict(view), f) 121 | 122 | 123 | if __name__ == "__main__": 124 | import ui 125 | a = ui.View() 126 | a.background_color = "#fff" 127 | 128 | button = ui.Button() 129 | button.title = "Hey, it's a thing!" 130 | 131 | a.add_subview(button) 132 | 133 | b = ui._view_from_dict(_view_to_dict(a), globals(), locals()) 134 | 135 | assert b.subviews[0].title == "Hey, it's a thing!" 136 | print("Successfully converted ui.View to a dict and back!") 137 | -------------------------------------------------------------------------------- /ui2/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/controversial/ui2/a06604afee2406efa7538dfa05e93174a10ad932/ui2/utils/__init__.py -------------------------------------------------------------------------------- /ui2/utils/tables.py: -------------------------------------------------------------------------------- 1 | """Internal utilities used in ui2.""" 2 | 3 | def make_table(rows, columns=None): 4 | """Create an ASCII table and return it as a string. 5 | 6 | Pass a list of dicts to represent rows in the table and a list of strings 7 | to represent columns. The strings in 'columns' will be used as the keys to 8 | the dicts in 'rows.' 9 | 10 | Not all column values have to be present in each row dict. 11 | 12 | >>> print(make_table([{"a": 1, "b": "test"}, {"yay": 16}])) 13 | +----------------+ 14 | | a | b | yay | 15 | |================| 16 | | 1 | test | | 17 | | | | 16 | 18 | +----------------+ 19 | """ 20 | # If columns aren't specified, use all keys in the rows, alphabetized 21 | if columns is None: 22 | columns = set() 23 | for r in rows: 24 | columns.update(r.keys()) 25 | columns = sorted(list(columns)) 26 | 27 | # If there are no rows, add a blank one to make it prettier 28 | if not rows: 29 | rows = [{}] 30 | 31 | # Calculate how wide each cell needs to be 32 | cell_widths = {} 33 | for c in columns: 34 | values = [str(r.get(c, "")) for r in rows] 35 | cell_widths[c] = len(max(values + [c], key=len)) 36 | 37 | # Used for formatting rows 38 | row_template = "|" + " {} |" * len(columns) 39 | 40 | # CONSTRUCT THE TABLE 41 | 42 | # The top row with the column titles 43 | justified_column_heads = [c.ljust(cell_widths[c]) for c in columns] 44 | header = row_template.format(*justified_column_heads) 45 | # The second row contains separators 46 | sep = "|" + "=" * (len(header) - 2) + "|" 47 | # Rows of data 48 | lines = [] 49 | for r in rows: 50 | fields = [str(r.get(c, "")).ljust(cell_widths[c]) for c in columns] 51 | line = row_template.format(*fields) 52 | lines.append(line) 53 | 54 | # Borders go on the top and the bottom 55 | border = "+" + "-" * (len(header) - 2) + "+" 56 | return "\n".join([border, header, sep] + lines + [border]) 57 | -------------------------------------------------------------------------------- /ui2/view_classes/BlurView.py: -------------------------------------------------------------------------------- 1 | from objc_util import * 2 | import ui 3 | 4 | 5 | class BlurView(ui.View): 6 | """A class for applying a heavy iOS-style blur to elements behind it.""" 7 | def __init__(self, dark=False, *args, **kwargs): 8 | super().__init__(self, *args, **kwargs) 9 | 10 | self._objc = ObjCInstance(self) 11 | self.background_color = 'clear' 12 | self._effect_view = ObjCClass('UIVisualEffectView').new() 13 | self._effect_view.setFrame_(self._objc.frame()) 14 | # Flexible width and height 15 | self._effect_view.setAutoresizingMask_((1 << 1) + (1 << 4)) 16 | 17 | self._set_dark(dark) 18 | self._objc.addSubview_(self._effect_view) 19 | 20 | @on_main_thread 21 | def _set_dark(self, dark=True): 22 | """Set whether the blur is dark or light.""" 23 | style = 2 if dark else 1 24 | self._effect = ObjCClass('UIBlurEffect').effectWithStyle(style) 25 | self._effect_view.setEffect_(self._effect) 26 | 27 | @property 28 | def dark(self): 29 | return True if self._effect._style() == 2 else False 30 | 31 | @dark.setter 32 | def dark(self, value): 33 | self._set_dark(value) 34 | 35 | @property 36 | def light(self): 37 | return not self.dark 38 | 39 | @light.setter 40 | def light(self, value): 41 | self.dark = not value 42 | 43 | def toggle_brightness(self): 44 | """Toggle the view's brightness between light and dark.""" 45 | self.dark = not self.dark 46 | -------------------------------------------------------------------------------- /ui2/view_classes/CameraView.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | 3 | import objc_util 4 | import ui 5 | 6 | 7 | AVCaptureSession = objc_util.ObjCClass("AVCaptureSession") 8 | AVCaptureDevice = objc_util.ObjCClass("AVCaptureDevice") 9 | AVCaptureDeviceInput = objc_util.ObjCClass("AVCaptureDeviceInput") 10 | AVCaptureVideoPreviewLayer = objc_util.ObjCClass("AVCaptureVideoPreviewLayer") 11 | 12 | 13 | class UnsupportedDeviceError(Exception): 14 | """Raised if your iOS device isn't supported.""" 15 | pass 16 | 17 | 18 | class CameraView(ui.View): 19 | def __init__(self, *args, **kwargs): 20 | super().__init__(self, *args, **kwargs) 21 | self._objc = objc_util.ObjCInstance(self) 22 | 23 | # Set up the camera device 24 | self._session = AVCaptureSession.new() 25 | camera_type = ctypes.c_void_p.in_dll(objc_util.c, "AVMediaTypeVideo") 26 | device = AVCaptureDevice.defaultDeviceWithMediaType(camera_type) 27 | input = AVCaptureDeviceInput.deviceInputWithDevice_error_(device, None) 28 | if input: 29 | self._session.addInput(input) 30 | else: 31 | raise UnsupportedDeviceError("Failed to connect to camera.") 32 | 33 | # Add the camera preview layer 34 | self._layer = self._objc.layer() 35 | self._camera_layer = AVCaptureVideoPreviewLayer.layerWithSession( 36 | self._session 37 | ) 38 | self._layer.addSublayer(self._camera_layer) 39 | 40 | self._auto_rotating = True 41 | 42 | @ui.in_background # Apple recommends this at http://apple.co/2a1Ayca 43 | def start(self): 44 | """Start the capture.""" 45 | self._session.startRunning() 46 | 47 | def stop(self): 48 | """Stop the capture.""" 49 | self._session.stopRunning() 50 | 51 | @property 52 | def running(self): 53 | return self._session.running() 54 | 55 | def layout(self): 56 | """Called when the view's size changes to resize the layer.""" 57 | self._camera_layer.setFrame(((0, 0), (self.width, self.height))) 58 | -------------------------------------------------------------------------------- /ui2/view_classes/MapView.py: -------------------------------------------------------------------------------- 1 | import objc_util 2 | import ui 3 | 4 | 5 | # CTYPES 6 | 7 | 8 | class _CoordinateRegion (objc_util.Structure): 9 | _fields_ = [ 10 | ("center", objc_util.CGPoint), 11 | ("span", objc_util.CGSize) 12 | ] 13 | 14 | 15 | # MAIN INTERFACE 16 | 17 | 18 | class MapView(ui.View): 19 | @objc_util.on_main_thread 20 | def __init__(self, *args, **kwargs): 21 | super().__init__(self, *args, **kwargs) 22 | 23 | self._view = objc_util.ObjCClass("MKMapView").new() 24 | self._view.setFrame_( 25 | objc_util.CGRect( 26 | objc_util.CGPoint(0, 0), 27 | objc_util.CGSize(self.width, self.height) 28 | ) 29 | ) 30 | self._view.setAutoresizingMask_(18) # W+H 31 | objc_util.ObjCInstance(self).addSubview_(self._view) 32 | 33 | self.animated = True 34 | 35 | @property 36 | def bounds(self): 37 | """The upper left and lower right coordinates of the visible map area 38 | as latitude/longitude values.""" 39 | center = self._view.region().a.a, self._view.region().a.b 40 | span = self._view.region().b.a, self._view.region().b.b 41 | min_x = center[0] - span[0] / 2 42 | min_y = center[1] - span[1] / 2 43 | max_x = center[0] + span[0] / 2 44 | max_y = center[1] + span[1] / 2 45 | 46 | return min_x, min_y, max_x, max_y 47 | 48 | @bounds.setter 49 | def bounds(self, coords): 50 | """get the upper left and lower right coordinates of the visible map 51 | area with latitude/longitude values.""" 52 | topleft, bottomright = coords[:2], coords[2:] 53 | 54 | min_x, min_y = topleft 55 | max_x, max_y = bottomright 56 | span = ( 57 | max_x - min_x, 58 | max_y - min_y 59 | ) 60 | center = ( 61 | min_x + span[0] / 2, 62 | min_y + span[1] / 2 63 | ) 64 | self._view.setRegion_animated_( 65 | _CoordinateRegion( 66 | objc_util.CGPoint(*center), 67 | objc_util.CGSize(*span), 68 | ), 69 | self.animated, 70 | 71 | restype=None, argtypes=[_CoordinateRegion, objc_util.c_bool] 72 | ) 73 | 74 | @property 75 | def center(self): 76 | coord = self._view.centerCoordinate() 77 | return coord.a, coord.b 78 | 79 | @center.setter 80 | def center(self, coords): 81 | self._view.setCenterCoordinate_animated_( 82 | objc_util.CGPoint(coords[0], coords[1]), 83 | self.animated, 84 | restype=None, 85 | argtypes=[objc_util.CGPoint, objc_util.c_bool] 86 | ) 87 | -------------------------------------------------------------------------------- /ui2/view_classes/PathView.py: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui2 3 | 4 | 5 | class PathView(ui.View): 6 | """ A class for displaying a ui.Path inside a ui.View, which automatically 7 | scales and moves the path when you change the frame """ 8 | def __init__(self, path, color="black", shadow=("black", 0, 0, 0), *args, 9 | **kwargs): 10 | super().__init__(self, *args, **kwargs) 11 | 12 | # Store arguments 13 | self._path = path 14 | self._color = color 15 | self._shadow = shadow 16 | # Store other data (data that requires processing) 17 | self._pathsize = path.bounds.max_x, path.bounds.max_y 18 | self.width, self.height = self._pathsize 19 | 20 | def draw(self): 21 | # Set drawing attributes 22 | ui.set_color(self._color) 23 | ui.set_shadow(*self._shadow) 24 | # Calculations 25 | scale_x = self.width / self._pathsize[0] 26 | scale_y = self.height / self._pathsize[0] 27 | # Scale the path 28 | new_path = ui2.path_helpers.scale_path(self._path, (scale_x, scale_y)) 29 | new_path.fill() 30 | -------------------------------------------------------------------------------- /ui2/view_classes/ProgressPathView.py: -------------------------------------------------------------------------------- 1 | from objc_util import * 2 | import ui 3 | 4 | 5 | def _get_CGColor(color): 6 | """Get a CGColor from a wide range of formats.""" 7 | return UIColor.colorWithRed_green_blue_alpha_( 8 | *ui.parse_color(color) 9 | ).CGColor() 10 | 11 | 12 | class ProgressPathView(ui.View): 13 | """A view class which can turn a ui.Path into a progress bar. 14 | 15 | This allows you not only to create linear and circular progress bars, but 16 | to create progress bars of any shape """ 17 | def __init__(self, path, width=5, color="#21abed", 18 | show_track=True, track_width=5, track_color="#eee", 19 | *args, **kwargs): 20 | super().__init__(self, *args, **kwargs) 21 | 22 | # Draw the full path on one layer 23 | self._track_layer = ObjCClass("CAShapeLayer").new() 24 | self._track_layer.setFillColor_(UIColor.clearColor().CGColor()) 25 | self._track_layer.setStrokeColor_(_get_CGColor(track_color)) 26 | self._track_layer.setLineWidth_(track_width) 27 | self._track_layer.setPath_(ObjCInstance(path).CGPath()) 28 | ObjCInstance(self).layer().addSublayer_(self._track_layer) 29 | # Set up the layer on which the partial path is rendered 30 | self._layer = ObjCClass("CAShapeLayer").new() 31 | self._layer.setFillColor_(UIColor.clearColor().CGColor()) # No fill 32 | self._layer.setPath_(ObjCInstance(path).CGPath()) 33 | ObjCInstance(self).layer().addSublayer_(self._layer) 34 | 35 | self.progress = 0 # Progress starts at 0 36 | 37 | # Apply arguments 38 | self.color = color 39 | self.stroke_width = width 40 | 41 | self.track_color = track_color 42 | self.track_width = track_width 43 | 44 | if not show_track: 45 | self._track_layer.setOpacity_(0) 46 | 47 | 48 | # Update progress level 49 | 50 | @property 51 | def progress(self): 52 | return self._layer.strokeEnd() 53 | 54 | @progress.setter 55 | def progress(self, value): 56 | self._layer.setStrokeEnd_(value) 57 | self._layer.setNeedsDisplay() 58 | 59 | # Progress bar 60 | 61 | @property 62 | def stroke_width(self): 63 | return self._layer.lineWidth() 64 | 65 | @stroke_width.setter 66 | def stroke_width(self, width): 67 | self._layer.setLineWidth_(width) 68 | 69 | @property 70 | def color(self): 71 | color = UIColor.colorWithCGColor_(self._layer.strokeColor()) 72 | return color.red(), color.green(), color.blue(), color.alpha() 73 | 74 | @color.setter 75 | def color(self, color): 76 | self._layer.setStrokeColor_(_get_CGColor(color)) 77 | 78 | # Track 79 | 80 | @property 81 | def track_color(self): 82 | color = UIColor.colorWithCGColor_(self._track_layer.strokeColor()) 83 | return color.red(), color.green(), color.blue(), color.alpha() 84 | 85 | @track_color.setter 86 | def track_color(self, color): 87 | self._track_layer.setStrokeColor_(_get_CGColor(color)) 88 | 89 | @property 90 | def track_width(self): 91 | return self._track_layer.lineWidth() 92 | 93 | @track_width.setter 94 | def track_width(self, width): 95 | self._track_layer.setLineWidth_(width) 96 | 97 | @property 98 | def track_shown(self): 99 | return bool(self._track_layer.opacity()) 100 | 101 | def show_track(self): 102 | """Show the progress bar track.""" 103 | self._track_layer.setOpacity_(1) 104 | 105 | def hide_track(self): 106 | """Hide the progress bar track.""" 107 | self._track_layer.setOpacity_(0) 108 | 109 | def toggle_track(self): 110 | """Toggle the visibility of the track.""" 111 | self._track_layer.setOpacity_(0 if self.track_shown else 1) 112 | 113 | # Extras 114 | 115 | @property 116 | def is_complete(self): 117 | return self.progress == 1 118 | 119 | def complete(self): 120 | """Finish the progressbar.""" 121 | self.progress = 1 122 | -------------------------------------------------------------------------------- /ui2/view_classes/TableView.py: -------------------------------------------------------------------------------- 1 | """A high-level wrapper around the whole ui.TableView system.""" 2 | 3 | import ui 4 | import collections 5 | 6 | 7 | class Cell(): 8 | """A single cell in a ui.TableView. 9 | 10 | This class "subclasses" ui.TableViewCell by wrapping it. 11 | """ 12 | def __init__(self): 13 | self._cell = ui.TableViewCell() 14 | 15 | @property 16 | def accessory_type(self): 17 | return self._cell.accessory_type 18 | 19 | @accessory_type.setter 20 | def accessory_type(self, value): 21 | self._cell.accessory_type = value 22 | 23 | @property 24 | def content_view(self): 25 | return self._cell.content_view 26 | 27 | @property 28 | def detail_text_label(self): 29 | return self._cell.detail_text_label 30 | 31 | @property 32 | def image_view(self): 33 | return self._cell.image_view 34 | 35 | @property 36 | def selectable(self): 37 | return self._cell.selectable 38 | 39 | @selectable.setter 40 | def selectable(self, value): 41 | self._cell.selectable = value 42 | 43 | @property 44 | def selected_background_view(self): 45 | return self._cell.selected_background_view 46 | 47 | @selected_background_view.setter 48 | def selected_background_view(self, value): 49 | self._cell.selected_background_view = value 50 | 51 | @property 52 | def text_label(self): 53 | return self._cell.text_label 54 | 55 | 56 | class Section(collections.MutableSet): 57 | """A section inside a TableView. 58 | 59 | This contains TableView cells. 60 | """ 61 | def __init__(self, tableview): 62 | self.cells = set() 63 | self.tableview = tv 64 | 65 | def __contains__(self, item): 66 | return item in self.cells 67 | 68 | def __iter__(self): 69 | return iter(self.cells) 70 | 71 | def add(self, cell): 72 | self.cells.add(key) 73 | 74 | def discard(self, cell): 75 | self.cells.discard(cell) 76 | 77 | 78 | class TableView(collections.Container): 79 | """A view to display a list of items in a single column.""" 80 | def __init__(self): 81 | self.sections = [Section(self)] 82 | 83 | def __contains__(self, key): 84 | return key in self.sections 85 | -------------------------------------------------------------------------------- /ui2/view_classes/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------