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.
Attaching a class to a ui element such as uiButton (I am desperate)
-
I know this maybe a mute point for many, But I really would love a way to attach a class (a normal class, inheriting from object) to a ui object. In a generic way. Yes, I could write a custom class to encapsulate the ui element inside a custom class. However 2 things bother me about this approach, memory and performance overhead as well as losing the ability to code my custom class as I would say a normal button. Well at least I think that's the case. I tried to be smart and stuff my class reference into the transform attribute. But got a error of expecting a ui.transform object (makes sense). I had a glimmer of hope that if I could stuff the reference in there, maybe the underlying code only acknowledges its presence if that objects method are called explicitly on the object(I haven't seen class casting, maybe it's possible). Yeah, slim chance to none I guess. I Had to try. But again, I find my code gets so dirty and unwieldy quickly with this limitation when dealing with lots of ui objects. I accept it could be just my inexperience. I mentioned memory and performance overhead above. But I mention it because one view I present is a calendar. Each day is made up of 5 buttons and 2 views. Could be more by the time I am finished. But that alone is 155 buttons and 62 views on the screen for 31 days. Then I have a header for the days of the week, then a status bar. I also need filler bars for the previous months days etc...
Anyway, getting close to 250++ items on the view. Again, maybe this is not a problem. I just really don't know.
Sorry, I know this is a long winded post, but it's difficult to explain why I really want to be able to attach my own class to a ui element rather than encapsulating it in a custom class. I did briefly give some thought to trying to use the name of the ui element then trying to do something with eval, but I can see big potential problems with the garbage collector with reference counts and other things I don't even understand. But I have seen you guys do some amazing tricky code here before. I was hoping someone maybe able to pull a rabbit out of the hat that will enable me to attach and recover a class to a ui object generically.Big thank you in advance!
A screen shot of the calendar I make reference to above. This pic has no days of the week or status bars.
https://www.dropbox.com/s/2ga2ndwmfioh9x7/file 6-07-2015 19 11 40.png?dl=0 -
I'm working on something to make this sort of thing easier.
-
@omz, thank you so much for the info. Your app touches my life every day. I love it.your app is something to be truely be proud of. I think you don't like the compliments so much, but I had to say it!
-
I do this by supplying a custom action for the ui element which overloads call(self, bnt): self.action(btn) to perform the action. Then I simply add whatever I need to this object and access it with the ui.action member.
-
that's a good idea gcarver: a method of your custom class can return the button instance (or create it for first time), that way you can do styling, etc from outside the button as well.
Phuket, it is also possible to have a custom encapsulating View with methods of "telegraphing" attribute changes down to the enclosed subview. there are two methods I have used for this.
first, you can use @property's for some of the key features, like action, bg_color, title, etc, or, you can override
def __setattr__(self, name,value):
and
def __getattribute__(self,name):
to pass on the appropriate attribute changes.
see http://github.com/jsbain/uicomponents
dropdown.py for the former approach, and AdvancedTextField.py for the latter. not that for things like size, you can just use flex WH, and size the subview to fill the view when it is first created.you can/should also create a custom init that either takes kwargs and calls
ui.View.__init__
, or handles them one at a time so you can provide rational defaults. -
@Gcarver, yes sounds like a great idea. But sorry I don't get it. But I want to understand it. I would appreciate if you could do a code snippet.
To prepare I did this,
import ui def eureka(sender): print 'eureka' if __name__ == '__main__': v = ui.View() btn = ui.Button(title = 'eureka moment') btn.action = eureka v.add_subview(btn) v.present('sheet')
Or do I need to start off with a custom class?
-
My approach to this is to abuse delegates, because they are Python classes/objects that can have arbitrary attributes. It would be nice if all
ui.View
subclasses could have custom attributes, I assume they can't yet because they are C classes without a__dict__
or something? -
@JonB, thank you, I sort of thought I could do something like telegraphing, but in my opinion, way to messy and can break so easily. In my mind, I want a solution that won't break. As I would like to build Libs off this construct. If its fragile code, really no point. Could be ok for one small project, but then again one small project you don't need the flexibility. Could just hard code it and be done with it.
Omz says he is working on something, not sure about the implementation he has in mind. In my view he just needs to add a variable to each ui element. (Yes I know I am talking about the tag again) but in this language it's 1 million times better than what we had.If we didn't care then would be no point discussing :)
-
@dgelessus, wow, sorry I also don't get it. I guess I am missing something here. as far as I know only a few ui objects have delegates. Unless it's undocumented, a ui.Button for example does not have a delegate class as far as I can see, or does it?
-
No, buttons don't have a delegate, this solution of course only works with objects that have a delegate. I've never had to store extra data on delegate-less elements, so that always worked for me.
Gcarver's solution uses a custom class, which probably looks something like this:
class CustomAction(object): def __init__(self, action=None): self.action = action def __call__(self, sender): return self.action(sender)
The
__call__
method makes instances ofCustomAction
callable like a function:def real_action(self, sender): print("Eureka!") fake_action = CustomAction(real_action) button = ui.Button() button.action = fake_action button.action.random_attr = 42 # No error!
Because
button.action
is an instance of a Python class, you can assign new attributes to it.Now let's look at what happens when
button.action
is called:- You tap the button
ui
callsbutton.action
button.action
is not a function, but it has a__call__
method, so that gets called insteadbutton.action.__call__
callsbutton.action.action
button.action.action
printsEureka!
Like the delegate variant, this only works on views that have an
action
. -
@dgelessus, just return home after a hard night out. I think i follow the CustomAction code. I will give it a try in in some hours from now. 1:05am here already. But really thank you, it looks promising
-
neat idea with the
__call__
... I learned something new!yet another variation on gcarvers approach, which could be combined above: generally if you have extra data, you are going to be modifying it outside of an elements
action
... so it is not necessary to access the custom object from within the ui element -- you probably already have a custom View containing class which can store the data object in a more accessible form(such as a list, or dict, or named attribute). if you use custom @property getters/setters, the underlying data is accessible, and can also update the ui element when the data changes.here is a simple counter which has a data object, which also acts as a controller, as you can update the count which then automatically updates the view. this is a trivial example, but you could easily imagine a complicated object that updates a complicated bit of view... for instance, a Day controller object might have multiple representations, such as might be shown on a whole month vs single day. the higher level view doesn't need to know about how to draw a Day, the Day object can take care of that, and only triggers updates when the data changes
import ui class counter(object): def __init__(self): self._count=0 self.button=None def getButton(self): '''return the ui button object associated with this object, or cate a new instance''' if self.button is None: self.button=ui.Button(bg_color=(0,0,1)) self.button.action=self.button_action self.button.title=str(self._count) self.button.width=100 self.button.height=100 return self.button @property def count(self): return self._count @count.setter def count(self,value): '''update count attribute, and update the button title to show current count''' self._count=value if self.button: self.button.title=str(self._count) def button_action(self,sender): '''increment count in the underlying model. the @property takes care of updating the ui''' self.count+=1 b=counter() v=ui.View(bg_color='white') v.add_subview(b.getButton()) v.present() #note, if you set b.count from outside the view, the button title gets updated.
-
Why not create a
ButtonView
class that is a ui.View that embeds a ui.Button inside and attach the Button frame size and action to the ButtonView frame size and action? This provides an object which can be customized to hold any data and have complex functionality yet it is a ui.View so it behaves nicely (resizing, hiding, colors, etc.) within a hierarchy of ui elements. By making all the dates on your calendar ButtonViews, you could have custom ui elements that held rich data and functionality yet in a ui view hierarchy, they behave the way that you expect all ui elements to behave. -
Guys, thanks for all the input. I have had some friends turn up to visit (I live in a tourist town), I haven't had time to try much, but I will try each solution. I can not just read each solution and understand all the ramifications. Just don't have the experience yet. Just wanted to let you all know I appreciate all your solutions and I will definitely try them all.
-
@Dgelessus, I could not get your example working exactly. But is still great. Thank you @Gcarver!!
Is a very nice option. I did this :import ui class CustomAction(object): def __init__(self): self.action = self.real_action self.myid = 666 def __call__(self, sender): return self.action(sender) def real_action(self, sender): print("Eureka!") btn = ui.Button(title = 'test') btn.action = CustomAction() btn.action.random_attr = 42 # No error! print btn.action.myid, btn.action.random_attr v = ui.View() v.add_subview(btn) v.present('sheet')
The ui elements that have action
Button has action method
ButtonItem has action method
SegmentedControl has action method
Slider has action method
Switch has action method
TextField has action method
DatePicker has action method -
After playing around, and reading, my previous post would seem like the best solution for me. Of course it would be better if every single ui element had an action, but it seems the elements I normally deal with today are covered. But ultimately the real solution will come from omz. Just takes one common user property across all ui classes. But it is still a fantastic discussion. Brings out a lot of innovation from you guys.
-
Have to say it's very nice, the CustomAction(object) approach....
import ui import uuid import datetime class CustomAction(object): def __init__(self): self.action = self.real_action self.uuid = uuid.uuid4() def __call__(self, sender): return self.action(sender) def real_action(self, sender): print("Eureka!") if __name__ == '__main__': btn = ui.Button(title = 'test') btn.action = CustomAction() btn.action.random_attr = 42 # No error! btn.action.today = datetime.date.today() print btn.action.uuid, btn.action.random_attr print btn.action.today td = datetime.timedelta(days = 1) btn.action.today += td print btn.action.today print '*' * 59 print 'dir btn' print dir(btn) print '*' * 59 print 'dir btn.action' print dir (btn.action) v = ui.View() v.add_subview(btn) v.present('sheet')
Seems to work very generically.
-
I did some more playing around with the CustomAction Class. I was able to use isinstance to verify if an action has a CustomAction, also if no action is provided from the calling class, a function in the CustomAction is called.
One thing I am not sure about is the getting a reference to the parent object that the CustomAction is attached to. At the moment, I pass the parent object in the init of the CA Class. I Would appreciate if anyone could tell me how I could get a reference without having to pass it as a param. Seems to me I am missing something easy here. This would just clean it up a little.
I think the rest of my code is ok. Just trying to get a basic template in place.import ui import uuid def make_btn(title, use_custom_action = False, action = None): btn = ui.Button(title = title) if use_custom_action: btn.action = CustomAction(btn, action) else: btn.action = action btn.border_width = 1 btn.width , btn.height = 100, 32 return btn class MyCustomClass(ui.View): def __init__(self): self.background_color = 'white' # make a btn with a CustomAction, with action btn = make_btn('Custom',True, self.btn_act) self.add_subview(btn) btn.x , btn.y = 100, 100 # make a btn with a CustomAction, # with no action, will call CustomAction action btn = make_btn('Custom 2',True, None) self.add_subview(btn) btn.x , btn.y = 100, 150 # referencing the CustomAction inline self.ext(btn).group_id = 666 # create a button without the CustomAction btn = make_btn('Normal',False , self.btn_act) btn.x , btn.y = 100, 200 self.add_subview(btn) # pass though reference to the CustomAction def ext(self, obj): return obj.action def btn_act(self, sender): # check to see if sender has a CustomAction if isinstance(sender.action, CustomAction): ca = self.ext(sender) print ca.index, ca.group_id, ca.alt_text, ca.uuid else: print sender.title class CustomAction(object): def __init__(self, parent, action = None, group_id = None ): # i think i need to pass in the parent obj in # the init to be able to recover it in my code self.obj = parent # if no action is passed on init, use a function in the CustomActionClass if not action: action = self.fallback_act self.action = action #some vars i think will be useful self.index = -1 self.group_id = group_id self.alt_text = None # may or may not be useful later self.uuid = uuid.uuid4() def __call__(self, sender): #the magic thanks to pythonista Forums return self.action(sender) # this func is called if no action is supplied # in the init def fallback_act(self, sender): print 'called in the CustomAction' print self, sender if __name__ == '__main__': x = MyCustomClass() x.present('sheet')
-
That is correct. Without storing the "parent" object as an attribute, there is no way to tell what an object's "parent object" is. The reason for that is simple - a single object can have more than one name. For example:
class Useless(object): def __init__(self, attr=None): self.attr = attr test_list = [] useless1 = Useless(test_list) useless2 = Useless(test_list) # Now both useless1 and useless2 have test_list as their parent. # Proof: useless1.attr.append('hi there') print(useless2.attr) # --> ['hi there']
There is no single "parent object" in this case. So yes, you need to store the parent as an attribute on
CustomAction
. In most cases thesender
parameter should be enough though, unless you need to use it outside of__call__
. -
@dgelessus, ok thanks. I thought I might have been missing some trick. But makes sense what you say. I think it's better just to pass the parent each time rather than speculating when you actually might need to use the parent outside call. So I will just do that.
Thanks again