Forum Archive

Listening for View.frame changes

brx

Hi all, is it possible to listen for changes in View.frame, View.x etc... so that changing them could results in calling other functions?
I tried subclassing ui.View and using a property(get, set, del) called x but changes on it doesn’t reflect in View.frame.


class PView(ui.View):
  def __init__(self):
    self._x = None
  @property
  def x(self):
    return self._x
  @x.setter
  def x(self, value):
    self._x = value
    self.layout()
  def layout(self):
    print(self.frame)

Let say I have c = PView()
Now c.frame leads to Rect(0, 0, 100, 100)
Calling c.x = 7 will print Rect(0, 0, 100, 100) instead of Rect(7, 0, 100, 100)
Also, calling c.frame = (9, 0, 100, 100) doesn’t print Rect(9, 0, 100, 100) as requested and c.x doesn’t lead to 9.

mikael

@brx , here's how to do that:

import ui

class TrackView(ui.View):

    @property
    def x(self):
        return ui.View.x.__get__(self) 

    @x.setter
    def x(self, value):
        print('changed')
        ui.View.x.__set__(self, value)

v = TrackView()
v.x = 7
assert v.frame == (7, 0, 100, 100)

Now, writing that several times for all the different properties would be boring, so:

from functools import partial
import ui

class TrackView(ui.View):

    def _prop(attribute):
        p = property(
            lambda self:
                partial(TrackView._getter, self, attribute)(),
            lambda self, value:
                partial(TrackView._setter, self, attribute, value)()
        )
        return p

    def _getter(self, attribute):
        return getattr(ui.View, attribute).__get__(self)

    def _setter(self, attribute, value):
        print('changed')
        return getattr(ui.View, attribute).__set__(self, value)

    x = _prop('x')
    y = _prop('y')
    width = _prop('width')
    # And so on...


v = TrackView()
v.width = 200
assert v.frame == (0, 0, 200, 100)

This works for your custom view, but if you wanted to track the change of any view (like ui.Label), we would need to use the ObjC KeyValueObserving protocol. Implementation here, or you can get it with pip install pythonista-anchors.

brx

Thank you @mikael , it works. Maybe it is something basic but for my actual level in python, it was hard to imagine. Also I think I have to study more to understand the second example… can’t say anything about the third, python first, then bindings… 😤

mikael

@brx, the last option is the easiest to use, I think. See the example below, where the key part is the on_change function. It takes a view and a function to call if the size or position of the view changes; called function gets the view that changed as an argument.

import ui

from anchors.observer import on_change


class Mover(ui.View):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.add_subview(
            ui.Label(
                text='Move me',
                text_color='white',
                alignment=ui.ALIGN_CENTER,
                center=self.bounds.center(),
                flex='RTBL',
            )
        )

    def touch_moved(self, t):
        self.center += t.location - t.prev_location


v = ui.View()

mover = Mover(
    background_color='darkred',
    center=v.bounds.center(), flex='RLTB',
)

mirror = ui.View(
    background_color='lightgrey'
)

v.add_subview(mover)
v.add_subview(mirror)

def move_mirror(sender):
    mirror.center = sender.center + (0, 200)

on_change(mover, move_mirror)

v.present('fullscreen', animated=False)

Several functions or methods can be registered to watch one view, and there is a remove_on_change function to stop observing the view (same arguments as on_change).

brx

That seems easier… I’ll try it! Thanks