omz:forum

    • Register
    • Login
    • Search
    • Recent
    • Popular
    1. Home
    2. mikael
    3. Best

    Welcome!

    This is the community forum for my apps Pythonista and Editorial.

    For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.


    • Profile
    • Following 0
    • Followers 20
    • Topics 97
    • Posts 1623
    • Best 353
    • Controversial 0
    • Groups 0

    Best posts made by mikael

    • [Share code] Gestures for Pythonista

      This is a convenience class for enabling gestures in Pythonista ui applications, including built-in views. Main intent here has been to make them Python friendly, hiding all the Objective-C stuff.

      Get it from GitHub.

      For example, do something when user swipes left on a TextView:

      def swipe_handler(view, swipe_start_location):
          print ‘I was swiped, starting from ‘ + str(swipe_start_location)
       
      tv = ui.TextView()
      gestures = Gestures()
      gestures.add_swipe(tv, swipe_handler, direction = Gestures.LEFT)
      

      These gestures and methods are provided:

      *add_tap(view, action, number_of_taps_required, number_of_touches_required)

      • add_long_press(view, action, number_of_taps_required, number_of_touches_required, minimum_press_duration, allowable_movement)
      • add_pan(view, action, minimum_number_of_touches, maximum_number_of_touches, set_translation)
      • add_screen_edge_pan(view, action, edges) (see below for possible edges values)
      • add_pinch(view, action)
      • add_rotation(view, action)
      • add_swipe(view, action, direction, number_of_touches_required) (see below for possible direction values)

      In all cases, only the view and action (event handler function like the swipe_handler in the example) are required. Refer to the UIKit UIGestureRecognizer documentation on usage and default values.

      Corresponding handler signatures are (representative names only, use whatever is convenient for you):

      • tap(view, location) - same for long presses and swipes (where the location is the where the swipe began)
      • pan(view, location, absolute_translation, velocity) - same for pans from the screen edges
      • pinch(view, location, scale, velocity)
      • rotation(view, location, rotation, velocity)

      scale, rotation and velocity are numbers. location and velocity values are ui.Point instances with x and y members.

      Possible screen edge pan edges values are (only one of these): Gestures.EDGE_NONE, Gestures.EDGE_TOP, Gestures.EDGE_LEFT, Gestures.EDGE_BOTTOM, Gestures.EDGE_RIGHT, Gestures.EDGE_ALL

      Possible swipe direction values are (one or a list of these; if you need to know the actual swipe direction, add different directions as separate gestures):Gestures.RIGHT, Gestures.LEFT, Gestures.UP,Gestures.DOWN

      All of the add_x methods return a recognizer object that can be used to remove or disable the gesture as needed:

      • remove(view, recognizer)
      • disable(recognizer)
      • enable(recognizer)

      You can also remove all gestures from a view with remove_all_gestures(view).

      NOTES:

      • To bridge the Objective-C and Pyyhon worlds, all the gestures depend on the Gestures instance used to create them being live, so retain the reference to it or Pythonista will crash when the gesture is detected.
      • Single Gestures instance can be used to add any number of gestures to any number of views.
      • If you need to create a lot of dynamic gestures in a long-running, make sure to explicitly remove them when no longer needed, to avoid a memory leak.
      posted in Pythonista
      mikael
      mikael
    • WKWebView - modern webview for Pythonista

      The underlying component used to implement ui.WebView in Pythonista is UIWebView, which has been deprecated since iOS 8. This module implements a Python webview API using the current iOS-provided view, WKWebView. Besides being Apple-supported, WKWebView brings other benefits such as better Javascript performance and an official communication channel from Javascript to Python. This implementation of a Python API also has the additional benefit of being inheritable.

      Available as a single file on GitHub. Run the file as-is to try out some of the capabilities; check the end of the file for demo code.

      Credits: This would not exist without @JonB and @mithrendal.

      Basic usage

      WKWebView matches ui.WebView API as defined in Pythonista docs. For example:

      v = WKWebView()
      v.present()
      v.load_html('<body>Hello world</body>')
      v.load_url('http://omz-software.com/pythonista/')
      

      For compatibility, there is also the same delegate API that ui.WebView has, with webview_should_start_load etc. methods.

      Mismatches with ui.WebView

      eval_js – synchronous vs. asynchronous JS evaluation

      Apple's WKWebView only provides an async Javascript evaliation function. This is available as an eval_js_async, with an optional callback argument that will called with a single argument containing the result of the JS evaluation (or None).

      Here we also provide a synchronous eval_js method, which essentially waits for the callback behind the scenes before returning the result. For this to work, you have to call the method outside the main UI thread, e.g. from a method decorated with ui.in_background.

      scales_page_to_fit

      UIWebView had such a property, WKWebView does not. It is likely that I will implement an alternative with script injection.

      Additional features and notes

      http allowed

      Looks like Pythonista has the specific plist entry required to allow fetching non-secure http urls.

      Swipe navigation

      There is a new property, swipe_navigation, False by default. If set to True, horizontal swipes navigate backwards and forwards in the browsing history.

      Note that browsing history is only updated for calls to load_url - load_html is ignored (Apple feature that has some security justification).

      Data detection

      By default, no data detectors are active for WKWebView. If there is demand, it is easy to add support for activating e.g. turning phone numbers automatically into links.

      Messages from JS to Python

      WKWebView comes with support for JS to container messages. Use this by subclassing WKWebView and implementing methods that start with on_ and accept one message argument. These methods are then callable from JS with the pithy window.webkit.messageHandler.<name>.postMessage call, where <name> corresponds to whatever you have on the method name after the on_ prefix.

      Here's a minimal yet working example:

      class MagicWebView(WKWebView):
        
        def on_magic(self, message):
          print('WKWebView magic ' + message)
          
      html = '<body><button onclick="window.webkit.messageHandlers.magic.postMessage(\'spell\')">Cast a spell</button></body>'
      
      v = MagicWebView()
      v.load_html(html)
      

      Note that the message argument is always a string. For structured data, you need to use e.g. JSON at both ends.

      User scripts a.k.a. script injection

      WKWebView supports defining JS scripts that will be automatically loaded with every page.

      Use the add_script(js_script, add_to_end=True) method for this.

      Scripts are added to all frames. Removing scripts is currently not implemented.

      Javascript exceptions

      WKWebView uses both user scripts and JS-to-Python messaging to report Javascript errors to Python, where the errors are simply printed out.

      Customize Javascript popups

      Javascript alert, confirm and prompt dialogs are now implemented with simple Pythonista equivalents. If you need something fancier or e.g. internationalization support, subclass WKWebView and re-implement the following methods as needed:

      def _javascript_alert(self, host, message):
        console.alert(host, message, 'OK', hide_cancel_button=True)
        
      def _javascript_confirm(self, host, message):
        try:
          console.alert(host, message, 'OK')
          return True
        except KeyboardInterrupt:
          return False
        
      def _javascript_prompt(self, host, prompt, default_text):
        try:
          return console.input_alert(host, prompt, default_text, 'OK')
        except KeyboardInterrupt:
          return None
      
      posted in Pythonista
      mikael
      mikael
    • RE: Can’t get present full screen to work in the latest beta

      @superrune, yes, it is a bit of a pain.

      The correct full screen argument is fullscreen, not full_sceen. Earlier the default was full screen, so it did not really matter which way you wrote it, but now it seems to be sheet (which I think is a bug).

      But anyway, a quick fix is to remove the underscore.

      posted in Pythonista
      mikael
      mikael
    • [Share code] ReminderStore - not what Apple intended

      Logo

      Link to Github

      From the readme:

      ReminderStore

      Key-value store using iOS Reminders for persistence and distribution across iOS devices.

      Introduction

      ReminderStore is a Pythonista persistence provider. It uses the reminders module to store the values in a specific list in the iOS Reminders app. If you set that list to be distributed across your iOS devices, you get a free cloud-based storage for your app data, suitable for prototyping different distributed use cases.

      API

      Create a store object providing the name of the Reminders list to be created/used:

      store = ReminderStore(namespace, to_json=False, cache=False)

      Use the store object to store, retrieve, list and delete items:

      • Store: store['key'] = 'value'
      • Retrieve: store['key']
      • List: for key in store: print store[key]
      • Delete: del store['key']

      If you want to store structures instead of plain strings, set to_json=True. Store and retrieval operations will then serialize and restore the values to and from JSON.

      Setting cache=True reduces direct access to the Reminders app. Use the refresh_cache() method to refresh the cache and get information on any background changes. See Notes below for more details.

      Notes

      • Key is stored as the title of the reminder, and value in the notes field.
      • Reminder titles are not unique, but ReminderStore uses an intermediate dict to enforce uniqueness. Results are likely to be unproductive if you create duplicate titles manually.
      • Key is used as-is if it is a plain or unicode string. Otherwise str() is called on key before using it.
      • By default, ReminderStore goes to the Reminders app to get the latest information. This is somewhat inefficient since checking for new items always requires loading the full list. There are two cases where you might to prefer to activate the intermediate caching instead:
        • If you have no-one making any updates remotely, cache will be more efficient and will never need manual refreshing.
        • If you want to get more control over when any background changes are applied in your app, use the caching and call refresh_cache to check and process changes. The method returns None if there are no changes, or a dict with added, deleted and changed sets if there are changes. Each set contains the keys of the stored items that were inserted, deleted or had their contents modified, respectively. You can use this info to e.g. remove deleted items from the UI, or inform the user of a conflict and let them decide which version to keep.
      • Note that iOS Reminders app provides no support for complex atomic transactions or referential integrity between items.
      posted in Pythonista
      mikael
      mikael
    • iOS 14 UIMenu for buttons

      A wrapper around the button UIMenu introduced in iOS 14.

      Installation

      pip install ui3
      

      Usage

      Simplest way to set it up is to use a list defining a title and a handler function for each menu item:

      from ui3.menu import set_menu
      
      def handler(sender, action):
          print(action.title)
      
      set_menu(button, [
          ('First', handler),
          ('Second', handler),
          ('Third', handler),
      ])
      

      First menu with 3 simple items

      Handler gets the button as sender, and the selected action.

      By default, the menu is displayed by a single tap, but you can set it to be activated with a long press, which enables you to use the regular button action for something else:

      set_menu(button, [
          ('First', handler),
          ('Second', handler),
          ('Third', handler),
      ], long_press=True)
      
      button.action = something_else
      

      For slightly more complex menus, you can define Actions:

      from ui3.menu import set_menu, Action
      from ui3.sfsymbol import SymbolImage
      
      placeholder = print
      
      set_menu(button, [
          Action(
              'Verbose menu item gets the space it needs', placeholder,
          ),
          Action(
              'Regular Pythonista icon', placeholder,
              image=ui.Image('iob:close_32'),
          ),
      
          Action(
              'SFSymbol', placeholder,
              image=SymbolImage('photo.on.rectangle'),
          ),
          Action(
              'Destructive', placeholder,
              image=SymbolImage('tornado'),
              attributes=Action.DESTRUCTIVE,
          ),
          Action(
              'Disabled', placeholder,
              attributes=Action.DISABLED,
          ),
      ])
      

      More complex menu

      Actions have the following attributes:

      • title
      • handler - function or method
      • image - if you set the destructive attribute, image is tinted system red automatically
      • attributes - summed combination of Action.HIDDEN, DESTRUCTIVE and DISABLED- by default none of these are active
      • state - either Action.REGULAR (default) or SELECTED
      • discoverability_title

      ... and some convenience boolean properties (read/write):

      • selected
      • hidden
      • disabled
      • destructive

      (Note that there is nothing inherently destructive by an action marked as destructive, it's just visuals.)

      Changing the Action's attributes automatically updates the menu that it is included in. See this example that shows both the selection visual and updating a hidden action:

      expert_action = Action(
          "Special expert action",
          print,
          attributes=Action.HIDDEN,
      )
      
      def toggle_handler(sender, action):
          action.selected = not action.selected
          expert_action.hidden = not action.selected
      
      set_menu(button2, [
          ('Expert mode', toggle_handler),
          expert_action,
      ])
      

      Toggling and hiding

      Still something to be thought about is a way to keep the menu visible as actions are selected/hidden/shown/added/removed.

      posted in Pythonista
      mikael
      mikael
    • pip install gestures, WKWebView, UI animations and more

      I finally got around to publishing some of my Pythonista libraries to PyPi, so
      now you can just pip install the following:

      • pythonista-gestures - multi-touch gestures (readme)
      • pythonista-wkwebview - non-obsolete webview with a JS console (readme)
      • pythonista-scripter - easy UI animations (readme)
      • pythonista-multipeer - ad hoc networking between Apple devices (readme)
      • pythonista-anchor - UI layouts with constraints instead of coordinates (readme)
      • pythonista-docgen - generate a README from the docstrings in the code (readme)

      I also took this opportunity to revisit the gestures module, and updated it to be more in line with what I have learned in the intervening years about ObjC/Python interfaces and nicer APIs. This resulted in a not backwards-compatible change, so if you are using the earlier version, you might consider not installing this one.

      Basic use of the new API:

      import gestures
      
      def swipe_handler(data):
          print(f‘I was swiped left, starting from {data.location}')
      
      label = ui.Label()
      gestures.swipe(label, swipe_handler, direction=gestures.LEFT)
      

      Coordinating several gestures is now a bit more straight-forward. For example, if you want to give the swipe gesture priority over a pan:

      panner = pan(view, pan_handler)
      swiper = swipe(view, swipe_handler, direction=RIGHT)
      swiper.before(panner)
      
      posted in Pythonista
      mikael
      mikael
    • RE: New Beta for Pythonista 3.3

      @omz, my sincere thanks to you, as well.

      Pythonista and this incredible community have enabled me to keep up and develop my Python knowledge, squeezed in between a non-coding job and a busy family life.

      I am not exaggerating when I say that that was the primary thing that enabled me to get a new job, starting a new career next month as a full-time senior Python developer.

      posted in Pythonista
      mikael
      mikael
    • Pure Python gestures

      Python gesture implementation on Github for those situations where you cannot or do not want to use the ObjC gestures.

      Simple usage example:

      import pygestures
      
      class MyTouchableView(pygestures.GestureView):
        
        def on_swipe(self, data):
          if data.direction in (data.UP, data.DOWN):
            print('I was swiped vertically')
      

      Run the file as-is to play around with the gestures. (Green circles track your touches, crosshairs show the centroid, red circle reflects pan, pinch and rotation.)

      Demo

      In your subclass, implement any or all the methods below to handle gestures. All methods get an information object with attributes including:

      • state - one of BEGAN, CHANGED, ENDED
      • location - location of the touch, or the centroid of all touches, as a scene.Point
      • no_of_touches - use this if you want to filter for e.g. only taps with 2 fingers

      Methods:

      • on_tap
      • on_long_press
      • on_swipe - data includes direction, one of UP, DOWN, LEFT, RIGHT
      • on_swipe_up, on_swipe_down, on_swipe_left, on_swipe_right
      • on_pan - data includes translation, the distance from the start of the gesture, as a scene.Point. For most purposes this is better than location, as it does not jump around if you add more fingers.
      • on_pinch - data includes scale
      • on_rotate - data includes rotation in degrees, negative for counterclockwise rotation

      There are also prev_translation, prev_scale and prev_rotation, if you need them.

      If it is more convenient to you, you can inherit GestureMixin together with ui.View or some other custom view class. In that case, if you want to use e.g. rotate, you need to make sure you have set multitouch_enabled = True.

      posted in Pythonista
      mikael
      mikael
    • RE: Wish list for next release

      One more thing:

      Could we please remove or have an option to silence the various clicking sounds made by the editor auxiliary keyboard keys?

      Currently they do not respect the system-wide setting, and are really annoying in a quiet environment.

      posted in Pythonista
      mikael
      mikael
    • [Share code] MarkdownView

      MarkdownView

      MarkdownView is a Pythonista UI library component. It is a drop-in replacement for ui.TextView that supports both editing markdown tagged text and viewing it as HTML.

      Demo

      Features

      • Integrated markdown editing and HTML viewing modes - end editing and HTML is shown, click somewhere on the HTML text and markdown editing starts in the same position.
      • Markdown editing supported by additional keys. (Thanks JonB for help on Obj-C.)
      • Implements ui.TextView API for easy integration to existing code.
      • Customizable through attributes or CSS.

      ##Quick start

      Download (from Github) both the MarkdownView.py script and this readme.md file into same directory, and you can run the script in Pythonista to view and try out editing this readme file.

      Import MarkdownView as a module and use wherever you would use a TextView. Markdown text can be set and accessed with the text attribute. Implement a delegate with textview_did_end_editing or textview_did_change method to handle saving the contents - see the end of the MarkdownView.py file for an example.

      posted in Pythonista
      mikael
      mikael
    • RE: Pure Python gestures

      @Drizzel, below works to separate the two gestures, but we lose the visual feedback for the tap selection, you would need to implement that yourself.

      Better would be to find the built-in tap and give our doubletap preference over it, but I could not (yet...?) find it on the tableview, cell, content view, or the label.

      import ui
      import gestures
      
      def double_tap(data):
          print('double tapped row', data.view.row)
          
      def tap(data):
          print('tapped', data.view.row)
          
      def tableview_cell_for_row(tableview, section, row):
          data = tableview.data_source.items[row]
          cell = ui.TableViewCell('subtitle')
          cell.selectable = False
          cell.text_label.text = data
          cell.row = row
          doubler = gestures.doubletap(cell, double_tap)
          tapper = gestures.tap(cell, tap)
          doubler.before(tapper)
          return cell
          
      class Delegate():
          def tableview_did_select(self, tableview, section, row):
              print('selected row', row)
      
      tv = ui.TableView(allows_selection=False)
      tv.delegate = Delegate()
      tv.data_source = ui.ListDataSource([str(x) for x in range(5)])
      tv.data_source.delete_enabled = False
      tv.data_source.tableview_cell_for_row = tableview_cell_for_row
      
      tv.present('fullscreen')
      
      posted in Pythonista
      mikael
      mikael
    • We live in interesting times - be safe

      Luckily no Pythonista owner will be bored in quarantine.

      posted in Pythonista
      mikael
      mikael
    • RE: Pure Python gestures

      @FrankenApps, for scenes, it is easier to use the ObjC version, available as Gestures.py.

      Panning and zooming is a bit more involved than just using the gestures, so in the same repo there is also zoompanscene.py, which you use like this:

      from zoompanscene import *
      
      class SpaceScene(ZoomPanScene):
        
        def setup(self):
          super().setup()
          
          ship = SpriteNode('spc:PlayerShip1Orange')
          ship.position = self.size / 2
          self.add_child(ship)
          
      run(SpaceScene())
      
      posted in Pythonista
      mikael
      mikael
    • Pythonista apps on Mac

      When Apple delivers on their WWDC promise to have iOS UIKit on Mac, will we be able to port Pythonista apps to Mac with ease?

      posted in Pythonista
      mikael
      mikael
    • UI composites for the world

      This recent thread by @cvp on how to right-align the label on a button prompted me to think about a more general solution to these kinds of requirements. Hence this utility view, available on GitHub.

      Sample image

      Composite is a custom Pythonista UI View that supports stacking several other views in the same area, while providing easy access to the features of the individual views.

      Example #1 - Basic use

      Let's stack a ui.Label on top of a ui.Button:

      lbl_btn = Composite(Button, Label)
      

      Composite members are listed in bottom-up order, and you can provide both classes and instances; classes are simply instantiated with no arguments.

      When you access or set an attribute on the composite, it is also applied in the order given to the constructor, above. Thus, if I set the alignment:

      lbl_btn.alignment = ALIGN_RIGHT
      

      This applies to the Label, as Button does not have that attribute. On the other hand, setting the background color:

      lbl_btn.background_color = 'lightgrey'
      

      Applies only to the first view, Button, even though the Label has that attribute as well.

      The underlying Composite view grabs all attributes that affect the sizing and positioning of the whole composited view. For example:

      lbl_btn.center = (100, 100)
      

      Likewise, setting the size of the Composite will lay out all the contained views so that they all fill the whole area of the Composite.

      If you need to explicitly manage a specific view, for example to manage which view gets touch events, you have two options:

      1. Instantiate it and retain the reference to it, in order to set values before or after giving it to the Composite constructor.
      2. Get the specific view by class name, and set specific values on it.

      An example of the second option:

      lbl_btn['Label'].border_color = 'black'
      

      Example #2 - Margins and corners

      Composite comes with a Margins view, which can be included to inset the following views by a set amount, by default 5 pixels from every edge:

      lbl = Composite(Margins, Label)
      

      You can set the margin values for every edge separately, with any of the following options:

      • single number - Same amount on all sides
      • 2-tuple - (top and bottom, sides)
      • 3-tuple - (top, sides, bottom)
      • 4-tuple - (top, right, bottom, left)

      For example:

      lbl.margin = (5, 10)
      

      Sets top and bottom margins to 5 pixels, and side margins to 10 pixels.

      Labels do not normally support rounded corners eithout some objc_util code, but luckily the containing View does, so this works as well:

      lbl.corner_radius = 10
      

      size_to_fit also works as expected, arranging the whole Composite view around the preferred size of the innermost/topmost view.

      Example #3 - Shadow

      Setting the shadow options for the whole Composite view is supported, either with the convenience function set_drop_shadow that only needs the color of the shadow as a parameter, or with the individual settings:

      • shadow_opacity
      • shadow_offset (tuple)
      • shadow_color
      • shadow_radius

      Example #4 - Blurred background

      iOS BlurEffect is available through a separate auxiliary view:

      blur_lbl = Composite(Blur, Button, Label)
      

      style option can be used to set the brightness of the blurred area in relation to the underlying layer to one of the following values:

      • LIGHTER
      • SAME (default)
      • DARKER

      Run the examples

      All of the above examples are demonstrated in the end of the composite.py file.

      To do

      Editable views like TextField do not work right.

      Should include the vibrancy effect as well.

      Other composable examples are highly welcome.

      Thanks to the following contributions on the Pythonista forum:

      • ObjC code for the shadow settings
      • BlurView code
      posted in Pythonista
      mikael
      mikael
    • RE: Pure Python gestures

      @cvp, forgot to advertise earlier that the latest version of gestures in the ui3 module accepts ObjC views in addition to ui module views.

      from ui3.gestures import *
      
      tap(objc_view, handler)
      
      posted in Pythonista
      mikael
      mikael
    • RE: Passing formatted text as an argument to a shortcut action

      @cvp, that’s why I never have line 11 in my scripts.

      posted in Pythonista
      mikael
      mikael
    • RE: Need to take a One Drive file and upload it to a FTP

      @cvp

      for filename, url in files.items():
      

      😁😉

      posted in Pythonista
      mikael
      mikael
    • RE: scenes updating each other’s variables

      @resserone13, @JonB is an absolute hero and might come back with something, but in general dumping several screenfuls of code to a forum post and asking someone to "make it better" is unlikely to get you the help you are looking for.

      For best results, you need to take the time to think of specific questions and to pull out tight (short) examples to help discuss them.

      posted in Pythonista
      mikael
      mikael
    • RE: Pick file from page in ui.WebView

      @lon2, can you share a minimal example of your code? How you create the ui.WebView, and which page/content you are accessing, is it using JS or regular HTML for the file downloader?

      posted in Pythonista
      mikael
      mikael