ChainMap = css for Maya

Like all sane people, I hate writing Maya native gui. And like all sane people I also hate QT. Both are necessary but both are extra chatty apis which often smother you in implementation detail.

I hate JavaScript too (starting to see a pattern here…) but I do kind of envy the CSS + HTML ecosystem which does a better job of keeping the layout and structure of a UI separate the implementation. Especially in maya.cmds this is hard to do!

However, all of the gui calls in cmds accept the ** form of arguments – you can pass them a dictionary instead of manually specifying a flag, ie

      cmds.button(label = "label", width = 100, height = 40)

can be written as

     buttonstyle = {'width': 100, 'height': 40, 'label': 'label'}
     cmds.button(**buttonstyle)

this is particularly handy because you can mix flags and the dictionaries as long as you don’t overlap them, so

     buttonstyle = {'width': 100, 'height': 40} # remove 'label'
     cmds.button(label = "button1", **buttonstyle)
     cmds.button(label = "button2", **buttonstyle)

But the other nice thing about CSS is that it’s hierarchical – you can pass settings down from one CSS style to another. In the example above you’d have to do that manually. But modern Python includes the often-overlooked collections.ChainMap, which is basically just a way to layer dictionaries on top of each other. So you can do something like

     std_width = {'width': 72} 
     extra_wide = {'width': 100}
     std_height = {'height': 40}
     red = {'bgc': (1,0.2, 0.2)}

     red_button = ChainMap(std_width , std_height, red)

     cmds.button(label = "regular size red button", **red_button)

     big_red = ChainMap(extra_wide, red_button)
     cmds.button(label = "big red button", **big_red )

plus you can always make little factories to mass produce these:

      def  styled_control (cmd, style):
            def _factory(*args, **kwargs):
                  final_style = ChainMap(kwargs, style)
                  return  cmd(*args, **final_style)
            return _factory

      brb  =  styled_control(cmds.button, big_red)

      brb(label = 'a big red button')
      brb(label = 'another big red button')
      brb(label = 'super big red button', width = 300)  # overrides the style's flag

I find this a very handy way to take all off the fiddly little flags out of actual GUI calls and put them in a place where it’s easy to make adjustments.

8 Likes

Here’s a followup to the above:



@contextmanager
def layout(cmd, *styles, **kwargs):
    """
    Treat a layout as a context manager
    """
    final_style = ChainMap(styles, kwargs)
    result = cmd(**final_style)
    try:
        yield result
    finally:
        cmds.setParent("..")

That will let you treat things like columns or rows as context manager to make if visibly clear where things live in the layout.

If you’re willing to deal with formLayouts you can also add a variant of the above which handles all the formLayout attaches at the end of the context manager:

def form (layout_fn, *styles, **kwargs):
    final_style = ChainMap(styles, kwargs)
    result = cmds.formLayout(**final_style)
    try:
        yield result
    finally:
        cmds.setParent("..")
        layout_args = layout_fn(result)
        cmds.formLayout(result, e=True, **layout_args)
      

where the layout function produces the proper formLayout arguments (af = [....], ac = [....] etc)

3 Likes