Tyler Thornock
Technical Animator
Home Tutorials Tools Rigs About/Resume
Qt/Maya - Decorators

When it comes to PySide UIs in Maya, one big annoyance is how Undo is handled during things like clicking a button.  By default it will separate each undoable command like setAttr into a separate undo, so if you called it 3 times to set 3 attributes, you would need to undo/redo for each one individually.  I have ran several tools created by long term veterans at a studio or even bought from a 3rd party that you would have to undo like 50+ times to really get back to the original state, but no more!

A simple way to fix this issue, along with a few more handy tricks, is with custom decorators.  You may have scene the @ before above a function, something like:

    @property
    def stamp_type(self):
        # name of the current brush stamp
        return self._stamp_type

    @staticmethod
    def creator():
        return OpenMayaMPx.asMPxPtr(MyNode())

Although those cases are more of a descriptor that replace your function, you can also use this to "wrap" your function.  Wrapping essentially allows you to run code before your actual function is called, and also afterwards.  So for the above issue with undo, you can open and undo chunk, run your setAttrs or anything else that creates undos, and close the undo chunk so everything can be done in a single undo/redo.

import functools

def undo_chunk(func):
    """
    For maya, maya.cmds code called via a UI button/action signal like clicks are each treated as their own undo, this
    will chunk them all together.
    """
    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        # get a nicer name that is shown on undo/redo
        func_name = func.__name__ if hasattr(func, '__name__') else 'unnamed_function'

        # open the undo chunk
        cmds.undoInfo(chunkName=func_name, openChunk=True)
        try:
            # run the original function
            return func(*args, **kwargs)
        finally:
            # close the undo chunk
            cmds.undoInfo(closeChunk=True)
    return wrapped
    

Notice how the undo_chunk function is passed your function to func, which then creates a wrapper around it with functools and that will get called when you function gets called.  Also notice how now we have control of when the original function is really called, so we can open an undo chunk, run the function, then close the chunk (using a try/except to prevent leaving an open chunk).

Now all we have to do is use the @ to decorate the function called by a button in the UI.

    @m_decorators.undo_chunk
    def set_attributes_clicked(self):
        for x in range(3):
            transform = cmds.createNode('transform', skipSelect=True)
            cmds.setAttr(f'{transform}.translateX', x + 1)

So now when the set_attributes_button is clicked, it will run the wrapped version of that function, putting everything in one undo chunk!

This can be extremely handy and powerful.  You can do a simple "time elapsed" print by storing the time before and after your function call, you can do a more complex code profiling, you can restore selection, you can turn off autokey, or you can prevent reference edits.

One of my favorites for UIs, is one that captures an exception and shows a dialog instead of just printing the exception to the script editor that and artist or animator will never see and probably click the button multiple times breaking things further.