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.
[Share] An attempt to centralize actions to the so called root class
-
Maybe this should be called experimental or crap. Get it's still good to get feedback.
The point here is trying to deal with actions coming from objects in view that is made up of a composite of many views that can contain many subviews. The idea was to point all the actions to one method, in the parent class to dispatch them. Easy enough. But dispatch to were. In this case I wanted the root object.
I am not saying this is good or there is not a far simpler way to do it.
But the one reality I find is, you can get as smart as you like building up views from numerous views, that contain numerous subviews. Then your button 5 or 6 layers deep needs to do something. If we say in MVC (Model/View/Controller)model , the root view class is basically your controller. It seems to make sense to dispatch your actions from there. Well, that's my thoughts anyway. Please don't hesitate to tell me I have my head up my a**. I am trying to make lasagna from spaghetti 😁 I also can take a whipping.Maybe this subject has been broached here. I don't really remember it. But it does come up as you make more complicated views.
The code below, just click in the field and hit enter. Just prints some info
import ui def get_root(v): # get the root view of the view sv = v while sv.superview: sv= sv.superview return sv class ViewBase(ui.View): def __init__(self, w, h, *args, **kwargs): super().__init__( *args, **kwargs) self.w = w self.h = h self.cc = ui.View(frame = self.bounds) self.cc.flex = 'wh' self.add_subview(self.cc) def layout(self): if not self.superview: return sv = self.superview if self.w <= 1: self.width = sv.bounds.width * self.w else: self.width = self.w if self.h <= 1: self.height = sv.bounds.height * self.h else: self.height = self.h if hasattr(self, 'Userlayout'): self.Userlayout() # an attempt to rationalise action flow. well in this case, directing # all actions to single method in the root class , if the root class # has exposed the method. def dispatch_action(self, sender): root = get_root(sender) if hasattr(root, 'actions'): root.actions(sender) class SearchView(ViewBase): def __init__(self, w=1, h=1 , *args, **kwargs): super().__init__(w, h, *args, **kwargs) self.fld_search = None self.make_view() def make_view(self): self.bg_color = 'darkgray' r = ui.Rect(*self.bounds).inset(6, 8) sf = ui.TextField(frame = r, name = 'SearchTextField') # here, a test to attempt to pass actions through to the root view sf.action = self.dispatch_action sf.placeholder = 'Search' sf.flex = 'wh' self.fld_search = sf self.add_subview(sf) def Userlayout(self): # we want to base class to do some inital work in Layout. #if this method exists in the child class its called. pass class NavigationView(ViewBase): def __init__(self, w=1, h=1, *args, **kwargs): super().__init__( w, h, *args, **kwargs) self.bg_color = 'lightyellow' class MyClass(ui.View): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # attempt to centralise action handling without explicitly setting # the action chain. this method needs to of course examine each # sender to take the appropriate action. def actions(self, sender): print('sender' , sender, sender.name) if isinstance(sender, ui.TextField): print(sender.text) if __name__ == '__main__': w, h = 600, 800 f = (0, 0, w, h) mc = MyClass(frame = f, bg_color = 'white') nv = NavigationView(w = 1, h = 44) sv = SearchView(.5, 1) nv.add_subview(sv) mc.add_subview(nv) mc.present('sheet')
-
This does not feel right to me. What if you have two textfields with different actions, this requires complex logic at the root level to sort out. Also seems to limit reuse.. if you come up with an awsome filterable tableview widget that uses this approach, if i want to use it, i would have to know the innards of your control.
I like the delegate pattern for this sort of thing-- your reusable widget would define delegate methods that you connect at the appropriate level.Alternatively, you could sort of "auto delegate", by seaching up the view chain. This would allow reuse and encapsulation -- i.e a control containing a searchfield gets first crack at handling search actions, rather than pushing it all up to the top.
def dispatch(action_name): def action(sender): sv=sender.superview # walk up view chain to find callable action_name while sv: a=getattr(sv,action_name, None) if callable(a): return a(sender) sv=sv.superview return action #example sf.action=dispatch('searchfield_action')
-
@JonB , I agree. I also have mis giving a about it. But I keep thinking about it, and do nothing about it. I thought at least if I have something to pull apart it my show a clearer way forward.
I personally feel that a bubbling up msg style is the best. I mean like HyperCard/SuperCard Visual Basic ui builders had. Eg, a button press on a card/view could be intercepted at the card level, stack level (collection) of cards and at the project level. Pretty sure you could also stop the msg bubbling up if you wanted to or not, so it could be intercepted at all the various levels if you wanted to be. The other nice thing with that system is could you could generate those Msgs in code yourself. Hence ability to automate your ui for testing. One thing that would really help in such a system is static id's for each item and a index all maintained by the ui.py.Your dispatch function is sort of doing it. It's a big improvement over what I am doing. I am jumping straight to the root.
But would message system be better? A queue of messages say dict records. How entangled does it have to be in the ui. Not sure something like this could be built nicely as an afterthought. Anyway, will keep trying. I will go back and use your dispatch function and see how it feels using it in practice.
But as always, thanks for input and code 😁
Will post back if I can make any progress -
@JonB / others, maybe you think this attempt is worse than the previous attempt. I like my idea here in concept. Again, not sure it makes sense to implement on top of what is here. I think even if this looks stupid, if you think ui just worked like that or something like that out of the box I think it makes more sense.
Anyway, the below is a rough concept for messaging/event driven ui. I understand, to have something like this, it needs to be really well designed and thought out.
''' Pythonista Forum - @Phuket2 ''' import ui, editor import uuid, time from collections import namedtuple _msg_fields = ['target', 'func_name', 'action_type', 'post_time' ] MSG = namedtuple('MSG', _msg_fields) class UIMessenger(object): def __init__(self): self.msg_listeners=[] self.msg_queue = [] # just a list to begin with def add_listener(self, obj): if obj not in self.msg_listeners: self.msg_listeners.append(obj) def send_message(self, obj): # insert a msg into the queue programatically. Not thought yet, # in terms of how to get obj easily. # but just to demonstrate a point. self.post_msg(obj) def post_msg(self, sender): if not sender.name: sender.name = str(uuid.uuid4()) msg = MSG(target=sender, func_name = sender.func_name, action_type = 'click', post_time=time.time()) self.msg_queue.append(msg) # calling the below self.process_msg() as just testing... # this class needs to be on a thread # or in a loop to continously process msgs self.process_msg() def get_msg(self): return self.msg_queue.pop(0) def process_msg(self): # processing one msg here as a test, but for each listener. # in a real implementation, # get_msg could return a generator. process each msg possibily # with a time.sleep very small time to keep async msg = self.get_msg() for obj in self.msg_listeners: if hasattr(obj, msg.func_name): result = getattr(obj, msg.func_name)(msg) if result: break # a global messenger controller gUIMessenger = UIMessenger() def set_custom_attrs(obj, **kwargs): # set attrs passed in kwargs but are not in ui.View but instance # attrs set in the object attrs = set(kwargs) - set(ui.View.__dict__) for attr in attrs: if hasattr(obj, attr): setattr(obj, attr, kwargs.get(attr)) class WidgetBase(ui.View): # using the base, so the child class can be hosted in code or # in a pyui file. Not coded yet, but thats the idea def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) set_custom_attrs(self, **kwargs) class ToolItem(WidgetBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.make_view() def make_view(self): btn = ui.Button(title = 'Tool') btn.border_width = .5 btn.corner_radius = 3 btn.width = 100 btn.func_name = 'tool_action_copy' btn.action = gUIMessenger.post_msg self.add_subview(btn) if __name__ == '__main__': w, h = 600, 800 f = (0, 0, w, h) class MyClass(ui.View): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.ti = ToolItem(frame = (50, 50, 200, 32)) self.add_subview(self.ti) gUIMessenger.add_listener(self) def tool_action_copy(self, msg): # was not sure how to get the action_type 's working. # didnt spend a lot of time on it. but more to show that # objects normally respond to more than one action/msg if hasattr(self, msg.action_type): getattr(self, msg.action_type)(msg) def click(msg): print('clicked') def dbl_click(msg): print('Double Clicked') print(msg) # maybe namedtuple is not so smart, cant write back to the # msg return True # we handled this mc = MyClass(frame=f, bg_color='white', shit = 78) mc.present('sheet', animated=False)
-
Hmmm, I can see I should have added some notes...
Firstly the term listener is used out of context. A listener would normally imply it's listening, on a thread etc. This implementation that's not happening. It's more like registering objects that potentially can deal with a message. So basically those objects are inspected To see if they can or can not deal with the message.
Also, what I tried to show is an action that has handlers inside the action. Nothing new. HyperCard did it. But an action can have states hence should generate multiple messages and you should be able to trap them. Like a click, double_click etc... Currently if behaviour like this is required in Pythonista it's done via a delegate.
But of course you could raise any event/msg that was meaningful to your object. Like long_press, title_changed etc... As I say, I am thinking it probably gets very inefficient in Python , but done the right way possibly not.
To me Python seems blindly fast. So maybe a event driven ui messaging system could work fast enough.Also I purposely don't try to bind the action to object. Yes, it's inefficient, but was going for more flexibility then speed. In this sort of model everything is going to be late bound.
Ok, just my extra 2 cents worth
-
@JonB , not sure what you think of the below mods to your dispatch func. I am doing something with multiple views at the moment. And this seems pretty good. Maybe speed could become an issue if you had a lot of layers, but I doubt it.
If any action/handler returns True, it stops looking for handlers. Idea being you have handled that event in its entirety. Or conversely it can execute all the handlers in the view hierarchy.
Again I am not trying to bind a function/method to an object. A little inefficient, but would not break if you dynamically added a custom view with handlers that should be executed. Also, if you allow multiple handlers to be called, binding to a single func/method does not make sense anyway....''' Pythonista Forum - @Phuket2 ''' import ui def dispatch(sender): # based on @jonB code # make sure the sender obj has an attr 'event_handler' if not hasattr(sender, 'event_handler'): return sv=sender.superview handler = sender.event_handler # walk up view chain to find callable action_name while sv: a=getattr(sv, handler, None) if callable(a): stop = a(sender) if stop: return # if any handler returns True, we exit sv=sv.superview return class BClass(ui.View): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.make_view() def make_view(self): btn = ui.Button(frame = (20, 20, 100, 32 )) btn.title = 'Next' btn.action = dispatch btn.event_handler = 'do_next' btn.border_width = .5 self.add_subview(btn) btn = ui.Button(frame = (20, 80, 100, 32 )) btn.title = 'Prev' btn.action = dispatch btn.event_handler = 'do_prev' btn.border_width = .5 self.add_subview(btn) # do_next is also defined in the parent class def do_next(self, sender = None): print('BClass:do_next') #return True class MyClass(ui.View): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.view_b = BClass(frame = self.bounds) self.add_subview(self.view_b) def do_next(self, sender = None): print('MyClass:do_next') def do_prev(self, sender = None): print('MyClass:do_prev') if __name__ == '__main__': w, h = 320, 320 f = (0, 0, w, h) mc = MyClass(frame=f, bg_color='white') mc.present('sheet', animated=False)
-
@JonB , ok this is another refinement. To make it more generic, as well as receiving events can also raise our own events via the same mechanism. It's sort of ok, but I know it lacks some finesse.
I know it's not great, but I feel it's getting there...
If anyone has some ideas about how to improve this idea, love to hear about it.''' Pythonista Forum - @Phuket2 ''' import ui def raise_event(sender, event_handler = None, **kwargs): sv=sender.superview if not event_handler: if not hasattr(sender, 'event_handler'): return else: event_handler = sender.event_handler handler = event_handler # walk up view chain to find callable action_name while sv: a=getattr(sv, handler, None) if callable(a): stop = a(sender, **kwargs) if stop: return sv=sv.superview return class BClass(ui.View): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.make_view() def make_view(self): btn = ui.Button(frame = (20, 20, 100, 32 )) btn.title = 'Next' btn.action = raise_event btn.event_handler = 'do_next' btn.border_width = .5 self.add_subview(btn) btn = ui.Button(frame = (20, 80, 100, 32 )) btn.title = 'Prev' btn.action = raise_event btn.event_handler = 'do_prev' btn.border_width = .5 self.add_subview(btn) # do_next is also defined in the parent class def do_next(self, sender = None): print('BClass:do_next') #return True def change_btn_name(self, sender, **kwargs): sender.title = kwargs.get('title', '') class MyClass(ui.View): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.view_b = BClass(frame = self.bounds) self.add_subview(self.view_b) def do_next(self, sender = None): print('MyClass:do_next') def do_prev(self, sender = None): print('MyClass:do_prev') raise_event(sender,'change_btn_name', title = 'WOW') if __name__ == '__main__': w, h = 320, 320 f = (0, 0, w, h) mc = MyClass(frame=f, bg_color='white') mc.present('sheet', animated=False)