We've Moved


The blog has been retired - it's up for legacy reasons, but these days I'm blogging at blog.theodox.com. All of the content from this site has been replicated there, and that's where all of the new content will be posted. The new feed is here

Sunday, February 23, 2014

Rescuing Maya GUI from itself

Update 4/11/2015: Fixed the code examples which were blown away in the current Blogger template, and also dead image links

Last time out was devoted to a subject most TA’s already know: the shortcomings of Maya’s native GUI. This time we’re going to start looking at ways to rescue Maya from itself.

And if you don’t know what that picture is there, go here first - this tech-art stuff is not as important as a good understanding of Thunderbirds!).

With that out of the way:

Any good rescue mission starts with objectives. The three main drawbacks to coding in Maya GUI natively are nasty syntax, clunky event handling, and difficult management. In today’s thrill-packed episode, we’re going lay some foundations for tackling that old-school syntax and dragging Maya GUI kicking and screaming into the 21st century.

Under the surface

Composing a Maya GUI in code is annoying because the only way to access the properties of a Maya GUI node is via a command - there’s no way to get at the properties directly without a lot of command mongering.

Sure, the purist might say that alternatives are just syntax sugar - but Maya GUI’s drawbacks are are (a) an obstacle to readability (and hence maintenance) and (b) such a big turn off that people don’t bother to learn what native GUI can do. This is particularly true for formLayouts, which are the most useful and powerful - and also the least handy and least user-friendly - way of layout of controls in Maya. All the power is no use if you just stick with columnLayouts and hand-typed pixel offsets because setting things up takes a whole paragraph’s worth of typing.

So, the first thing I’d like to ponder is how to cut out some of the crap. Not only will a decent wrapper be more pleasant to read and write - at some point in the future when we get to talk about styling controls, real property access will be a big help in keeping things tidy. Plus, by putting a wrapper around property access we’ll have a built in hook for management and cleaning up event handling as well, even though that’s a topic for a future post.

The upshot of it all: we’re stuck with the under-the-hood mechanism, but there’s no reason we can’t wrap it in something prettier. Consider this simple example:

import maya.cmds as cmds

class ExampleButton(object):
    CMD = cmds.button

    def __init__(self, *args, **kwargs):
        self.Widget = self.CMD (*args, **kwargs)

    @property
    def Label(self):
        return self.CMD(self.Widget, q=True, label=True)

example = cmds.window()
col = cmds.columnLayout()
btn = ExampleButton("hello world")
cmds.showWindow(example)
print btn.Label
# hello world

This is plain-vanilla Python properties in action. It’s easy to extend it so you can set ‘Label’ also:

    @Label.setter
    def Label(self, val):
        return self.CMD(self.Widget, e=True, label=val)

# add this to the example above:
btn.Label = "Goodbye cruel world"

Rescuing the rescuers

While this is a nice trick, it doesn’t take long to figure out that replacing the whole Maya GUI library with this will take a lot of annoying, repetitive, and typo-prone code. cmds.button alone has 34(!) properties to manage, and real offenders like rowLayout have a lot more. Writing wrappers for all of these is a huge waste of valuable human brainpower

Luckily, that’s not the end. Property objects are really instances of Python descriptors, which means they are classes. And since they are classes, we have some more options for creating them.

The official docs on descriptors are kind of opaque, but the link I shared above to Chris Beaumont’s article on properties and descriptors does a great job of explaining what they do: which is, in a nutshell, to provide property like services in the form of class-level objects. (Update: here’s great five minute video too). Instead of defining methods and decorating them as we did above, you create a class which handles the function-to-property behavior (both getting and setting) and stick it directly into your own class namespace, the same way you would place a def or a constant (as an aside, this placement is why the CMD field in the example is a class field rather than a hard code or an instance property - it makes it easy for the descriptor to call the right cmds function and flags. We could make a separate class for cmds.floatField, for example, swapping out only the class level CMD parameter, and it would ‘just work’ the same way).

The gotcha to bear in mind with descriptors is that they are separate objects that live in the class, not instance members You don’t create them inside your __init__, you declare them in the class namespace. They don’t belong to individual instances - that’s why in the example below you’ll notice that self refers to the descriptor itself, and not to the ExampleButton class (this is how each descriptor in the example below remembers how to format it’s own call to the maya command under the hood).

The “bad” part of that is that you the descriptor is ignorant of the class instance to which it is attached when you call it. Pyhton will pass the instance in to the descriptor, as you’ll see in the example below. The good part, on the other hand, is that the descriptor itself can (if need be) have a memory of its own - that’s why the descriptors in the next example can remember which flags to use when they call the underlying Maya GUI commands.

While this sounds scary, it’s mostly a minor mental adjustment - once you do a couple times it will be routine. And all the oddness is concentrated in the definition of the descriptor objects themselves - once the descriptor is actually declared, you access it just as if it were a conventional instance property and all is plain-jane foo.bar = baz.

Here’s the button example re-written with a couple of descriptors:

class CtlProperty (object):
    '''
    Property descriptor.  When applied to a Control-derived class, invokes the correct Maya command under the hood to get or set values
    '''

    def __init__(self, flag, cmd):
        assert callable(cmd), "cmd flag must be a maya command for editing gui objects"
        self.Flag = flag
        self.Command = cmd 

    def __get__(self, obj, objtype):
        '''
        Class instance <obj> and its type <objtype> are passed in automatically. 
        <self> is this descriptor object, NOT an owning class instance!
        '''
        ctrl = obj.Widget if hasattr(obj, "Widget") else str(obj)
        return self.Command(ctrl, **{'q':True, self.Flag:True})

    def __set__(self, obj, value):
        '''
        Again, the owning instance is passed in as <obj> automatically
        '''
        ctrl = obj.Widget if hasattr(obj, "Widget") else str(obj)
        self.Command(ctrl, **{'e':True, self.Flag:value})

class ExampleButton(object):
    CMD = cmds.button

    def __init__(self, *args, **kwargs):
        self.Widget = self.CMD (*args, **kwargs)

    Label = CtlProperty('label', CMD)
    BackgroundColor = CtlProperty('bgc', CMD)

# same example as before    
example = cmds.window()
col = cmds.columnLayout()
btn = ExampleButton("hello world")
cmds.showWindow(example)
btn.Label = "Thunderbirds are GO!"
btn.BackgroundColor = (.25, 1, .25)

That’s more like it - only two lines of data-driven code where we used to have six (well, not counting CtlProperty - but thats a one time cost to be spread out over scads of different GUI classes later). It’s a lot easier to read and understand as well, and contains far fewer opportunities for typos.

But… we’re still talking 34 lines like that for cmds.button, and God knows how many for cmds.rowColumLayout.

Sigh.

Act III

No rescue drama is complete without a false climax, and this is ours. Despite the ominous music just before the commercial,. the situation is not really that bad. The last example shows that the problem is not really one of code any more, it’s just data. Since descriptors are objects, you can crank them out like any other object: provide a list of the appropriate flags for a given class and you can crank out the correct descriptors, as they say, “automagically.”

As long as you promise not to use that stupid word around me.

Fortunately for our rescue team, Python treats classes the same way it treats anything else: as objects that can be created and maniuplated.

If you use the Python builtin type on any Python class, you’ll get back
type 'type'. In other words, a Python class definition is itself an instance of the class ‘type’. How… meta.

The reason this matters to us is that we can fabricate classes the same way fabricate other kinds of Python things. You would not hesitate to crank out a list of strings assembled in code: there’s no reason you can’t do the same thing for descriptors! You could do this by hand, creating type instances and filling them out yourself: types take three arguments: a string name, a list of parent types, and dictionary of named fields and propertis. Thus:

def constructor(self, name):
    self.Name = name

example = type('Example', (), {'__init__':constructor } )

Test = example("Hello world")
# <__main__.Example object at 0x00000000022D6198>
Test.Name
# Hello world

However this would send you down a possible rabbit hole, since the idea we’re really chasing is a way to mass produce classes to make UI coding easier and it would not be very easy if all of the classes had to be coded up in this clunky way. Luckily Python has an obscure but extremely powerful mechanism designed for just this sort of problem. Because, you know, it’s the language of geniuses.

“Brains, Activate the Metaclass”

The helpful MacGuffin in this case it the Metaclass. _Metaclasses have a reputation - not _entirely undeserved - as deep voodoo. The most commonly circulated quote about them is that “If you can solve the problem without a metaclass, you should.”

However, in our case we really can’t solve the problem without some form of class factory. In particular, we need a way to bang out classes with the right collection of Descriptors to cover all of the zillions of flags in the Maya GUI system. So just this once we can put on the big blue glasses and lab coats and venture into the super secret lair of the mad metaclass scientists.

The job of a metaclass is to customize the act of class creation. When a class is first defined, python will pass the type it creates (that same object we played with in the last example) to the metaclass for further manipulation. The __new__ function of the metaclass will be called on the just-defined type, taking it name, parents and internal dictionary as arguments. The __new__ can fiddle with any of these as it sees fit before passing it along for actual use.

As you can imagine, this is a good time for PythonMan’s Uncle Ben to remind us that ‘with great power comes great responsibility’ – it’s easy to shoot yourself in the foot with a metaclass, since you can make changes to the runtime versions of your classes that will not be represented in your source files. Don’t just run off and meta all over everything in sight. A minimalist approach is the best way to stay sane.

But you’d probably like to see what this really looks like in practice. Here’s an example.

class ControlMeta(type):
    '''
    Metaclass which creates CtlProperty  objects for maya gui proxies
    '''
    CONTROL_ATTRIBS = ['annotation', 'backgroundColor', 'defineTemplate', 
                'docTag', 'dragCallback', 'dropCallback', 'enable', 
                'enableBackground',  'exists', 'fullPathName', 'height',  
                'manage', 'noBackground',  'numberOfPopupMenus', 'parent', 
                'popupMenuArray', 'preventOverride', 'useTemplate', 'visible', 
                'visibleChangeCommand', 'width']

    def __new__(cls, name, parents, kwargs):
        '''
        __new__ is called then classes using this meta are defined.  It will add 
        all of the items in CONTROL_ATTRIBS to the new class definition as 
        CtlProperty descriptor objects using the CMD field (a maya.cmds command) 
        provied in the outer class.
        '''

        CMD = kwargs.get('CMD', None)

        if not kwargs.get('CMD'):
            CMD = parents[0].CMD

        for item in ControlMeta.CONTROL_ATTRIBS:
            kwargs[item] = CtlProperty(item, CMD)

        return super(ControlMeta, cls).__new__(cls, name, parents, kwargs)

The actual code is pretty simple. It takes the type object created by the ‘real’ class and grabs the contents of the CMD class field (remember that from the earlier examples?). Then it loops through its own list of command names and inserts them all into the new class as descriptors with the correct commands and the maya command that was stored in the command object. So our earlier button example becomes:

https://gist.githubusercontent.com/theodox/9106403/raw/60a06cba76748d7b157a5219349888f7b0bf0214/ButtonWithMeta.py
class MetaButton(object):
    CMD = cmds.button
    __metaclass__ = ControlMeta

    def __init__(self, *args, **kwargs):
        self.Widget = self.CMD (*args, **kwargs)


w = cmds.window()
c = cmds.columnLayout()
mb = MetaButton("button1")
cmds.showWindow(w)

print mb.exists  # We never had to add this one!
# True
print mb.visible  # or this
# True

There is a minor problem with this very truncated example, however: there’s no label or command in the the metaclass, so the MetaButton has no button specific properties - only the generic ones in our list (which I made by trolling the flags for cmds.control, the ‘base class’ of all Maya control commands).

This is easily fixed by adding properties that are specific to buttons to a class field, and tweaking the metaclass to read and use them the same way it already uses the CMD class field. Like CMD, these are good class-level attributes since the collection of flags is shared by all buttons, fields or whatever.

class ControlMeta(type):
    '''
    Metaclass which creates CtlProperty  objects for Control classes
    '''
    CONTROL_ATTRIBS = ['annotation', 'backgroundColor', 'defineTemplate', 'docTag', 
                        'dragCallback', 'dropCallback', 'enable', 'enableBackground', 
                        'exists', 'fullPathName', 'height',  'manage', 'noBackground', 
                        'numberOfPopupMenus', 'parent', 'popupMenuArray', 'preventOverride', 
                        'useTemplate', 'visible', 'visibleChangeCommand', 'width']


    def __new__(cls, name, parents, kwargs):

        CMD = kwargs.get('CMD', None)
        _ATTRIBS = kwargs.get('_ATTRIBS',[])  # unique props from outer class

        if not kwargs.get('CMD'):
            CMD = parents[0].CMD

        for item in ControlMeta.CONTROL_ATTRIBS:
            kwargs[item] = CtlProperty(item, CMD)

        for item in _ATTRIBS:   # now add in the outer class's unique props too
            kwargs[item] = CtlProperty(item, CMD)

        return super(ControlMeta, cls).__new__(cls, name, parents, kwargs)


class MetaButton(object):
    CMD = cmds.button
    _ATTRIBS = ['label', 'command']  # button specific props
    __metaclass__ = ControlMeta

    def __init__(self, *args, **kwargs):
        self.Widget = self.CMD (*args, **kwargs)

class MetaFloatField(object):
    CMD = cmds.floatField
    _ATTRIBS = ['editable','precision','value','maxValue','step',
                'minValue', 'changeCommand','dragCommand','enterCommand',
                'receiveFocusCommand']  # this one has a lot of properties

    __metaclass__ = ControlMeta

    def __init__(self, *args, **kwargs):
        self.Widget = self.CMD (*args, **kwargs)

As you can see, extending the automatic analysis is easy now that we know the basic trick. Just add a semi-private class field with the class specific attributes, and away we go!

In our next exciting episode…

I think this pretty much demonstrates that overhauling the Maya GUI toolkit is possible. However, in its current state it’s just a down-payment.

The combination of descriptors and metaclasses is an incredibly powerful tool and it’s not hard to see what comes next (it’s also easy to imagine similar setups for other problems which suffer from ugly imperative syntax). Now that we have a method for cranking out control widget classes by the bucketload, filling out the class library itself is pretty simple. There are, though, a few tricks we can use to make it better and less manual, as well as making sure it is complete. So, in a future outing, we’ll tackle a method for replicating the whole Maya command hierarchy in a more or less automatic way.

If you want to roll your own lightwieght properties library, this should give you enough tools to work with. If you’re more interested in actually doing GUI work without all the cmds crap, you should check out mGui, which is a library based on exactly this metaclass strategy to make GUI code more declarative and less ugly.

In the mean time,as we say at International Rescue Headquarters: F.A.B!

Thursday, February 20, 2014

Some new research in the academic page

Still cleaning up after the long blog silence (teaching that Unity class was fun -- but six games in five weeks plus a day job is murder!). So I should point out a couple of additions to the Interesting Research page.

Sunday, February 16, 2014

Pity for the outcast

I don't think it's too far over the top to say that everybody hates Maya's internal GUI system. It combines a very 1990's selection of widgets with a very 1970's coding style: it's a  1970's/1990's Frankenstein monster as horrifying as Ashton Kutcher in That 70's Show.



Not surprisingly a lot of TA's feel like they have to turn to PyQT if they want to deliver polished tools with a modern UI.  Unfortunately this is also less than ideal - while PyQT is a very powerful framework, it's got a very distinctive idiom of its own to learn, and moreover its hard to distribute since it depends on C++ dlls.  If you want to share a tool across studios or with outsourcers on a variety of boxes, OS'es and Maya versions it can become a Dantesque journey into DLL hell.

Because we do a lot of work with outsourcers, I've been looking into ways to render native Maya GUI a little less irritating. A quick review of my own pain-points in Maya GUI development showed me three main problems with the existing system


Icky syntax

Default Maya GUI is purely imperative; while Maya creates an in-memory object for every widget you create, you can only interact through it with via commands: In the typical idiom you create a button:

cmds.button("big button");

but your can only interact with it by calling more commands:

mybutton = cmds.button("big button")
cmds.button(mybutton, e=True, w=128)
cmds.button(mybutton, e=True, label = 'Red button'))
cmds.button(mybutton, e=True, bgc = (1,.5, .5))

This gets old pretty fast.  It's particularly bad for GUI components like formLayout, which can easily require whopping big arguments like this:

cmds.formLayout( form, edit=True, attachForm=[(b1, 'top', 5), (b1, 'left', 5), (b2, 'left', 5), (b2, 'bottom', 5), (b2, 'right', 5), (column, 'top', 5), (column, 'right', 5) ], attachControl=[(b1, 'bottom', 5, b2), (column, 'bottom', 5, b2)], attachPosition=[(b1, 'right', 5, 75), (column, 'left', 0, 75)], attachNone=(b2, 'top') )

Which is, frankly, stupid.

Lousy event handling

Another big knock on Maya's native gui is it's lousy callback system. The python version is bolted on to the original, string based MEL version and confuses a lot of people with uncertain scoping and unclear syntax (Check out these threads on Tech Artists.Org and  StackOverflow for some examples of how people get lost) .

Even when default Maya GUI callbacks do fire, they don't usually indicated who fired them off. This means you need to capture any relevant information ahead of time and encode it into the callback. In lots of GUI systems, you could handle a raft of similar functions like this pseudocode:

button_codes = ['red', 'green', 'blue']
for code in button_codes:
    button(code, tag = code, handler = apply_color)

def apply_color(button):
   target.color = constants[button.tag]

In Maya, on the other hand, you need to compose the callbacks with the right context when you create them using a functools.partial or a closure.  This makes coding up anything like dynamic model-view-controller UI into a real chore, and forces you to interleave your layout architecture and your data model in clumsy ways.

Moreover, Maya GUI callbacks are single-function calls. Its common in other frameworks -- for example in C#  - to have multicast delegates which can trigger multiple actions from a single callback. This makes for cleaner, more general code since you can split UI-only functionality ('highlight this button') from important stuff ('delete this model').

No Hierarchy

It might not be strictly fair to say that Maya GUI has 'no hierarchy'; anybody who has ever mucked around with cmds.setParent or cmds.lsUI knows that the actual widgets are composed in a strict hierarchical tree; hence UI objects with names like
window1|formLayout57|formLayout58|frameLayout38|columnLayout5|button5
The problem is that Maya doesn't manage this for you - you are responsible for capturing the pathname of every UI widget you create if you ever want to access it again, which imposes a useless maintenance tax on otherwise simple code. Moreover the names aren't determninistic, thanks to Maya's rule against identical paths: that means you may want a button called 'Button', but it may decide to call itself 'Button1' or 'Button99' and there's nothing you can do about it. As if that weren't bad enough, the strongly imperative style of the Maya GUI code also requires manual management. If you declare a layout and start filling it up with widgets, you're also responsible deciding when a particular container is full. A missed cmds.setParent can easily screw up your visual layout or the ordering of a menus and it's possible to shoot yourself in the foot without any corresponding gain in power, productivity or even prettiness. This limitation is why Maya has to offer all those redundant command sets for multiple-column rows and grids. When a monstrosity like cmds.rowLayout(nc=5, cw5=(100,100,50,50,80), ct5 =("left", "left", "both","both","right")) is a win for code cleanliness and legibility you know something has gone horribly wrong.


So What?

All of that amounts to a long-winded way of restating the obvious: nobody likes coding in standard Maya GUI.  The question is, can something be done about it?

Actually, quite a bit..  Next time out I'll talk about some practical ways to make Maya GUI coding... well, let's not say "fun", lets run with another 90's retread:


For 'Windows 95' substitute 'Maya GUI' and you've got the idea (For the origins of this authentic 2400-baud era meme, click here.) 

Shameless plug time

The book I worked on is finally available for ordering on Amazon!



I'm pleased and of course also nervous.  If you like it, please be sure to leave a positive review on Amazon - I'm told it makes a big difference.  I don't actually make any money from this one (such is modern publishing, alas) but I do have another book under way and it'll be much easier to get that published if this one does well and is well reviewed.