Forum Archive

Polling from a ui.View (built in timers in ui.Views)

Phuket2

@omz, did you ever go further with the idea of a ui.View having its own timer that you could hook into to do updates etc. ? Personally, I think I would be a great advance for ui.View or Custom ui.Views. I realise there are different ways to do it already. But it's so easy to get it wrong, well at least for me. I am sure you could implement it many ways. For me, I think the best would be a callback same as draw in subclassed ui.Views. Like the draw method, if a timer method is not defined then no additional overhead or speed issues would impact the class. Also some way to set the interval of the callback as well as a way to able to suspend/activate the timer callbacks. I think it could be more flexible if it did not automatically suspend if it's not the foremost view. Eg, you may have a view/s as an overlay/s with the alpha set so it's transparent, but you want to still want it to execute the code in the timer callback method.
Anyway, I think this would be super helpful. Not sure what others think.

mikael

Working with the tools we have, here's a decorator using ui.delay:

from ui import *
from functools import partial

def poll(interval):
  def interval_decorator(func):
    def func_wrapper(self, *args, **kwargs):
      with_args = partial(wrapped_func, self, *args, **kwargs)
      delay(with_args, interval)
      return func(self, *args, **kwargs)
    wrapped_func = func_wrapper
    return func_wrapper
  return interval_decorator

Which can be used like this:

class CustomView(View):

  @poll(1)
  def get_text(self, name):
    print("Hello " + name)

  def will_close(self):
    cancel_delays()

if __name__ == '__main__':
  v = CustomView()
  v.background_color = 'white'
  v.present('sheet')

  v.get_text('John')

... but as with anything using ui.delay, this is unstable and keeps crashing my Pythonista.

Phuket2

@mikael , exactly my point. Not easy to do. Also cancel delays is global in that it cancels all the queue. @omz mentioned before it might be a good idea for him to add something like this to a view. He Did not talk about the implementation. But of course he is in the best position to implement this type of callback that does not break. I have my fingers crossed he will do something 🤑

omz

@Phuket2 I still think it might be a good idea, but I haven't found the time to actually work on this yet.

mikael

While we are waiting, the version below:

  • does not use cancel_delays
  • does not keep crashing
  • does not need the View to be the root view (i.e. the one that is presented)
  • will go dormant when off screen
  • can be turned on and off per view by setting polling attribute True or False

I think I will turn this into a Composite component, with the following planned features:

  • can be applied to any view (not just ui.Views)
  • usage is easier - just define the polling methods
  • support UI thread or background execution
  • support chaining of these methods, as many animations need something like that

Updated code, sorry for the dump, but it is not awfully long:

#coding: utf-8
from ui import *
from functools import partial

def poll(interval):
  def interval_decorator(func):
    def func_wrapper(self, *args, **kwargs):
      if self.polling and isinstance(self, View) and self.on_screen:
        with_args = partial(wrapped_func, self, *args, **kwargs)
        delay(with_args, interval)
        return func(self, *args, **kwargs)
      else:
        self._polling = False
    wrapped_func = func_wrapper
    return func_wrapper
  return interval_decorator

class CustomView(View):

  def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self._polling = False

  @poll(1)
  def get_text(self, name):
    print("Hello " + name)

  def will_close(self):
    self.polling = False

  @property
  def polling(self):
    return self._polling

  @polling.setter
  def polling(self, value):
    was_polling = self._polling
    self._polling = value
    if not was_polling and self._polling:
      self.get_text('John')

if __name__ == '__main__':
  v = View()
  v.background_color = 'white'
  v.present('sheet')

  c = CustomView()
  v.add_subview(c)

  c.polling = True
enceladus

May be just use scene timer.
https://gist.github.com/f229353624df386c1beffb864dc2cce0

import ui, scene

class TimerView(ui.View):
    class TimerScene(scene.Scene):
        def update(self):
            self.view.superview.update()

    def create_sceneview(self):
        scene_view = scene.SceneView()
        scene_view.width = 0
        scene_view.height = 0
        scene_view.frame_interval = self.frame_interval
        scene_view.scene = TimerView.TimerScene()
        return scene_view

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.frame_interval = kwargs.get('frame_interval', 1)
        self.add_subview(self.create_sceneview())

    @property    
    def start_time(self):
        return self.subviews[0].scene.t

    def draw(self):
        pass

    def update(self):  
        self.set_needs_display()

if __name__ == '__main__':
    from time import localtime
    class DigitalClock(TimerView):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
        def draw(self):
            t = localtime()
            ui.draw_string("{:02}:{:02}:{:02}".format(
                t.tm_hour, t.tm_min, t.tm_sec),
                font=('Helvetica', 20),
                rect=(100, 100,0,0),
                alignment=ui.ALIGN_CENTER)

    v = DigitalClock(frame=(0,0,300, 300))
    v.present('sheet')

Phuket2

Guys,
thanks for your code posts. I will give both versions a try out a little later today. Also thanks for your reply @omz.

enceladus

You can also use async module timer

import ui
import asyncio
from time import localtime

class DigitalClock(ui.View):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def draw(self):
        t = localtime()
        ui.draw_string("{:02}:{:02}:{:02}".format(
            t.tm_hour, t.tm_min, t.tm_sec),
            font=('Helvetica', 20),
            rect=(100, 100,0,0),
            alignment=ui.ALIGN_CENTER)

    def update(self, event_loop):
        self.set_needs_display()
        event_loop.call_later(.5, self.update, event_loop)

v = DigitalClock(frame=(0,0,300, 300), frame_interval=10)
v.present('sheet')

event_loop = asyncio.get_event_loop()
event_loop.call_soon(v.update, event_loop)
event_loop.run_forever()




Stop watch example (use the stopwatch.pyui from gist link in previous post)

import ui
import asyncio

class StopWatch(ui.View):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.value = 0
        self.state = 'stop'

    def draw(self):
        t0 = (self.value//(600*60), self.value//600, self.value//10)
        t1 = (t0[0], t0[1]%60, t0[2]%60)
        ui.draw_string("{:02}:{:02}:{:02}".format(*t1),
            font=('Helvetica', 20),
            rect=(150, 0, 0, 0),
            color='black',
            alignment=ui.ALIGN_CENTER)

    def update(self, event_loop):
        if self.state == 'run':
            self.value += 1
        self.set_needs_display()
        event_loop.call_later(.1, self.update, event_loop)

def button_action(sender):
    v1 = sender.superview['view1']    
    if sender.title == 'Reset':
        v1.value = 0
        v1.state = 'stop'
    elif sender.title == 'Start':
        v1.value = 0
        v1.state = 'run'
    elif sender.title == 'Stop':
        v1.state = 'stop'


v = ui.load_view()
v.present('sheet') 

event_loop = asyncio.get_event_loop()
event_loop.call_soon(v['view1'].update, event_loop)
event_loop.run_forever()       


Phuket2

@mikael , thanks again. It seems your implementation would be more standard, however I am not good enough to judge the merits of the different methods shown. But your plans for

Phuket2

@enceladus , they thanks for your code samples also.

I modified your update method in the first example. Just quickly, so it would stop after closing the window. Guessing more checks should be done.

def update(self, event_loop):
        if not self.on_screen:
            event_loop.stop()
            return

        self.set_needs_display()
        event_loop.call_later(.5, self.update, event_loop)

According to the docs the BaseEventLoop class is not thread safe. I am not good enough to under the implications/restrictions of this both as a standalone as well how it would interact with Pythonista's Threads. Any insights would be appreciated.
Again, for me at least is just to have a reliable and controllable way to easily add a way to call a so called poll/update method on a class, including a ui.View async.

Out of the above implementations again I am not good enough to say which is the best implementation. Appears asyncio is py3+ only. Which is fine by me, other might be looking for a 2.7 solution also.

enceladus

I think that calls in event loop are run on the same thread. Button action (in stop watch example) should be put in the same event loop so that there are no thread-safety issues. As you have mentioned there could be other issues. (My experience in asyncio module is very limited and I am in learning mode.)

def button_action(sender):
    event_loop.call_soon(button_action_event_loop, sender)

def button_action_event_loop(sender):
    v1 = sender.superview['view1']    
    if sender.title == 'Reset':
        v1.value = 0
        v1.state = 'stop'
    elif sender.title == 'Start':
        v1.value = 0
        v1.state = 'run'
    elif sender.title == 'Stop':
        v1.state = 'stop'

omz

@Phuket2 I've added something to the latest beta. From the release notes:

In custom ui.View subclasses, you can now implement an update method (no arguments, except for self) that gets called automatically by an internal timer. To make this work, you also have to set the new update_interval attribute. It specifies how often the timer fires (e.g. set to 1.0, the timer fires every second). It defaults to 0.0 (disable update timer) because otherwise this new feature might break existing code that happens to implement a method called update.

I hope this works for you.

Phuket2

@omz , yup pulled another rabbit out of the hat :) was so excited when I seen the email this morning for the new beta. You have seemed to have done a lot. Just did a quick test with the update method. Appears to work perfectly. Just had a button on a form to start and stop the updates by setting the update interval. Very nice. I will do some more tests with opening Dialogs over the top etc... but many thanks, I think this will make a lot of ppl happy. Ok, back to exploring the new features :)

technoway

Is the update_interval, and update, attributes in the latest pythonista?
If now, can I get the beta?

By the way, it would be nice if the update method was an empty method (just "pass") that already existed, so users could set the update_interval and update method in the subclassed view's init method.

And whatever already exists, or is done in the future, thank you for a truly great product!

omz

@technoway It's currently only in the beta. With regards to an empty method in the base class, setting the method in __init__ etc., that's not really possible because the act of implementing the method is a signal to change the behavior (hope that makes sense). The entire update timer machinery isn't initialized at all if your View class doesn't implement update.

technoway

Thank you for the quick reply.

Am I allowed to install the beta version, and if so, where is it?

I am running Pythonista on an iPhone (and iPad too, but I need this particular functionality on my iPhone).

I have purchased both the Python 2.x and the Python 3.x versions of Pythonista for my iPhone.

technoway

Ah, I found the message about sending my apple ID through the e-mail. I'll do that. Thanks again, and sorry for all the noise. (I did search before my last post, but missed the relevant post).

omz

@technoway You should have a beta invite in your email (please check your spam folder if it isn't there).

mikael

@omz, arrgh, spam. I have been patiently waiting for my invite, so long that the spam folder has been purged already. Is it easy for you to resend my invite, or should I just send another request? Sorry.

omz

@technoway I sent the invite to the Gmail address you sent me yesterday. Did you just purge your spam folder?

mikael

@omz, I am not technoway. Sorry for jumping in; I have been waiting for the beta invite in order to test the polling functionality.

omz

@mikael Ah, I'm sorry, I'll send a new invite to the email address you're using for this forum.

mikael

@omz, got it, thanks!

mikael

@omz, some feedback on the update feature, all of it positive:

  • Very stable and consistent. Stops when the view is closed, no threading hassles.
  • Very intuitive to use. Changing the update_interval to 0.0 stops updates, and positive numbers start them again.

With my limited Threading skills, I was unable to create stable and predictable UI animations with either ui.delay or custom Threads. With update, no issues. This is my vote for moving the feature out of beta.

omz

@mikael Thanks for your feedback! I appreciate it.

mikael

@Phuket2, thank you for taking this up.

Phuket2

@mikael , thanks all to @omz. When i had to do something threaded or in a loop, I never felt confident about it. I have a small thing now where its just a label update to show when something expires. It's so nice and simple with update mechanism.
My experience has been same as yours. Very stable.

Phuket2

@mikael , I mentioned something in the github issues area about ui.TableViewCell. @omz said just add your view to the cell. I had forgotten @JonB had helped me with this a long time ago. Anyway, i was playing around. The below maybe is not pretty. But I find it interesting and it shows off a few things. Also how well update works. Well i think it does anyway.

EDIT: to see the cool stuff I think you have to tap a cell and also scroll. You can see how things are getting suspended when you scroll. I think its nice

import ui
from random import choice


_color_list = ['purple', 'orange', 'deeppink', 'lightblue', 'cornflowerblue'
               'red', 'yellow', 'green', 'pink', 'navy', 'teal', 'olive',
               'lime', 'maroon', 'aqua', 'silver', 'fuchsia',
               ]


class MyCustomCell(ui.View):
    def __init__(self, parent, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.cell = parent
        self.tableview = None
        self.blink_count = 0
        self.lb = None
        self.frame = self.cell.frame
        self.flex = 'wh'
        self.width -= 10
        self.x = 5
        self.height -= 10
        self.y = 5
        self.alpha = .5
        self.corner_radius = 6

        # this allows the touch events to pass through my subview
        self.touch_enabled = False

        self.update_interval = .2

        lb = ui.Label(frame=(0, 0, 24, 24), bg_color='black',
                      text_color='white', alignment=ui.ALIGN_CENTER)

        lb.center = self.center
        lb.corner_radius = 12

        self.lb = lb
        self.add_subview(lb)

    def rect_onscreen(self):
        '''
        Have to write this method.  Would be nice if this was built in.
        like ui.TableView.is_visible for example.  I know its just some rect
        math, but it means you need to save extra references etc.. to calculate
        it yourself.
        '''
        return True

    def update(self):
        if not self.tableview:
            return

        # I did not implement this yet. A little drunk and having a party today.
        # but gives the idea...
        if not self.rect_onscreen():
            return

        if self.blink_count == 98:
            self.update_interval = 0

        self.blink_count += 1
        self.lb.text = str(self.blink_count)
        self.bg_color = choice(_color_list)


def create_cell():
    '''
    Create and return a ui.TableViewCell. We add a custom ui.View to
    the TableViewCell.content_view. This means our view is sitting on top
    of the normal TableViewCell contents. All is still there.
    Also create an attr in the cell at runtime that points to our custom class.
    I guess this can be done many ways.  I choose this way for the example.
    To me its at least clear for access.
    '''
    cell = ui.TableViewCell()
    myc = MyCustomCell(cell)
    cell.content_view.add_subview(myc)
    cell.my_cell = myc
    return cell


class MyDataSource(object):

    def __init__(self, data):
        self.data = data
        self.sel_item = 0
        self.cells = [create_cell()
                      for _ in range(len(self.data))]

    def tableview_number_of_rows(self, tableview, section):
        # Return the number of rows in the section
        return len(self.data)

    def tableview_cell_for_row(self, tableview, section, row):
        # Create and return a cell for the given section/row
        cell = self.cells[row]
        cell.text_label.text = self.data[row]

        # just showing we can access our class from the my_cell attr
        # we added. In this case I want to save the tableview attr
        cell.my_cell.tableview = tableview
        return cell

    def tableview_did_select(self, tableview, section, row):
        # Called when a row was selected.
        self.select_row(row)

    def select_row(self, sel_row):
        for cell in self.cells:
            cell.accessory_type = ""

        self.cells[sel_row].accessory_type = 'checkmark'
        self.sel_item = sel_row


def get_table(items):
    tbl = ui.TableView(frame=(0, 0, 300, 400))
    tbl.data_source = MyDataSource(items)
    tbl.delegate = tbl.data_source
    return tbl


if __name__ == '__main__':
    v = get_table(['Ian', 'Fred', 'John', 'Paul', 'Gaew', 'Pete',
                   'Ole', 'Christian', 'Mary', 'Susan', 'Juile'
                   'Simone', 'Terry', 'Michael', 'James'])
    v.present(style='sheet', animated=False)
mikael

@Phuket2, could you make it a gist?

Phuket2

@mikael , sure here is the gist. I forgot it can be hard to copy from here. Anyway, its a stupid example

mikael

@Phuket2, it sure works, although your animations are so subtle, I hardly noticed them at first. :-)

Phuket2

@mikael , lol. But it's stress testing :)

Phuket2

Have to say, you can click around, drag around etc.. everything keeps working which is great

technoway

@omz - Hi. I also found using update_interval and update very straightforward and it worked without a hitch.

Sorry I did not give any feedback before now. I didn't know where to give feedback. Now, I see that this is the place to do that.

I had no issues with programming the beta version at all.

There is a bug in an example program, which also exist in the release (non-beta) version. The Calculator.py example in the "Widget" folder gives "v is not defined" - "v" should be "widget_view". When that change is made, the program works. (The other Calculator.py program in the "User Interface" folder has no issues).

I noticed the download in the App store has been changed to be named, "Pythonista 3", I presume to match the version of Python used. The main application screen still has "Pythonista 2 Documents".

Thanks for creating such a great App.

omz

@technoway Thanks for your feedback.

Pythonista 3 is the current version. The folder "Pythonista 2 Documents" exists to access files from the old version. It only shows up if you had version 2 installed at some point, and you can turn it off in the settings.

technoway

If not running the Pythonista 3 beta, someone can also have an update method be periodically called as shown below.

This is not my idea, I just simplified an idea in the TimedRefreshView.py example program that I found at:

https://github.com/cclauss/Pythonista_ui/blob/master/TimedRefreshView.py

This program below, named say_random_digit.py, says a random digit from 0 to 9 every 5 seconds.

I had to change the data member "self.update_interval" to "self.updatex_interval" and the method "update" to "updatex" so as not to conflict with the names in the Pythonista 3 beta, which I am running.

I like having this functionality built into the class much more than having to implement it, so I look forward to the beta becoming the released product.

import ui
import threading
import speech
from random import randint

class TimedUpdateView(ui.View):

    def __init__(self):
        self.updatex_interval = 5
        self.update_after_delay()

    def updatex(self):
        """ Say a random digit from 0 to 9 every 5 seconds. """
        speech.say('%s' % (randint(0, 9)))

    def update_after_delay(self):
        """ This just method calls the updatex method periodically """
        self.updatex()
        update_thread = threading.Timer(self.updatex_interval,
                                        self.update_after_delay).run()

if __name__ == "__main__":
    v = TimedUpdateView()
    v.present('sheet')
technoway

I just updated my iPhone 6s to IOS 12.0.1, and the 'update' method is not longer called in my ui.View derived class.

I wonder if anyone else has encountered this?

mikael

@technoway, 12.0.1 on iPhone X, still works as previously.

JonB

You sure you have update_interval set?

technoway

Thank you mikael and JonB for the help.

@JonB said:

You sure you have update_interval set?

Yes. I have:

self.update_interval = 1.0

in my derived class's __init__ method.

It likely I did something to break code that was working, but I don't see anything wrong. I'll debug my application - if this works for others, then I'm sure I introduced a bug somewhere. It's odd though, because I only made two very small changes and then updated the OS.

Next time, before I upgrade the OS, I'll test before to make sure it's the OS that breaks code, and not my code changes.