omz:forum

    • Register
    • Login
    • Search
    • Recent
    • Popular
    1. Home
    2. TPO

    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 0
    • Topics 1
    • Posts 13
    • Best 4
    • Controversial 0
    • Groups 0

    TPO

    @TPO

    4
    Reputation
    864
    Profile views
    13
    Posts
    0
    Followers
    0
    Following
    Joined Last Online

    TPO Unfollow Follow

    Best posts made by TPO

    • RE: SFSymbols?

      @mikael you could try FuzzyFinder in 10 lines of Python

      Cheers,

      posted in Pythonista
      TPO
      TPO
    • RE: Is it possible to send a URL to pythonista from my phone and make it open on Windows?

      @Jhonmicky, I have had the exact same need for an app I am currently developping.

      You can use Desktop.py below as follows:

      • Run the module on the PC
      • On the iPhone :
      from Desktop import announce_service
      
      # Will launch Firefox on the Desktop, and open 'your_url'
      announce_service(your_url, True)
      ...
      # (Optional) Will close the Firefox window
      announce_service(your_url, False)
      
      

      Lots of things can be configured, please see in the source code. This module was developped for a specific application (MyDB, you will see it mentionned several times in the source), not as a general purpose module, you may have to tweak it a bit to suit your needs.

      Note on implementation : what started out as a simple UDP broadcast function ended up as a full protocol with a fallback against routers that filter out UDP broadcast packets, and fences against lost or delayed packets, WiFi networks that are slow to authenticate, etc., as I tried it out in various environments (a tame Internet Box at home, enterprise LAN, hotel LAN, etc.)

      This is still work in progress.

      Hope this helps.

      """ Desktop companion for MyDB Web UI.
      
      There are two components to this module:
      - service_daemon() runs on the desktop. It will automatically open a browser
        window when the user activates desktop mode on the iPhone, and close the
        browser window when the user exits desktop mode. This is done by listening
        for MyDB Web UI service announcements.
      - announce_service() is used by Web_UI.py, to announce that MyDB's Web UI
        service is available / unavailable.
      
      Revision history:
      22-Jul-2019 TPO - Created this module
      25-Jul-2019 TPO - Initial release """
      
      import json
      import os
      import socket
      import subprocess
      import time
      import threading
      
      # When the following variable is set to True, both service_daemon() and
      # announce_service() will print debug information on stdout. 
      DEBUG = True
      
      # Change the following 2 variables if using another browser than Firefox:
      START_BROWSER = [(r'"C:\Program Files (x86)\Mozilla Firefox\Firefox.exe" '
                        r'-new-window {ip}/'),
                       (r'"C:\Program Files\Mozilla Firefox\Firefox.exe" '
                        r'-new-window {ip}/')]
      STOP_BROWSER = r'TASKKILL /IM firefox.exe /FI "WINDOWTITLE eq MyDB*"'
      
      
      ANNOUNCE_PORT = 50000
      ACK_PORT = 50001
      MAGIC = "XXMYDBXX"
      
      ANNOUNCE_COUNTER = 0
      
      
      def debug(message: str) -> None:
          global DEBUG
          if DEBUG:
              print(message)
      
      
      def announce_service(ip: str, available: bool) -> bool:
          """ Announce that MyDB's Web UI service is available / unavailable.
      
          Broadcast the status of the MyDB Web UI service, so that the desktop
          daemon can open a browser window when the user activates desktop mode on
          the iPhone, and close the browser window when the user exits desktop mode.
      
          Arguments:
          - ip: string containing our IP address.
          - available: if True, announce that the service is now available. If False,
            announce that the service is now unavailable.
      
          Returns: True if the announcement has been received and ackowledged by the
          desktop daemon, False otherwise.
      
          Two broadcast modes are tried:
          - UDP broadcast is tried first (code is courtesy of goncalopp,
            https://stackoverflow.com/a/21090815)
          - If UDP broadcast fails, as can happen on LANs where broadcasting packets
            are filtered by the routers (think airport or hotel LAN), a brute force
            method is tried, by sending the announcement packet to 254 IP adresses,
            using values 1 - 255 for byte 4 of our own IP address (should actually
            use subnet mask, but this is a quick and dirty kludge !)
      
          TODO: document ACK mechanism + counter and session id """
      
          def do_brute_force_broadcast(s: socket.socket,
                                       ip: str,
                                       port: int,
                                       data: bytes) -> None:
              ip_bytes_1_to_3 = ip[:ip.rfind('.')] + '.'
              for i in range(1, 255):
                  print(i, sep=" ")
                  s.sendto(data, (ip_bytes_1_to_3 + str(i), port))
              print(".")
      
          global ACK_PORT, ANNOUNCE_PORT, MAGIC, ANNOUNCE_COUNTER
          SOCKET_TIMEOUT = 0.3
          data = json.dumps({'magic': MAGIC,
                             'counter': ANNOUNCE_COUNTER,
                             'service available': available,
                             'IP': ip,
                             'Session id': time.time()}).encode('utf-8')
          snd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
          snd.bind(('', ANNOUNCE_PORT))
          rcv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
          rcv.bind(('', ACK_PORT))
          rcv.settimeout(SOCKET_TIMEOUT)
          ack = False
          debug(f"Counter = {ANNOUNCE_COUNTER}, announcing service is "
                f"{'ON' if available else 'OFF'}")
          for brute_force, retries in ((False, 3), (True, 8)):
              debug(f"  Trying {'Brute force' if brute_force else 'UDP'} broadcast")
              snd.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, not brute_force)
              for retry in range(retries):
                  debug(f"    Retry # {retry}")
                  if brute_force:
                      brute_force_broadcast_thread = threading.Thread(
                          target=do_brute_force_broadcast,
                          args=(snd, ip, ANNOUNCE_PORT, data))
                      brute_force_broadcast_thread.start()
                  else:
                      snd.sendto(data, ('<broadcast>', ANNOUNCE_PORT))
                  while 1:
                      try:
                          data_bytes, addr = rcv.recvfrom(1024)
                      except socket.timeout:
                          debug("      Socket time out, going for next retry")
                          break
                      debug(f"      Received {data_bytes}")
                      try:
                          data = json.loads(data_bytes.decode('utf-8'))
                      except json.JSONDecodeError:
                          debug("      Invalid JSON, ignoring")
                          continue
                      if (isinstance(data, dict)
                              and data.get('magic') == MAGIC
                              and data.get('counter') == ANNOUNCE_COUNTER
                              and data.get('IP') == ip):
                          debug("      ACK received")
                          ack = True
                          break
                      print("      Invalid ACK, ignoring")
                  if ack:
                      break
              if brute_force:
                  # Need to wait for broadcast_thread to be done before we proceed
                  # to close the snd socket, or do_brute_force_broadcast() will fail
                  # with "Errno 9: Bad file descriptor".
                  brute_force_broadcast_thread.join()
              if ack:
                  break
          if not ack:
              debug("  Both UDP and brute force broadcast methods failed, giving up")
          snd.close()
          rcv.close()
          ANNOUNCE_COUNTER += 1
          return ack
      
      
      def service_daemon():
          """ Automatically open / close web browser on desktop.
      
          service_daemon() runs on the desktop. It will automatically open a browser
          window when the user activates desktop mode on the iPhone, and close the
          browser window when the user exits desktop mode. This is done by listening
          for MyDB Web UI service announcements. """
      
          global ACK_PORT, ANNOUNCE_PORT, MAGIC
      
          # Keep track of the counter value for last annoucement packet processed, in
          # order to ignore retry packets sent by announce_service(), which all have
          # the same counter value.
          last_counter = -1
      
          # Web_UI sessions all start with a counter value of 0, so we need to keep
          # track of Web_UI sessions and reset last_counter every time a new session
          # is started (i.e. when the user activates desktop mode on the iPhone)
          current_session_id = -1
      
          rcv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
          rcv.bind(('', ANNOUNCE_PORT))
          snd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
          snd.bind(('', ACK_PORT))
          debug(f"Listening on port {ANNOUNCE_PORT}")
          while 1:
              data_bytes, addr = rcv.recvfrom(1024)
              debug(f"Received packet from {addr}:\n    '{data_bytes}'")
              try:
                  data = json.loads(data_bytes.decode('utf-8'))
              except json.JSONDecodeError:
                  debug("    Invalid JSON, ignoring")
                  continue
              if (isinstance(data, dict)
                      and data.get('magic') == MAGIC
                      and 'counter' in data
                      and 'IP' in data
                      and 'service available' in data
                      and 'Session id' in data):
                  if (data['Session id'] == current_session_id
                          and data['counter'] <= last_counter):
                      debug(f"    Ignoring MyDB announcement for counter = "
                            f"{data['counter']}, already processed")
                      continue
                  current_session_id = data['Session id']
                  last_counter = data['counter']
                  debug(f"    MyDB announcement: IP = {data['IP']}, "
                        f"service {'ON' if data['service available'] else 'OFF'}, "
                        f"counter = {data['counter']}")
                  ack = json.dumps({'magic': MAGIC,
                                    'counter': data['counter'],
                                    'IP': data['IP']}).encode('utf-8')
                  snd.sendto(ack, (data['IP'], ACK_PORT))
                  debug(f"    ACK sent back to {data['IP']}:{ACK_PORT}")
                  if data['service available']:
                      debug("    Launching browser")
                      for start_browser in START_BROWSER:
                          try:
                              subprocess.Popen(start_browser.format(ip=data['IP']))
                          except FileNotFoundError:
                              continue
                          break
                  else:
                      debug("    Closing browser")
                      os.system(STOP_BROWSER)
              else:
                  debug("    Not a MyDB announcement, ignoring")
      
      
      if __name__ == '__main__':
          service_daemon()
      
      posted in Pythonista
      TPO
      TPO
    • RE: Prevent duplicate launch from shortcut

      @shinyformica, I have the same issue and have come up with a solution based on a simple protocol. When applications follow the protocol, they will not "pile up on top of one another" when launched from the iOS home screen:

      • When an app is already active in Pythonista and it is launched again with its home screen shortcut, the new instance of the app detects the situation and exits, leaving the already active instance of the app on screen.
      • When an app is launched from its home screen shortcut and a previous app is already active in Pythonista, the previous app will be notified that it should terminate, its main UI view will be closed, and the new app will be launched.

      Protocol

      1. An application should create an instance of class AppSingleLaunch, and use it to test if the application is already active, using the is_active() method. If yes, the application should simply exit. If not, the application should declare its main UI view, using the will_present() method, and present the view. Here is an example:

         import app_single_launch
        
         app = AppSingleLaunch("MyApp")
         if not app.is_active():
             view = MyAppView(app)
             app.will_present(view)
             view.present()
        
      2. An application should make a call to the AppSingleLaunch.will_close() method, from the will_close() method of its main UI view:

         class MyAppView(ui.View):
        
             def __init__(self, app):
                 self.app = app
        
             def will_close(self):
               self.app.will_close()
        

      Demo

      Save the code for app_single_launch.py and the two demo apps in the same directory.

      Define home screen shortcuts for the two demo apps.

      Launch demo app 1 using its home screen shortcut, type some text in the text field, then relaunch the app using its home screen shortcut : the text typed previously is still showing, meaning we are using the first launched instance, not the second one. Closing the app brings us back to the Pythonista IDE, not to previously piled up instances of the app.

      Launch demo app 1 using its home screen shortcut, then Launch demo app 2 using its home screen shortcut, then close it : the Pythonista IDE shows, not a piled up instance of demo app 1.

      Et voilà !

      app_single_launch.py

      """ Ensure a Pythonista script can only be launched once from the iOS home screen.
      
      This module provides a solution to the problem stated by shinyformica, which I
      have also encountered (https://forum.omz-software.com/topic/5440/prevent-
      duplicate-launch-from-shortcut):
          "Is there a good, canonical way to prevent a script from launching again if
          there is already an instance running? Specifically, I want to prevent the
          scenario where a user launches the script from the home screen via an app
          shortcut, then goes back to the home screen and launches it again.""
      
      The solution is based on a simple protocol, which applications need to adhere
      to. When this is the case, applications will not "pile up on top of one another"
      when launched from the iOS home screen:
      - When an app is already active in Pythonista and it is launched again with its
        home screen shortcut, the new instance of the app detects the situation and
        exits, leaving the already active instance of the app on screen.
      - When an app is launched from its home screen shortcut and a previous app is
        already active in Pythonista, the previous app will be notified that it
        should terminate, its main UI view will be closed, and the new app will be
        launched.
      
      Protocol:
      1) An application should create an instance of class AppSingleLaunch, and use
         it to test if the application is already active, using the is_active()
         method. If yes, the application should simply exit. If not, the application
         should declare its main UI view, using the will_present() method, and
         present the view. Here is an example:
      
              import app_single_launch
      
              app = AppSingleLaunch("MyApp")
              if not app.is_active():
                  view = MyAppView(app)
                  app.will_present(view)
                  view.present()
      
      2) An application should make a call to the AppSingleLaunch.will_close()
         method, from the will_close() method of its main UI view:
      
              class MyAppView(ui.View):
      
                  def __init__(self, app):
                      self.app = app
      
                  def will_close(self):
                    self.app.will_close()
      
      Implementation: in order to achieve the desired result, we need to remember the
      last application launched according to the protocol, to determine if it is
      still active, and, if it is, to close it. This is achieved by storing into a
      lock file some information about the last application launched:
      - Its name, as passed to single_launch.launching()
      - The id of the ui.View instance for its main view, as passed to single_launch.
        launching(). This is later used to determine if the view is still on screen
        (when an object is still associated with the id), and to close the app's view.
        After several tests, it turns out we must use an ui.View object for this
        purpose, as they seem to persist better than other objects after the cleanup
        pykit-preflight.py does when an app is launched from the home screen.
      The location of the lock file is defined by global variable LOCK_PATH. The
      default location is in the 'site-packages' directory.
      
      Known issue:
      - When an app is on screen, then launched again from its home screen shortcut,
        some issues may happen with inline import statements (rare, would need to be
        qualified further).
      
      26-Feb-2019 TPO - Created this module
      28-Feb-2019 TPO - Initial release
       3-Mar-2019 TPO - Wrapped the code into the AppSingleLaunch class """
      
      
      import gc
      import json
      from pathlib import Path
      import time
      from typing import Any
      
      import ui
      
      
      __all__ = [
          'AppSingleLaunch',
      ]
      
      
      DEBUG = False
      LOCK_PATH = '~/Documents/site-packages/single_launch.lock'
      
      
      def _object_for_id(id_: int) -> Any:
          """ Return an object, given its id. """
      
          # Do a complete garbage collect, to avoid false positives in case the
          # object was still in use recently. In the context of AppSingleLaunch,
          # this would happen if an app was closed, then launched again immediately.
          gc.collect()
          for obj in gc.get_objects():
              if id(obj) == id_:
                  return obj
          return None
      
      
      class AppSingleLaunch:
          """ Wrapper class for all module functionnality. """
      
          def __init__(self, app: str):
              """ Initialize an AppSingleLaunch instance.
      
              Arguments:
              - app: application name, which should be unique (but this is not
              enforced). """
              self.app = app
      
          def is_active(self) -> bool:
              """ Test if the application is already active.
      
              Returns:
              - True if the application is already running, in which case the caller
                should do nothing and exit.
              - False if the application is not already running, in which case the
                caller should launch the application in a normal way, and declare its
                main view by calling the will_present() method."""
              if DEBUG:
                  print(f"is_active(), app = {self.app}")
              lock_path = Path(LOCK_PATH).expanduser()
              if lock_path.exists():
                  with open(lock_path) as lock_file:
                      (lock_app, lock_view_id) = tuple(json.load(lock_file))
                  lock_view = _object_for_id(lock_view_id)
                  if DEBUG:
                      print("- Lock file =", lock_app, lock_view_id,
                            "valid" if lock_view else "invalid")
                  if lock_app == self.app and lock_view:
                      if DEBUG:
                          print(f"- App {self.app} already active")
                      return True
              if DEBUG:
                  print(f"- App {self.app} not active")
              return False
      
          def will_present(self, view: ui.View) -> None:
              """ Declare that the application is about to present its main view.
      
              Arguments:
              - view: ui.View instance for the app's main view. """
              if DEBUG:
                  print(f"will_present({id(view)}), app = {self.app}")
              lock_path = Path(LOCK_PATH).expanduser()
              if lock_path.exists():
                  with open(lock_path) as lock_file:
                      (lock_app, lock_view_id) = tuple(json.load(lock_file))
                  lock_view = _object_for_id(lock_view_id)
                  if DEBUG:
                      print("- Lock file =", lock_app, lock_view_id,
                            "valid" if lock_view else "invalid")
                  if lock_app == self.app and lock_view:
                      raise ValueError(f"App {self.app} is already active, cannot "
                                       f"call will_present() against it.")
                  else:
                      if lock_view and isinstance(lock_view, ui.View):
                          if DEBUG:
                              print(f"- Closing app {lock_app}")
                          lock_view.close()
                          time.sleep(1)  # Required for view to close properly
                      # else: lock is a leftover from a previous Pythonista session
                      #       and can be safely ignored.
              with open(lock_path, 'w') as lock_file:
                  json.dump([self.app, id(view)], lock_file)
              if DEBUG:
                  print(f"- Launching app {self.app}\n- Lock file =", self.app, id(view))
      
          def will_close(self) -> None:
              """ Declare that the application is about to close its main view. """
              lock_path = Path(LOCK_PATH).expanduser()
              if lock_path.exists():
                  with open(lock_path) as lock_file:
                      (lock_app, lock_view_id) = tuple(json.load(lock_file))
                  if lock_app != self.app:
                      raise ValueError(f"App {self.app} if not active, "
                                       f"{lock_app} is active")
                  lock_path.unlink()
      

      single_launch_demo1.py

      """ app_single_launch demo script #1. """
      from app_single_launch import AppSingleLaunch
      import ui
      
      
      class MainView(ui.View):
          def __init__(self, app: AppSingleLaunch):
              self.app = app
              self.name = "Demo app 1"
              self.flex = 'WH'
              self.background_color = 'white'
              self.add_subview(ui.TextField(
                  width=200,
                  height=30,
                  placeholder="Type some text"))
      
          def will_close(self) -> None:
              self.app.will_close()
      
      
      if __name__ == '__main__':
          app = AppSingleLaunch("Demo app 1")
          if not app.is_active():
              view = MainView(app)
              app.will_present(view)
              view.present()
      

      single_launch_demo2.py

      """ app_single_launch demo script #2. """
      from app_single_launch import AppSingleLaunch
      import ui
      
      
      class MainView(ui.View):
          def __init__(self, app: AppSingleLaunch):
              self.app = app
              self.name = "Demo app 2"
              self.flex = 'WH'
              self.background_color = 'white'
              self.add_subview(ui.TextField(
                  width=200,
                  height=30,
                  placeholder="Type some text"))
      
          def will_close(self) -> None:
              self.app.will_close()
      
      
      if __name__ == '__main__':
          app = AppSingleLaunch("Demo app 2")
          if not app.is_active():
              view = MainView(app)
              app.will_present(view)
              view.present()
      
      posted in Pythonista
      TPO
      TPO
    • RE: Webview and Reader mode

      I have packaged cvp's solution into a module, which works like a charm, thanks a lot cvp ! (I use it for a newsreader).

      I have come across two limitations, though:

      • reader_view() cannot be used if 'uiview' has a NavigationView, or if any view presented below it has a NavigationView (even if the view has no relationship with 'uiview' - very strange)
      • No resizing is performed when the reader view changes orientation

      I am not very proficient with objc, would anyone know how to improve this ?

      """ Modal view which displays a web page in reader mode.
      
      Based on cvp's code:
      https://forum.omz-software.com/topic/5392/webview-and-reader-mode/18
      
      When run as a script, the module shows a small demo application.
      
      Implementation note:
      
          Transmitting the callback function from reader_view() to safariView-
          ControllerDidFinish_() turns out to be trickier than expected, especially
          when combining reader_view.py with app_single_launch.py.
      
          In this case, when the user launches the same app a second time, a new
          'safari_view_controler' instance seems to be created automagically. We
          therefore cannot store the callback function as one of the 'safari_view_
          controler's properties, since the instance created by reader_view() is
          silently replaced by a new instance when the app is relaunched, and
          ControllerDidFinish_() will receive the new instance, not the old one.
      
          In the end, the simplest solution works, i.e. storing the callback in a
          global. This does of course come with theoretical limitations: an applica-
          tion should not make a second call to reader_view() until the reader view
          displayed by the first call has been closed. This should not be too much of
          an issue, as I really cannot imagine a practical use case for doing this. I
          have included a simple protection against this edge case anyway.
      
      Revision history:
       6-Fev-2019 TPO - Initial release
      28-Fev-2019 TPO - Added the 'enter_reader_mode' argument to reader_view()
       7-Mar-2019 TPO - reader_view() now works when used with app_single_launch.py """
      
      
      from typing import Callable, Optional
      
      from objc_util import create_objc_class, nsurl, ObjCClass, ObjCInstance
      import ui
      
      
      __all__ = [
          'reader_view',
      ]
      
      
      Callback = Callable[[], None]
      __CALLBACK: Optional[Callback]
      __READER_VIEW_DISPLAYED = False
      
      
      def safariViewControllerDidFinish_(_self, _cmd, _controller):
          global __CALLBACK, __READER_VIEW_DISPLAYED
          if __CALLBACK:
              __CALLBACK()
          __READER_VIEW_DISPLAYED = False
      
      
      def reader_view(url: str,
                      uiview: ui.View,
                      callback: Optional[Callback] = None,
                      enter_reader_mode: bool = True) -> None:
          """ Modal view which displays a web page in reader mode.
      
          Arguments:
          - url: of the web page to be displayed. The only URL schemes allowed are
            http and https.  Any other URL scheme will raise an Objective-C exception
            and cause Pythonista to be terminated.
          - uiview: ui.View instance on which the web view is to be displayed. Note
            that reader_view() does not work if this is an instance of ui.Navigation-
            View.
          - callback: function of no argument, which is called when the user closes
            the view.
          - enter_reader_mode: if true and if reader mode is available, it will be
            entered automatically; if false, the user will have to tap the reader
            mode icon to enter reader mode (when available). """
          global __CALLBACK, __READER_VIEW_DISPLAYED
          if __READER_VIEW_DISPLAYED:
              raise ValueError("reader_view() already in use")
          __READER_VIEW_DISPLAYED = True
          __CALLBACK = callback
      
          MySFSafariViewControllerDelegate = create_objc_class(
              'MySFSafariViewControllerDelegate',
              methods=[safariViewControllerDidFinish_],
              protocols=['SFSafariViewControllerDelegate'])
          delegate = MySFSafariViewControllerDelegate.alloc().init()
          safari_view_controler_conf = (
              ObjCClass('SFSafariViewControllerConfiguration').alloc().init())
          safari_view_controler_conf.setEntersReaderIfAvailable_(enter_reader_mode)
          safari_view_controler = ObjCClass('SFSafariViewController').alloc() \
              .initWithURL_configuration_(nsurl(url), safari_view_controler_conf)
          safari_view_controler.delegate = delegate
          safari_view_controler.setModalPresentationStyle_(3)  # 3 = currentContext
          vc = ObjCClass('SUIViewController').viewControllerForView_(ObjCInstance(uiview))
          vc.presentViewController_animated_completion_(safari_view_controler, True, None)
      
      
      if __name__ == '__main__':
          from datetime import datetime
      
          def done():
              print("After user has closed reader view", datetime.now())
      
          def test(sender):
              print("Before call to reader_view()", datetime.now())
              reader_view(url=url, uiview=view, callback=done)
      
          url = 'http://www.nytimes.com/2019/01/29/climate/global-warming-extreme-weather.html'
          view = ui.View(
              flex='WH',
              background_color='white',
              name='reader_view demo',
              right_button_items=[ui.ButtonItem(title='test', action=test)])
          view.present()
      
      posted in Pythonista
      TPO
      TPO

    Latest posts made by TPO

    • RE: SFSymbols?

      @mikael you could try FuzzyFinder in 10 lines of Python

      Cheers,

      posted in Pythonista
      TPO
      TPO
    • RE: Prevent duplicate launch from shortcut

      @nfmusician there you go : https://github.com/TPO-POMGOM/Pythonista-utilities

      (I currently lack time to migrate to Github other utilities and modules I have developped for Pythonista, will do it as soon as time allows)

      posted in Pythonista
      TPO
      TPO
    • RE: Is it possible to send a URL to pythonista from my phone and make it open on Windows?

      @Jhonmicky, can you be more precise on your use case ?

      More specifically, does your Windows 10 PC have a fixed IP address ? If so, the method proposed by @bennr01 should work (and would be simpler). If your PC does not have a fixed IP address, then you need to establish a protocol between your iOS device and your windows PC, so that they can exchange IP addresses (and this is where the module I posted earlier would come in handy).

      posted in Pythonista
      TPO
      TPO
    • RE: Is it possible to send a URL to pythonista from my phone and make it open on Windows?

      @Jhonmicky, I have had the exact same need for an app I am currently developping.

      You can use Desktop.py below as follows:

      • Run the module on the PC
      • On the iPhone :
      from Desktop import announce_service
      
      # Will launch Firefox on the Desktop, and open 'your_url'
      announce_service(your_url, True)
      ...
      # (Optional) Will close the Firefox window
      announce_service(your_url, False)
      
      

      Lots of things can be configured, please see in the source code. This module was developped for a specific application (MyDB, you will see it mentionned several times in the source), not as a general purpose module, you may have to tweak it a bit to suit your needs.

      Note on implementation : what started out as a simple UDP broadcast function ended up as a full protocol with a fallback against routers that filter out UDP broadcast packets, and fences against lost or delayed packets, WiFi networks that are slow to authenticate, etc., as I tried it out in various environments (a tame Internet Box at home, enterprise LAN, hotel LAN, etc.)

      This is still work in progress.

      Hope this helps.

      """ Desktop companion for MyDB Web UI.
      
      There are two components to this module:
      - service_daemon() runs on the desktop. It will automatically open a browser
        window when the user activates desktop mode on the iPhone, and close the
        browser window when the user exits desktop mode. This is done by listening
        for MyDB Web UI service announcements.
      - announce_service() is used by Web_UI.py, to announce that MyDB's Web UI
        service is available / unavailable.
      
      Revision history:
      22-Jul-2019 TPO - Created this module
      25-Jul-2019 TPO - Initial release """
      
      import json
      import os
      import socket
      import subprocess
      import time
      import threading
      
      # When the following variable is set to True, both service_daemon() and
      # announce_service() will print debug information on stdout. 
      DEBUG = True
      
      # Change the following 2 variables if using another browser than Firefox:
      START_BROWSER = [(r'"C:\Program Files (x86)\Mozilla Firefox\Firefox.exe" '
                        r'-new-window {ip}/'),
                       (r'"C:\Program Files\Mozilla Firefox\Firefox.exe" '
                        r'-new-window {ip}/')]
      STOP_BROWSER = r'TASKKILL /IM firefox.exe /FI "WINDOWTITLE eq MyDB*"'
      
      
      ANNOUNCE_PORT = 50000
      ACK_PORT = 50001
      MAGIC = "XXMYDBXX"
      
      ANNOUNCE_COUNTER = 0
      
      
      def debug(message: str) -> None:
          global DEBUG
          if DEBUG:
              print(message)
      
      
      def announce_service(ip: str, available: bool) -> bool:
          """ Announce that MyDB's Web UI service is available / unavailable.
      
          Broadcast the status of the MyDB Web UI service, so that the desktop
          daemon can open a browser window when the user activates desktop mode on
          the iPhone, and close the browser window when the user exits desktop mode.
      
          Arguments:
          - ip: string containing our IP address.
          - available: if True, announce that the service is now available. If False,
            announce that the service is now unavailable.
      
          Returns: True if the announcement has been received and ackowledged by the
          desktop daemon, False otherwise.
      
          Two broadcast modes are tried:
          - UDP broadcast is tried first (code is courtesy of goncalopp,
            https://stackoverflow.com/a/21090815)
          - If UDP broadcast fails, as can happen on LANs where broadcasting packets
            are filtered by the routers (think airport or hotel LAN), a brute force
            method is tried, by sending the announcement packet to 254 IP adresses,
            using values 1 - 255 for byte 4 of our own IP address (should actually
            use subnet mask, but this is a quick and dirty kludge !)
      
          TODO: document ACK mechanism + counter and session id """
      
          def do_brute_force_broadcast(s: socket.socket,
                                       ip: str,
                                       port: int,
                                       data: bytes) -> None:
              ip_bytes_1_to_3 = ip[:ip.rfind('.')] + '.'
              for i in range(1, 255):
                  print(i, sep=" ")
                  s.sendto(data, (ip_bytes_1_to_3 + str(i), port))
              print(".")
      
          global ACK_PORT, ANNOUNCE_PORT, MAGIC, ANNOUNCE_COUNTER
          SOCKET_TIMEOUT = 0.3
          data = json.dumps({'magic': MAGIC,
                             'counter': ANNOUNCE_COUNTER,
                             'service available': available,
                             'IP': ip,
                             'Session id': time.time()}).encode('utf-8')
          snd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
          snd.bind(('', ANNOUNCE_PORT))
          rcv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
          rcv.bind(('', ACK_PORT))
          rcv.settimeout(SOCKET_TIMEOUT)
          ack = False
          debug(f"Counter = {ANNOUNCE_COUNTER}, announcing service is "
                f"{'ON' if available else 'OFF'}")
          for brute_force, retries in ((False, 3), (True, 8)):
              debug(f"  Trying {'Brute force' if brute_force else 'UDP'} broadcast")
              snd.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, not brute_force)
              for retry in range(retries):
                  debug(f"    Retry # {retry}")
                  if brute_force:
                      brute_force_broadcast_thread = threading.Thread(
                          target=do_brute_force_broadcast,
                          args=(snd, ip, ANNOUNCE_PORT, data))
                      brute_force_broadcast_thread.start()
                  else:
                      snd.sendto(data, ('<broadcast>', ANNOUNCE_PORT))
                  while 1:
                      try:
                          data_bytes, addr = rcv.recvfrom(1024)
                      except socket.timeout:
                          debug("      Socket time out, going for next retry")
                          break
                      debug(f"      Received {data_bytes}")
                      try:
                          data = json.loads(data_bytes.decode('utf-8'))
                      except json.JSONDecodeError:
                          debug("      Invalid JSON, ignoring")
                          continue
                      if (isinstance(data, dict)
                              and data.get('magic') == MAGIC
                              and data.get('counter') == ANNOUNCE_COUNTER
                              and data.get('IP') == ip):
                          debug("      ACK received")
                          ack = True
                          break
                      print("      Invalid ACK, ignoring")
                  if ack:
                      break
              if brute_force:
                  # Need to wait for broadcast_thread to be done before we proceed
                  # to close the snd socket, or do_brute_force_broadcast() will fail
                  # with "Errno 9: Bad file descriptor".
                  brute_force_broadcast_thread.join()
              if ack:
                  break
          if not ack:
              debug("  Both UDP and brute force broadcast methods failed, giving up")
          snd.close()
          rcv.close()
          ANNOUNCE_COUNTER += 1
          return ack
      
      
      def service_daemon():
          """ Automatically open / close web browser on desktop.
      
          service_daemon() runs on the desktop. It will automatically open a browser
          window when the user activates desktop mode on the iPhone, and close the
          browser window when the user exits desktop mode. This is done by listening
          for MyDB Web UI service announcements. """
      
          global ACK_PORT, ANNOUNCE_PORT, MAGIC
      
          # Keep track of the counter value for last annoucement packet processed, in
          # order to ignore retry packets sent by announce_service(), which all have
          # the same counter value.
          last_counter = -1
      
          # Web_UI sessions all start with a counter value of 0, so we need to keep
          # track of Web_UI sessions and reset last_counter every time a new session
          # is started (i.e. when the user activates desktop mode on the iPhone)
          current_session_id = -1
      
          rcv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
          rcv.bind(('', ANNOUNCE_PORT))
          snd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
          snd.bind(('', ACK_PORT))
          debug(f"Listening on port {ANNOUNCE_PORT}")
          while 1:
              data_bytes, addr = rcv.recvfrom(1024)
              debug(f"Received packet from {addr}:\n    '{data_bytes}'")
              try:
                  data = json.loads(data_bytes.decode('utf-8'))
              except json.JSONDecodeError:
                  debug("    Invalid JSON, ignoring")
                  continue
              if (isinstance(data, dict)
                      and data.get('magic') == MAGIC
                      and 'counter' in data
                      and 'IP' in data
                      and 'service available' in data
                      and 'Session id' in data):
                  if (data['Session id'] == current_session_id
                          and data['counter'] <= last_counter):
                      debug(f"    Ignoring MyDB announcement for counter = "
                            f"{data['counter']}, already processed")
                      continue
                  current_session_id = data['Session id']
                  last_counter = data['counter']
                  debug(f"    MyDB announcement: IP = {data['IP']}, "
                        f"service {'ON' if data['service available'] else 'OFF'}, "
                        f"counter = {data['counter']}")
                  ack = json.dumps({'magic': MAGIC,
                                    'counter': data['counter'],
                                    'IP': data['IP']}).encode('utf-8')
                  snd.sendto(ack, (data['IP'], ACK_PORT))
                  debug(f"    ACK sent back to {data['IP']}:{ACK_PORT}")
                  if data['service available']:
                      debug("    Launching browser")
                      for start_browser in START_BROWSER:
                          try:
                              subprocess.Popen(start_browser.format(ip=data['IP']))
                          except FileNotFoundError:
                              continue
                          break
                  else:
                      debug("    Closing browser")
                      os.system(STOP_BROWSER)
              else:
                  debug("    Not a MyDB announcement, ignoring")
      
      
      if __name__ == '__main__':
          service_daemon()
      
      posted in Pythonista
      TPO
      TPO
    • RE: Prevent duplicate launch from shortcut

      @shinyformica, I have the same issue and have come up with a solution based on a simple protocol. When applications follow the protocol, they will not "pile up on top of one another" when launched from the iOS home screen:

      • When an app is already active in Pythonista and it is launched again with its home screen shortcut, the new instance of the app detects the situation and exits, leaving the already active instance of the app on screen.
      • When an app is launched from its home screen shortcut and a previous app is already active in Pythonista, the previous app will be notified that it should terminate, its main UI view will be closed, and the new app will be launched.

      Protocol

      1. An application should create an instance of class AppSingleLaunch, and use it to test if the application is already active, using the is_active() method. If yes, the application should simply exit. If not, the application should declare its main UI view, using the will_present() method, and present the view. Here is an example:

         import app_single_launch
        
         app = AppSingleLaunch("MyApp")
         if not app.is_active():
             view = MyAppView(app)
             app.will_present(view)
             view.present()
        
      2. An application should make a call to the AppSingleLaunch.will_close() method, from the will_close() method of its main UI view:

         class MyAppView(ui.View):
        
             def __init__(self, app):
                 self.app = app
        
             def will_close(self):
               self.app.will_close()
        

      Demo

      Save the code for app_single_launch.py and the two demo apps in the same directory.

      Define home screen shortcuts for the two demo apps.

      Launch demo app 1 using its home screen shortcut, type some text in the text field, then relaunch the app using its home screen shortcut : the text typed previously is still showing, meaning we are using the first launched instance, not the second one. Closing the app brings us back to the Pythonista IDE, not to previously piled up instances of the app.

      Launch demo app 1 using its home screen shortcut, then Launch demo app 2 using its home screen shortcut, then close it : the Pythonista IDE shows, not a piled up instance of demo app 1.

      Et voilà !

      app_single_launch.py

      """ Ensure a Pythonista script can only be launched once from the iOS home screen.
      
      This module provides a solution to the problem stated by shinyformica, which I
      have also encountered (https://forum.omz-software.com/topic/5440/prevent-
      duplicate-launch-from-shortcut):
          "Is there a good, canonical way to prevent a script from launching again if
          there is already an instance running? Specifically, I want to prevent the
          scenario where a user launches the script from the home screen via an app
          shortcut, then goes back to the home screen and launches it again.""
      
      The solution is based on a simple protocol, which applications need to adhere
      to. When this is the case, applications will not "pile up on top of one another"
      when launched from the iOS home screen:
      - When an app is already active in Pythonista and it is launched again with its
        home screen shortcut, the new instance of the app detects the situation and
        exits, leaving the already active instance of the app on screen.
      - When an app is launched from its home screen shortcut and a previous app is
        already active in Pythonista, the previous app will be notified that it
        should terminate, its main UI view will be closed, and the new app will be
        launched.
      
      Protocol:
      1) An application should create an instance of class AppSingleLaunch, and use
         it to test if the application is already active, using the is_active()
         method. If yes, the application should simply exit. If not, the application
         should declare its main UI view, using the will_present() method, and
         present the view. Here is an example:
      
              import app_single_launch
      
              app = AppSingleLaunch("MyApp")
              if not app.is_active():
                  view = MyAppView(app)
                  app.will_present(view)
                  view.present()
      
      2) An application should make a call to the AppSingleLaunch.will_close()
         method, from the will_close() method of its main UI view:
      
              class MyAppView(ui.View):
      
                  def __init__(self, app):
                      self.app = app
      
                  def will_close(self):
                    self.app.will_close()
      
      Implementation: in order to achieve the desired result, we need to remember the
      last application launched according to the protocol, to determine if it is
      still active, and, if it is, to close it. This is achieved by storing into a
      lock file some information about the last application launched:
      - Its name, as passed to single_launch.launching()
      - The id of the ui.View instance for its main view, as passed to single_launch.
        launching(). This is later used to determine if the view is still on screen
        (when an object is still associated with the id), and to close the app's view.
        After several tests, it turns out we must use an ui.View object for this
        purpose, as they seem to persist better than other objects after the cleanup
        pykit-preflight.py does when an app is launched from the home screen.
      The location of the lock file is defined by global variable LOCK_PATH. The
      default location is in the 'site-packages' directory.
      
      Known issue:
      - When an app is on screen, then launched again from its home screen shortcut,
        some issues may happen with inline import statements (rare, would need to be
        qualified further).
      
      26-Feb-2019 TPO - Created this module
      28-Feb-2019 TPO - Initial release
       3-Mar-2019 TPO - Wrapped the code into the AppSingleLaunch class """
      
      
      import gc
      import json
      from pathlib import Path
      import time
      from typing import Any
      
      import ui
      
      
      __all__ = [
          'AppSingleLaunch',
      ]
      
      
      DEBUG = False
      LOCK_PATH = '~/Documents/site-packages/single_launch.lock'
      
      
      def _object_for_id(id_: int) -> Any:
          """ Return an object, given its id. """
      
          # Do a complete garbage collect, to avoid false positives in case the
          # object was still in use recently. In the context of AppSingleLaunch,
          # this would happen if an app was closed, then launched again immediately.
          gc.collect()
          for obj in gc.get_objects():
              if id(obj) == id_:
                  return obj
          return None
      
      
      class AppSingleLaunch:
          """ Wrapper class for all module functionnality. """
      
          def __init__(self, app: str):
              """ Initialize an AppSingleLaunch instance.
      
              Arguments:
              - app: application name, which should be unique (but this is not
              enforced). """
              self.app = app
      
          def is_active(self) -> bool:
              """ Test if the application is already active.
      
              Returns:
              - True if the application is already running, in which case the caller
                should do nothing and exit.
              - False if the application is not already running, in which case the
                caller should launch the application in a normal way, and declare its
                main view by calling the will_present() method."""
              if DEBUG:
                  print(f"is_active(), app = {self.app}")
              lock_path = Path(LOCK_PATH).expanduser()
              if lock_path.exists():
                  with open(lock_path) as lock_file:
                      (lock_app, lock_view_id) = tuple(json.load(lock_file))
                  lock_view = _object_for_id(lock_view_id)
                  if DEBUG:
                      print("- Lock file =", lock_app, lock_view_id,
                            "valid" if lock_view else "invalid")
                  if lock_app == self.app and lock_view:
                      if DEBUG:
                          print(f"- App {self.app} already active")
                      return True
              if DEBUG:
                  print(f"- App {self.app} not active")
              return False
      
          def will_present(self, view: ui.View) -> None:
              """ Declare that the application is about to present its main view.
      
              Arguments:
              - view: ui.View instance for the app's main view. """
              if DEBUG:
                  print(f"will_present({id(view)}), app = {self.app}")
              lock_path = Path(LOCK_PATH).expanduser()
              if lock_path.exists():
                  with open(lock_path) as lock_file:
                      (lock_app, lock_view_id) = tuple(json.load(lock_file))
                  lock_view = _object_for_id(lock_view_id)
                  if DEBUG:
                      print("- Lock file =", lock_app, lock_view_id,
                            "valid" if lock_view else "invalid")
                  if lock_app == self.app and lock_view:
                      raise ValueError(f"App {self.app} is already active, cannot "
                                       f"call will_present() against it.")
                  else:
                      if lock_view and isinstance(lock_view, ui.View):
                          if DEBUG:
                              print(f"- Closing app {lock_app}")
                          lock_view.close()
                          time.sleep(1)  # Required for view to close properly
                      # else: lock is a leftover from a previous Pythonista session
                      #       and can be safely ignored.
              with open(lock_path, 'w') as lock_file:
                  json.dump([self.app, id(view)], lock_file)
              if DEBUG:
                  print(f"- Launching app {self.app}\n- Lock file =", self.app, id(view))
      
          def will_close(self) -> None:
              """ Declare that the application is about to close its main view. """
              lock_path = Path(LOCK_PATH).expanduser()
              if lock_path.exists():
                  with open(lock_path) as lock_file:
                      (lock_app, lock_view_id) = tuple(json.load(lock_file))
                  if lock_app != self.app:
                      raise ValueError(f"App {self.app} if not active, "
                                       f"{lock_app} is active")
                  lock_path.unlink()
      

      single_launch_demo1.py

      """ app_single_launch demo script #1. """
      from app_single_launch import AppSingleLaunch
      import ui
      
      
      class MainView(ui.View):
          def __init__(self, app: AppSingleLaunch):
              self.app = app
              self.name = "Demo app 1"
              self.flex = 'WH'
              self.background_color = 'white'
              self.add_subview(ui.TextField(
                  width=200,
                  height=30,
                  placeholder="Type some text"))
      
          def will_close(self) -> None:
              self.app.will_close()
      
      
      if __name__ == '__main__':
          app = AppSingleLaunch("Demo app 1")
          if not app.is_active():
              view = MainView(app)
              app.will_present(view)
              view.present()
      

      single_launch_demo2.py

      """ app_single_launch demo script #2. """
      from app_single_launch import AppSingleLaunch
      import ui
      
      
      class MainView(ui.View):
          def __init__(self, app: AppSingleLaunch):
              self.app = app
              self.name = "Demo app 2"
              self.flex = 'WH'
              self.background_color = 'white'
              self.add_subview(ui.TextField(
                  width=200,
                  height=30,
                  placeholder="Type some text"))
      
          def will_close(self) -> None:
              self.app.will_close()
      
      
      if __name__ == '__main__':
          app = AppSingleLaunch("Demo app 2")
          if not app.is_active():
              view = MainView(app)
              app.will_present(view)
              view.present()
      
      posted in Pythonista
      TPO
      TPO
    • RE: Webview and Reader mode

      @mikael you are quite right, dir() shows that safari_view_controler has a 'contentScrollView ' attribute.

      However, safari_view_controler.contentScrollView() returns None, both in reader_view() and in safariViewControllerDidFinish_() :-(

      I have gone through the official Apple doc on SFSafariViewController, 'contentScrollView ' is not mentionned anywhere.

      posted in Pythonista
      TPO
      TPO
    • RE: Webview and Reader mode

      @mikael : works like a charm, thanks a lot !

      posted in Pythonista
      TPO
      TPO
    • RE: Webview and Reader mode

      In addition to adopting cvp's SFSafariView solution, I have been experimenting with JonB's initial suggestion of using Safari's JS code for the reader mode.

      The reason for this is that SFSafariView is a black box which does not provide a way of getting / setting the position in the page. Being able to do this would allow a newsreader app to remember where the user stopped reading an article, and later re-open the article at this very position. This would be convenient for long articles which cannot be read in one sitting.

      If we had a way a generating a Safari-reader-mode-like filtered version of the web page, we could display it in a WebView and then get / set the position (as demonstrated by MarkdownView, https://github.com/mikaelho/pythonista-markdownview) to achieve this result.

      I have digged a bit into this, have found a more recent and more complete version of Safari's reader mode source code (both JS and CSS), and have had some success with it:

      • The web page is cleaned of most useless content
      • It is displayed in the mode selected (for example : sepia), with the font selected

      However:

      • Not all useless content is removed, and the result is not as good as what Safari produces
      • Margins and spacing between paragraphs are not handled properly

      You will find below my test code and pointers to the Safari sources, in case anyone is interested in investigating this further.

      Download the following files :

      • safari.js : https://github.com/liruqi/Safari/blob/master/safari.js
      • article-content.css : https://github.com/andjosh/safari-reader-css/blob/master/article-content.css
      • reader-ui.css : https://github.com/andjosh/safari-reader-css/blob/master/reader-ui.css
      • theming.css : https://github.com/andjosh/safari-reader-css/blob/master/theming.css

      Then run the following in the same directory :

      import wkwebview
      import ui
      
      with open('safari.js', 'r', encoding='utf-8') as f:
          safari = f.read()
      
      with open('reader-ui.css', 'r', encoding='utf-8') as f:
          reader_ui_css = f.read()
      
      with open('article-content.css', 'r', encoding='utf-8') as f:
          article_content_css = f.read()
      
      with open('theming.css', 'r', encoding='utf-8') as f:
          theming_css = f.read()
      
      js_script = f'''
          r = new ReaderArticleFinder(document);
          document.body.innerHTML = r.articleNode().innerHTML;
          document.body.classList="system sepia";
          var style = document.createElement('style');
          style.innerHTML = `{reader_ui_css}\n{article_content_css}\n{theming_css}`;
          var ref = document.querySelector('script');
          ref.parentNode.insertBefore(style, ref);
          document.getElementById("dynamic-article-content").sheet.insertRule("#article {{ font-size:15 px; line-height:25px; }}"); '''
      
      
      class Delegate:
      
          @ui.in_background
          def webview_did_finish_load(self, webview):
              w.eval_js(js_script)
      
      
      w = wkwebview.WKWebView(delegate=Delegate())
      w.present()
      w.add_script(safari)
      w.load_url('https://www.technologyreview.com/s/612929/wristwatch-heart-monitors-might-save-your-lifeand-change-medicine-too/')
      
      posted in Pythonista
      TPO
      TPO
    • RE: Webview and Reader mode

      I have packaged cvp's solution into a module, which works like a charm, thanks a lot cvp ! (I use it for a newsreader).

      I have come across two limitations, though:

      • reader_view() cannot be used if 'uiview' has a NavigationView, or if any view presented below it has a NavigationView (even if the view has no relationship with 'uiview' - very strange)
      • No resizing is performed when the reader view changes orientation

      I am not very proficient with objc, would anyone know how to improve this ?

      """ Modal view which displays a web page in reader mode.
      
      Based on cvp's code:
      https://forum.omz-software.com/topic/5392/webview-and-reader-mode/18
      
      When run as a script, the module shows a small demo application.
      
      Implementation note:
      
          Transmitting the callback function from reader_view() to safariView-
          ControllerDidFinish_() turns out to be trickier than expected, especially
          when combining reader_view.py with app_single_launch.py.
      
          In this case, when the user launches the same app a second time, a new
          'safari_view_controler' instance seems to be created automagically. We
          therefore cannot store the callback function as one of the 'safari_view_
          controler's properties, since the instance created by reader_view() is
          silently replaced by a new instance when the app is relaunched, and
          ControllerDidFinish_() will receive the new instance, not the old one.
      
          In the end, the simplest solution works, i.e. storing the callback in a
          global. This does of course come with theoretical limitations: an applica-
          tion should not make a second call to reader_view() until the reader view
          displayed by the first call has been closed. This should not be too much of
          an issue, as I really cannot imagine a practical use case for doing this. I
          have included a simple protection against this edge case anyway.
      
      Revision history:
       6-Fev-2019 TPO - Initial release
      28-Fev-2019 TPO - Added the 'enter_reader_mode' argument to reader_view()
       7-Mar-2019 TPO - reader_view() now works when used with app_single_launch.py """
      
      
      from typing import Callable, Optional
      
      from objc_util import create_objc_class, nsurl, ObjCClass, ObjCInstance
      import ui
      
      
      __all__ = [
          'reader_view',
      ]
      
      
      Callback = Callable[[], None]
      __CALLBACK: Optional[Callback]
      __READER_VIEW_DISPLAYED = False
      
      
      def safariViewControllerDidFinish_(_self, _cmd, _controller):
          global __CALLBACK, __READER_VIEW_DISPLAYED
          if __CALLBACK:
              __CALLBACK()
          __READER_VIEW_DISPLAYED = False
      
      
      def reader_view(url: str,
                      uiview: ui.View,
                      callback: Optional[Callback] = None,
                      enter_reader_mode: bool = True) -> None:
          """ Modal view which displays a web page in reader mode.
      
          Arguments:
          - url: of the web page to be displayed. The only URL schemes allowed are
            http and https.  Any other URL scheme will raise an Objective-C exception
            and cause Pythonista to be terminated.
          - uiview: ui.View instance on which the web view is to be displayed. Note
            that reader_view() does not work if this is an instance of ui.Navigation-
            View.
          - callback: function of no argument, which is called when the user closes
            the view.
          - enter_reader_mode: if true and if reader mode is available, it will be
            entered automatically; if false, the user will have to tap the reader
            mode icon to enter reader mode (when available). """
          global __CALLBACK, __READER_VIEW_DISPLAYED
          if __READER_VIEW_DISPLAYED:
              raise ValueError("reader_view() already in use")
          __READER_VIEW_DISPLAYED = True
          __CALLBACK = callback
      
          MySFSafariViewControllerDelegate = create_objc_class(
              'MySFSafariViewControllerDelegate',
              methods=[safariViewControllerDidFinish_],
              protocols=['SFSafariViewControllerDelegate'])
          delegate = MySFSafariViewControllerDelegate.alloc().init()
          safari_view_controler_conf = (
              ObjCClass('SFSafariViewControllerConfiguration').alloc().init())
          safari_view_controler_conf.setEntersReaderIfAvailable_(enter_reader_mode)
          safari_view_controler = ObjCClass('SFSafariViewController').alloc() \
              .initWithURL_configuration_(nsurl(url), safari_view_controler_conf)
          safari_view_controler.delegate = delegate
          safari_view_controler.setModalPresentationStyle_(3)  # 3 = currentContext
          vc = ObjCClass('SUIViewController').viewControllerForView_(ObjCInstance(uiview))
          vc.presentViewController_animated_completion_(safari_view_controler, True, None)
      
      
      if __name__ == '__main__':
          from datetime import datetime
      
          def done():
              print("After user has closed reader view", datetime.now())
      
          def test(sender):
              print("Before call to reader_view()", datetime.now())
              reader_view(url=url, uiview=view, callback=done)
      
          url = 'http://www.nytimes.com/2019/01/29/climate/global-warming-extreme-weather.html'
          view = ui.View(
              flex='WH',
              background_color='white',
              name='reader_view demo',
              right_button_items=[ui.ButtonItem(title='test', action=test)])
          view.present()
      
      posted in Pythonista
      TPO
      TPO
    • RE: Launching an app from Pythonista

      @cvp, it works, you're the greatest !!!

      posted in Pythonista
      TPO
      TPO