One common thing needed when working with Maya’s API and even certain cases with Python (or both), is undo and redo support. Typically this means you would create a plugin that would handle your use case, and then repeat as needed for any more use cases that come up. This can cause people to either have a bunch of individual plugins or some sort of uber plugin.
However, this will show an example of how you can use a single plugin that can be used for all your use cases, and you just have to setup a class with the appropriate functions to call. No need to add a new plugin to load at startup, or make sure your plugin is loaded when your command is called.
import functools
from maya import cmds
from maya.api import OpenMaya
class RunUndoablePython(OpenMaya.MPxCommand):
call_class = None
kCommandName = 'runUndoablePython'
def __init__(self):
"""
Allows you to run cmds.runUndoablePython(self) during the __init__ of your custom python class, which will
cause maya to see it as an undoable command. Then this class will call into the doIt, redoIt, and undoIt of
your class. This helps prevent the need to create a bunch of random plug-ins for various operations.
"""
super(RunUndoablePython, self).__init__()
self.py_class = None
def isUndoable(self):
"""
Make sure we support undoing.
"""
return True
def doIt(self, args):
"""
Your doIt should do the bulk of the work, eg calculating values to apply during the redoIt/undoIt call. You
do not have to specify a doIt method if you already had the information on self.py_class and are ready to apply.
"""
self.py_class = self.call_class
if hasattr(self.py_class, 'doIt'):
result = self.py_class.doIt()
if result is not None:
self.setResult(result)
self.redoIt()
def redoIt(self):
"""
Just calls your class's required redoIt method. Although this can use setResult, normally this is not needed.
"""
result = self.py_class.redoIt()
if result is not None:
self.setResult(result)
def undoIt(self):
"""
Just calls your class's required undoIt.
"""
# the undo queue can get into a bad state, so this is normally disabled, might want to add an option to skip
state = cmds.undoInfo(query=True, state=True)
if state:
cmds.undoInfo(stateWithoutFlush=False)
try:
result = self.py_class.undoIt()
if result is not None:
self.setResult(result)
finally:
if state:
cmds.undoInfo(stateWithoutFlush=True)
@classmethod
def wrap_command(cls):
"""
In order to bypass the normal argument required for commands due to trying to be compatible with MEL, we have
to wrap the created cmds.runUndoablePython so the class can be passed.
"""
cmd_func = getattr(cmds, cls.kCommandName)
@functools.wraps(cmd_func)
def wrapped(py_class):
# make sure we store the class so it wont go out of scope and then call it
cls.call_class = py_class
cmds.undoInfo(openChunk=True, chunkName=cls.kCommandName)
try:
return cmd_func()
finally:
cmds.undoInfo(closeChunk=True)
# now overwrite the one in cmds with this new wrapped version
wrapped.__wrapped__ = cmd_func
setattr(cmds, cls.kCommandName, wrapped)
def maya_useNewAPI():
pass
def creator():
return RunUndoablePython()
def initializePlugin(m_object):
m_plugin = OpenMaya.MFnPlugin(m_object)
try:
# let maya create the command, and then replace it with a wrapped version that can accept a python class
m_plugin.registerCommand(RunUndoablePython.kCommandName, creator)
RunUndoablePython.wrap_command()
except:
raise Exception('Unable to initialize: {}'.format(RunUndoablePython.kCommandName))
def uninitializePlugin(m_object):
m_plugin = OpenMaya.MFnPlugin(m_object)
try:
m_plugin.deregisterCommand(RunUndoablePython.kCommandName)
except:
raise Exception('Unable to uninitialize: {}'.format(RunUndoablePython.kCommandName))
Now just load the plugin in Maya and lets create our first class so we can test how it works.
class ExampleClass(object):
def __init__(self):
super(ExampleClass, self).__init__()
self.redo_text = ''
self.undo_text = ''
# run the command that will call the doIt, redoIt, undoIt functions at the appropriate times
cmds.runUndoablePython(self)
def doIt(self):
# gather / store data to use during redo / undo, eg doIt is only called once, then redo/undo happen while
# moving backwards/forward the undo stack... optionally you can do this work in your __init__ before
# you call the cmd.runUndoablePython
print('Preparing World!')
self.redo_text = 'Hello World!'
self.undo_text = 'Goodbye World!'
def redoIt(self):
# apply your changes here, usually something like MPlug.setValue, MFkSkinCluster.setWeights, etc
print(self.redo_text)
def undoIt(self):
# undo the changes to restore the previous state
print(self.undo_text)
# initializing the class will also run the command
ExampleClass()
Notice how when you call the class the first time, it prints for the doIt and redoIt, but then only the redoIt and undoIt while you navigate through the undo queue. This is important because you do not want to do any heavy lifting calculations in the redoIt or undoIt. It should do the minimum work needed to apply the desired state. Also, you can skip having the doIt function and handle it in the __init__ before you call the cmd.runUndoablePython.
I have used this for all sorts of use cases while working with Maya’s API like setting skin weights, vertex colors, mesh points, etc, but also to manage keeping a custom data storage class in sync during the use of an Artisan Painting tool.