Shared UI widget code design question


#1

I have a UI with multiple tabs. There are some simple UI widgets that are shared across these tabs (since I’m designing this in Maya they are duplicates, but have the same functionality), and these widgets interact with each other in similar ways across the different tabs. Is there is a smart approach regarding how to structure their callbacks? I want them to do similar things but they need to do them to different widgets depending on which tab is selected. I can think of a couple approaches:

  1. Write different methods for each callback, for each tab. So like :
def onPressTab1():
    # use tab1widget
def onPressTab2():
    # use tab2widget
  1. Have one method but pass it a value that tells it which tab is active:
def onPress(tab):
    if tab == 'tab1:
        # use tab1widget
    elif tab == 'tab2':
        # use tab2widget

3 Have the method determine which tab is active:

def onPress():
    label = tab.getLabel()
    if label == 'tab1':
        # use tab1widget
... and so on

If these were more complex widgets (or had more interdependencies) I would consider making them a class that contains its own methods, but these are fairly simple and that feels like overkill.


#2

You can make the event pass a functools.partial object which binds some arbitrary data with the function call. So you only need one handler func that gets the right widget as an argument:


from functools import partial

def handler(*args):
    print args[0]
    
w = cmds.window()
c = cmds.columnLayout()
b = cmds.button("b1", c = partial(handler, "b1 clicked"))
b2 = cmds.button("b2", c = partial(handler, "b2 clicked"))
cmds.showWindow(w) 


You can do the same thing with a lambda - except that the lambda can trip you up if you are creating controls in the loop (see http://techartsurvival.blogspot.com/2014/04/maya-callbacks-cheat-sheet.html)

You can make the handler pass the name of the calling widget by doing it in two lines:


def handler(*args):
    print args[0]
    
w = cmds.window()
c = cmds.columnLayout()
b = cmds.button("b1")
cmds.button(b, e=True, c = partial(handler, b))
b2 = cmds.button("b2")
cmds.button(b2, e=True, c = partial(handler, b2))
cmds.showWindow(w) 

Either approach should make it fairly simple to share a handler across multiple user widgets.


#3

The thing about this case is that there are often small differences in the behavior of a similar callback for each widget. w2 may do the same thing as w1 plus an additional function call, or maybe there’s an added process in the middle of the method. So either I have a shared method that looks something like this:

def callback(self):
    tab = self.getActiveTab()
    widget = self.commonUI[tab]['widget']

    # do common procedures

    if tab == 'tab1':
        # do stuff unique to tab1

Or I have two separate calls, one for each widget.

Since there is a fair amount of shared functionality I am leaning towards the first, but then my code will be scattered with if statements and I’m a little worried that it may turn into a fairly complex series of methods since there are quite a few that would operate on both sets of widgets. Is there is a general design method for something like this?


#4

If I am understanding you, I would tackle it like so:


MyBaseWidget(QtGui.QWidget):
    def __init__(self):
        # setup all the stuff that is the same... layouts, formatting, etc.
    def callback(self):
        # re-implement on sub-classes for specific behavior...
        pass


class MyFirstWidget(MyBaseWidget):
    def callback(self):
        # do custom stuff

class MySecondWidget(MyBaseWidget):
    def callback(self):
        # do custom stuff


#5

I have a hard time deciding when that’s the correct approach or when it’s overkill. These widgets have simple functionality, are only shared between two tabs, and won’t be used in another UI. The other thing is that the layouts and functionality are a little different between the two, so I can’t just create one base UI and only alter its behavior through subclassed methods.

The gist of that portion of the UI is that it allows browsing/viewing two asset types (bodies and clothing). Bodies only requires specifying a character type and body type. While clothing requires those two plus some additional subtypes (so it contains a couple more UI elements). They both display thumbnails and display the paths associated with the selected asset. It feels overkill to create a simple widget that has events/callbacks and placing that in both layouts, but maybe it’s not? I’m just not sure what approach to take to breaking up the UI elements and their callbacks. There are other portions of the UI that need to be notified of changes in asset selections, so it can’t be completely self-contained. I’m leaning towards just having separate widgets and callbacks for each layout. Although it would add duplicate code, it would easy to differentiate between the two.

Hopefully that description makes some sense.


#6

If you want to keep it light, you can use static helper functions to bundle multiple separate functions into a single callback. Then you can share what’s shareable but customize as needed).


from functools import partial

def create_handlers (widget, *callables):
      """
      returns an method that calls all of the supplied callables on <widget>
      """
      partials = [partial(c, widget) for c in callables]
      def wrapper(*args):
          return [p(*args) for p in partials]
      return wrapper
    
def add_handlers(target, cmd, event, *handlers):
    '''
    call <cmd> on <target>, editing it so that <event> fires the handler function(s) 
    '''
    cfg = {"e": 1, event: create_handlers(target, *handlers)}
    cmd(target, **cfg)

w = cmds.window()
c = cmds.columnLayout()
x = cmds.button()

def a(*args):
    print  "A called"
def b(*args): 
    print "B called"
    
add_handlers(x, cmds.button, "command", a, b)
cmds.showWindow(w)

The big thing is how much context to bundled up into the handler – this passes just the calling widget but you could pass something more elaborate, like a manager class that knew about more than one item in the gui.

I do think, though, that classes are really the way to go here. The costs aren’t big compared to the long term maintenance wins.


#7

Assuming just using maya’s ui commands, what method do you use to set up events/callbacks from your custom widget classes so that the parent UI can trigger events when certain changes occur?


Maya python modules and mehtod naming
#8

Here’s a really minimal example. MGui does more of this, including multicast delegates and pymel-style dot access to properties, but this is a cheap way to make extensible widgets:


class Widget(object):
    CMD = cmds.control
    
    def __init__(self, *args, **kwargs):
        self.widget = self.CMD(*args, **kwargs)
       
    def set(self, **kw):
        flags = {'e': True}
        flags.update(**kw)
        self.CMD(self.widget, **flags)

    def get(self, **kw):
        flags = {'q': True}
        flags.update(**kw)
        self.CMD(self.widget, **flags)
        
        
    def __repr__(self):
        return self.widget
        
class Button(Widget):
    CMD = cmds.button
    
    def __init__(self, *args, **kwargs):
        super(Button, self).__init__(*args, **kwargs)
        self.set(command = self.clicked)
        
    def clicked(*_):
        print "clicked"
        
class OtherButton(Button):
    
    def clicked(self, *_):
        super(OtherButton, self).clicked(*_)
        print "and did other stuff on", self
    
w = cmds.window()
c = cmds.columnLayout()
b = Button("button")
b2 = OtherButton("special button")
cmds.showWindow(w)

if you wanted to call the functionality from outside you could just do “b2.clicked()” although that’s bad practice. If “I can click or I can call” is the goal, then:


class Button(Widget):
    CMD = cmds.button
    
    def __init__(self, *args, **kwargs):
        super(Button, self).__init__(*args, **kwargs)
        self.set(command = self.clicked)
        self.execute = lambda ignore: None
        
        
    def clicked(self, *_):
        print "clicked"

You can replace execute with some other function that does the hard work but can be managed independently: for example you could tack this on to the end:


b2.execute = lambda x: cmds.polyCube()

would make button to create a cube, but you could split the ‘execute’ functionality from the pure UI stuff by calling execute in preference to clicked. And obviously it could be more elaborate than a dumb lambda. I usually make handlers pass themselves in so it’s easy to write generic handlers that can tell which widget called them


#9

What do you mean by making handlers pass themselves in?

I took at look at your mgui stuff and the events module was exactly what I was looking for.

An events question:
I have a scrollList that has a selectionChanged event. I also have a method that clears said scrollList. I consider clearing a scrollList to be a selection change (assuming something was selected), so I’m inclined to add a selectionChanged event trigger when the list is cleared. The thing is sometimes I want to clear the list, add new items, and then select one of them, but this would trigger two selectionChanged events (and each would pass a different selected-element list). Should I split those events into two separate ones (selectionChanged and listCleared), or add an option to the clearList method that will suppress the event, or avoid calling that clearList method and clear it more directly? Is there another way to handle that I’m not thinking of?


#10

I realize I should have said “make the widgets pass themselves in” – when I bind a default command to the widget’s event, I always pass the calling widget to the command so that the handlers don’t need any special info to figure out which widget triggered the handler. I can ignore the info if needed, but i’s usually handy and it saves a lot of state management.

As for event splitting:

The tricky bit is that in vanilla maya GUI you only get events that Maya gives you – if Maya wants to fire the event it will, and the handler won’t know why (and conversely if Maya doesn’t fire it you won’t know that either: I constantly get pissed that there’s no KeyDown event in maya.cmds widgets, although to date I have resisted the temptation to go around cmds and get the underlying widget to try to find the QT event directly… that way lies madness).

On the other hand there is nothing to stop you from creating your own events and firing them when you want to. It’s pretty good practice to publish the events you want to support and then let the consumers decide what to do. Here’s a pseudocode example of the way I’d do it in mGui:


class MyCustomList(TextScrollList):

     def __init__(self, key, *args, **kwargs):
          super(MyCustomList, self).init(key, *args, **kwargs):
          self.ListCleared = Event()
          # etc.....

     def clear(self):
          self.removeAll = True  # mGui for cmds.textScrollList(self.widget, e=True, ra=True)
          self.ListCleaered()

# and somewhere else:
     example = Window(None)
     col = ColumnLayout(None)
     the_list = MyCustomList(None)

     def when_list_is_cleared (*args, **kwargs):
           # in mGUi all events fire with a keyword that
           # id's the object which fired the callback...
           print kwargs['sender'], "was cleared"


     the_list.ListCleared += when_list_is_cleared


I do something like this in https://github.com/theodox/mGui/blob/master/mGui/observable.py where I have some classes that are basically just lists which emit events when items are added, removed, sorted or cleared. Since the handlers all have the same signature, some callers might subscribe to both SelectionChanged and ListCleared and do different things, while other callers could attach to only one of them, or you could attach the same handler to both events.

Since the event is a persistent object, you could easily add a flag to suppress the event temporarily – so if clearing the list fired an event you didn’t want you could do something like


self.SelectionChange.enabled = False
self.removeAll = True
self.SellctionChange.enabled = True

and nobody would ever receive the SelectionChanged event. It’d probably be a good idea to make that a context manager so you ddin’t inadvertently turn the event off and forget it… mGui.Events does not currently do this, but it would be trivial to add