3
Contains the undo engine allowing to adjust the scene with api commands while
4
providing full undo and redo support.
8
- modify dag or dg using the undo-enabled DG and DAG modifiers
9
- modify values using Nodes and their plugs (as the plugs are overridden
10
to store undo information)
15
- You cannot mix mel and API proprely unless you use an MDGModifier.commandToExecute
17
- Calling operations that flush the undo queue from within an undoable method
18
causes the internal python undo stack not to be flushed, leaving dangling objects
19
that might crash maya once they are undon.
21
- WORKAROUND: Mark these methods with @notundoable and assure they are not
22
called by an undoable method
24
- calling MFn methods on a node usually means that undo is not supported for it.
28
To globally disable the undo queue using cmds.undo will disable tracking of opeartions, but will
29
still call the mel command.
31
Disable the 'undoable' decorator effectively remove the surrounding mel script calls
32
by setting the ``MRV_UNDO_ENABLED`` environment variable to 0 (default 1).
33
Additionally it will turn off the maya undo queue as a convenience.
35
If the mrv undo queue is disabled, MPlugs will not store undo information anymore
36
and do not incur any overhead.
38
Implementing an undoable method
39
-------------------------------
40
- decorate with @undoable
41
- minimize probability that your operation will fail before creating an operation (for efficiency)
42
- only use operation's doIt() method to apply your changes
43
- if you raise, you should not have created an undo operation
45
__docformat__ = "restructuredtext"
50
__all__ = ("undoable", "forceundoable", "notundoable", "MuteUndo", "StartUndo", "endUndo", "undoAndClear",
51
"UndoRecorder", "Operation", "GenericOperation", "GenericOperationStack", "DGModifier",
54
_undo_enabled_envvar = "MRV_UNDO_ENABLED"
55
_should_initialize_plugin = int(os.environ.get(_undo_enabled_envvar, True))
60
""" Assure our plugin is loaded - called during module intialization
62
:note: will only load the plugin if the undo system is not disabled"""
63
pluginpath = os.path.splitext(__file__)[0] + ".py"
64
if _should_initialize_plugin and not cmds.pluginInfo(pluginpath, q=1, loaded=1):
65
cmds.loadPlugin(pluginpath)
67
# assure our decorator is available !
69
setattr(__builtin__, 'undoable', undoable)
70
setattr(__builtin__, 'notundoable', notundoable)
71
setattr(__builtin__, 'forceundoable', forceundoable)
73
return _should_initialize_plugin
81
# when we are here, these have been imported already
82
import maya.OpenMaya as api
83
import maya.cmds as cmds
87
isUndoing = api.MGlobal.isUndoing
88
undoInfo = cmds.undoInfo
91
# Use sys as general placeholder that will only exist once !
92
# Global vars do not really maintain their values as modules get reinitialized
94
if not hasattr(sys, "_maya_stack_depth"):
95
sys._maya_stack_depth = 0
98
_maya_undo_enabled = int(os.environ.get(_undo_enabled_envvar, True))
100
if not _maya_undo_enabled:
103
# command - only generate code if we are to initialize undo
104
# mpx takes .3 s to load and we can just safe that time
105
if _should_initialize_plugin:
106
import maya.OpenMayaMPx as mpx
107
class UndoCmd(mpx.MPxCommand):
108
kCmdName = "storeAPIUndo"
112
mpx.MPxCommand.__init__(self)
113
self._operations = None
116
def doIt(self,argList):
117
"""Store out undo information on maya's undo stack"""
118
# if we reach the starting level, we can actually store the undo buffer
119
# and allow us to be placed on the undo queue
120
if sys._maya_stack_depth == 0:
121
self._operations = sys._maya_stack
122
sys._maya_stack = list() # clear the operations list
128
msg = "storeAPIUndo may only be called by the top-level function"
129
self.displayError(msg)
130
raise RuntimeError(msg)
133
"""Called on once a redo is requested"""
134
if not self._operations:
137
for op in self._operations:
141
"""Called once undo is requested"""
142
if not self._operations:
145
# run in reversed order !
146
for op in reversed(self._operations):
149
def isUndoable(self):
151
:return: True if we are undoable - it depends on the state of our
153
:note: This doesn't really seem to have an effect as we will always end
154
up on the undo-queue it seems"""
155
return self._operations is not None
157
# END command methods
161
return mpx.asMPxPtr(UndoCmd())
167
syntax = api.MSyntax()
169
# id - just for information and debugging
170
syntax.addFlag(UndoCmd.fId, "-callInfo", syntax.kString)
177
def initializePlugin(mobject):
178
mplugin = mpx.MFnPlugin(mobject)
179
mplugin.registerCommand(UndoCmd.kCmdName, UndoCmd.creator, UndoCmd.createSyntax)
181
# Uninitialize the script plug-in
182
def uninitializePlugin(mobject):
183
mplugin = mpx.MFnPlugin(mobject)
184
mplugin.deregisterCommand(UndoCmd.kCmdName)
185
# END if plugin should be initialized
193
"""Indicate that a new method level was reached"""
194
sys._maya_stack_depth += 1
196
def _decrStack(name = "unnamed"):
197
"""Indicate that a method level was exitted - and cause the
198
undo queue to be stored on the command if appropriate
199
We try to call the command only if needed"""
200
sys._maya_stack_depth -= 1
202
# store our stack on the undo queue
203
# Only store anything if we have something on the queue
204
if sys._maya_stack_depth == 0 and sys._maya_stack:
205
mel.eval("storeAPIUndo -id \""+name+"\"")
207
class MuteUndo(object):
208
"""Instantiate this class to disable the maya undo queue - on deletion, the
209
previous state will be restored
211
:note: useful if you want to save the undo overhead involved in an operation,
212
but assure that the previous state is always being reset"""
213
__slots__ = ("prevstate",)
215
self.prevstate = cmds.undoInfo(q=1, st=1)
216
cmds.undoInfo(swf = 0)
219
cmds.undoInfo(swf = self.prevstate)
222
class StartUndo(object):
223
"""Utility class that will push the undo stack on __init__ and pop it on __del__
225
:note: Prefer the undoable decorator over this one as they are easier to use and FASTER !
226
:note: use this class to assure that you pop undo when your method exists"""
228
def __init__(self, id = None):
240
"""Call before you start undoable operations
242
:note: prefer the @undoable decorator"""
246
"""Call before your function with undoable operations ends
248
:note: prefer the @undoable decorator"""
252
"""Undo all operations on the undo stack and clear it afterwards. The respective
253
undo command will do nothing once undo, but would undo all future operations.
254
The state of the undoqueue is well defined afterwards, but callers may stop functioning
255
if their changes have been undone.
257
:note: can be used if you need control over undo in very specific operations and in
258
a well defined context"""
259
operations = sys._maya_stack
260
sys._maya_stack = list()
262
# run in reversed order !
263
for op in reversed(operations):
267
class UndoRecorder(object):
268
"""Utility class allowing to undo and redo operations on the python command
269
stack so far to be undone and redone separately and independently of maya's
272
It can be used to define sections that need to be undone afterwards, for example
273
to reset a scene to its original state after it was prepared for export.
275
Use the `startRecording` method to record all future undoable operations
276
onto the stack. `stopRecording` will finalize the operation, allowing
277
the `undo` and `redo` methods to be used.
279
If you never call startRecording, the instance does not do anything.
280
If you call startRecording and stopRecording but do not call `undo`, it
281
will integrate itself transparently with the default undo queue.
283
:note: as opposed to `undoAndClear`, this utility may be used even if the
284
user is not at the very beginning of an undoable operation.
285
:note: If this utility is used incorrectly, the undo queue will be in an
286
inconsistent state which may crash maya or cause unexpected behaviour
287
:note: You may not interleave the start/stop recording areas of different
288
instances which could happen easily in recursive calls."""
289
__slots__ = ("_orig_stack", "_recorded_commands", "_undoable_helper", "_undo_called")
291
# prevents recursive access
292
_is_recording = False
293
_disable_undo = False
296
self._orig_stack = None
297
self._recorded_commands = None
298
self._undoable_helper = None
299
self._undo_called =False
304
except AssertionError:
305
# invalid isntances shouldn't bark
307
# END exception handling
311
def startRecording(self):
312
"""Start recording all future undoable commands onto this stack.
313
The previous stack will be safed and restored once this class gets destroyed
314
or once `stopRecording` gets called.
316
:note: this method may only be called once, subsequent calls have no effect
317
:note: This will forcibly enable the undo queue if required until
318
stopRecording is called."""
319
if self._orig_stack is not None:
322
if self._is_recording:
323
raise AssertionError("Another instance already started recording")
326
if not undoInfo(q=1, st=1):
327
self.__class__._disable_undo = True
329
# END undo info handling
331
self._undoable_helper = StartUndo() # assures we have a stack
333
self.__class__._is_recording = True
334
self._orig_stack = sys._maya_stack
335
sys._maya_stack = list() # will record future commands
337
# put ourselves on the previous undo queue which allows us to integrate
338
# with the original undo stack if that is required
339
self._orig_stack.append(self)
341
def stopRecording(self):
342
"""Stop recording of undoable comamnds and restore the previous command stack.
343
The instance is now ready to undo and redo the recorded commands
345
:note: this method may only be called once, subsequent calls have no effect"""
346
if self._recorded_commands is not None:
350
if not self._is_recording:
351
raise AssertionError("startRecording was not called")
353
if self._orig_stack is None:
354
raise AssertionError("startRecording was not called on this instance, but on another one")
356
self.__class__._is_recording = False
357
self._recorded_commands = sys._maya_stack
358
sys._maya_stack = self._orig_stack
360
# restore previous undo queue state
361
if self._disable_undo:
362
self.__class__._disable_undo = False
367
self._undoable_helper = None
368
# END assure we finish our undo
372
"""Undo all stored operations
374
:note: Must be called at the right time, otherwise the undo queue is in an
376
:note: If this method is never being called, the undo-stack will undo itself
377
as part of mayas undo queue, and thus behaves transparently
378
:raise AssertionError: if called before `stopRecording` as called"""
379
if self._recorded_commands is None:
380
raise AssertionError("Undo called before stopRecording")
382
for op in reversed(self._recorded_commands):
384
# END for each operation to undo
385
self._undo_called = True
388
"""Redo all stored operations after they have been undone
389
:raise AssertionError: if called before `stopRecording`"""
390
if self._recorded_commands is None:
391
raise AssertionError("Redo called before stopRecording")
393
for op in self._recorded_commands:
395
# END for each operation to redo
397
# this reverts the effect of the undo
398
self._undo_called = False
404
"""Called only if the user didn't call undo"""
405
if self._undo_called or not self._recorded_commands:
408
# we have not been called, and now the user hits redo on the whole
410
for op in self._recorded_commands:
412
# END for each operation
415
"""called only if the user didnt call undo"""
416
if self._undo_called or not self._recorded_commands:
419
# we have not be undone, hence we are part of the default undo queue
420
for op in reversed(self._recorded_commands):
422
# END for each operation
431
"""Decorator wrapping func so that it will start undo when it begins and end undo
432
when it ends. It assures that only toplevel undoable functions will actually produce
434
To mark a function undoable, decorate it:
440
:note: Using decorated functions appears to be only FASTER than implementing it
441
manually, thus using these is will greatly improve code readability
442
:note: if you use undoable functions, you should mark yourself undoable too - otherwise the
443
functions you call will create individual undo steps
444
:note: if the undo queue is disabled, the decorator does nothing"""
445
if not _maya_undo_enabled:
449
if hasattr(func, "__name__"):
452
def undoableDecoratorWrapFunc(*args, **kwargs):
453
"""This is the long version of the method as it is slightly faster than
454
simply using the StartUndo helper"""
457
return func(*args, **kwargs)
463
undoableDecoratorWrapFunc.__name__ = name
464
undoableDecoratorWrapFunc.__doc__ = func.__doc__
465
return undoableDecoratorWrapFunc
467
def forceundoable(func):
468
"""As undoable, but will enable the undo queue if it is currently disabled. It will
469
forcibly enable maya's undo queue.
471
:note: can only be employed reasonably if used in conjunction with `undoAndClear`
472
as it will restore the old state of the undoqueue afterwards, which might be off, thus
473
rendering attempts to undo impossible"""
474
undoable_func = undoable(func)
475
def forcedUndo(*args, **kwargs):
477
if not undoInfo(q=1, st=1):
480
# END undo info handling
482
return undoable_func(*args, **kwargs)
486
# END exception handling
487
# END forced undo function
489
if hasattr(func, "__name__"):
490
forcedUndo.__name__ = func.__name__
492
forcedUndo.__doc__ = func.__doc__
496
def notundoable(func):
497
"""Decorator wrapping a function into a muteUndo call, thus all undoable operations
498
called from this method will not enter the UndoRecorder and thus pollute it.
500
:note: use it if your method cannot support undo, butcalls undoable operations itself
501
:note: all functions using a notundoable should be notundoable themselves
502
:note: does nothing if the undo queue is globally disabled"""
503
if not _maya_undo_enabled:
506
def notundoableDecoratorWrapFunc(*args, **kwargs):
507
"""This is the long version of the method as it is slightly faster than
508
simply using the StartUndo helper"""
509
prevstate = undoInfo(q=1, st=1)
512
return func(*args, **kwargs)
514
undoInfo(swf = prevstate)
515
# END exception handling
518
if hasattr(func, "__name__"):
519
notundoableDecoratorWrapFunc.__name__ = func.__name__
521
notundoableDecoratorWrapFunc.__doc__ = func.__doc__
522
return notundoableDecoratorWrapFunc
529
class Operation(object):
530
"""Simple command class as base for all operations
531
All undoable/redoable operation must support it
533
:note: only operations may be placed on the undo stack !"""
537
"""Operations will always be placed on the undo queue if undo is available
538
This happens automatically upon creation
540
:note: assure subclasses call the superclass init !"""
541
if _maya_undo_enabled and not isUndoing() and undoInfo(q=1, st=1):
543
if sys._maya_stack_depth < 1:
544
raise AssertionError("Undo-Stack was %i, but must be at least 1 before operations can be put - check your code !" % sys._maya_stack_depth)
546
sys._maya_stack.append(self)
547
# END if not undoing and undo is enabled
549
"""Do whatever you do"""
550
raise NotImplementedError
553
"""Undo whatever you did"""
554
raise NotImplementedError
557
class GenericOperation(Operation):
558
"""Simple oeration allowing to use a generic doit and untoit call to be accessed
559
using the operation interface.
561
In other words: If you do not want to derive from operation just because you would like
562
to have your own custom ( but simple) do it and undo it methods, you would just
563
use this all-in-one operation"""
565
__slots__ = ( "_dofunc", "_doargs", "_dokwargs", "_doitfailed",
566
"_undofunc", "_undoargs", "_undokwargs")
569
"""intiialize our variables"""
570
Operation.__init__(self)
573
self._dokwargs = None
574
self._doitfailed = False # keep track whether we may actually undo something
576
self._undofunc = None
577
self._undoargs = None
578
self._undokwargs = None
580
def setDoitCmd(self, func, *args, **kwargs):
581
"""Add the doit call to our instance"""
584
self._dokwargs = kwargs
586
def setUndoitCmd(self, func, *args, **kwargs):
587
"""Add the undoit call to our instance"""
588
self._undofunc = func
589
self._undoargs = args
590
self._undokwargs = kwargs
593
"""Execute the doit command
595
:return: result of the doit command"""
597
return self._dofunc(*self._doargs, **self._dokwargs)
599
self._doitfailed = True
602
"""Execute undoit if doit did not fail"""
606
self._undofunc(*self._undoargs, **self._undokwargs)
610
class GenericOperationStack(Operation):
611
"""Operation able to undo generic callable commands (one or multiple). It would be used
612
whenever a simple generic operatino is not sufficient
614
In your api command, create a GenericOperationStack operation instance, add your (mel) commands
615
that should be executed in a row as Call. To apply them, call doIt once (and only once !).
616
You can have only one command stored, or many if they should be executed in a row.
617
The vital part is that with each do command, you supply an undo command.
618
This way your operations can be undone and redone once undo / redo is requested
620
:note: this class works well with `mrv.util.Call`
621
:note: to execute the calls added, you must call `doIt` or `addCmdAndCall` - otherwise
622
the undoqueue might brake if exceptions occour !
623
:note: your calls may use MEL commands safely as the undo-queue will be torn off during execution
624
:note: Undocommand will be applied in reversed order automatically"""
626
__slots__ = ("_docmds", "_undocmds", "_undocmds_tmp")
629
"""intiialize our variables"""
630
Operation.__init__(self)
631
self._docmds = list() # list of Calls
632
self._undocmds = list() # will store reversed list !
633
self._undocmds_tmp = list() # keeps undo until their do was verified !
637
"""Call all doIt commands stored in our instance after temporarily disabling the undo queue"""
638
prevstate = undoInfo(q=1, st=1)
642
if self._undocmds_tmp:
643
# verify each doit command before we shedule undo
644
# if it raies, we will not schedule the respective command for undo
645
for i,call in enumerate(self._docmds):
649
# forget about this and all following commands and reraise
650
del(self._docmds[i:])
651
self._undocmds_tmp = None # next time we only execute the cmds that worked (and will undo only them)
654
self._undocmds.insert(0, self._undocmds_tmp[i]) # push front
656
self._undocmds_tmp = None # free memory
658
for call in self._docmds:
660
# END for each do calll
661
# END if undo cmds have been verified
663
undoInfo(swf=prevstate)
666
"""Call all undoIt commands stored in our instance after temporarily disabling the undo queue"""
667
# NOTE: the undo list is already reversed !
668
prevstate = undoInfo(q=1, st=1)
673
if self._undocmds_tmp:
674
raise AssertionError("Tmp undo commands queue was not None on first undo call - this means doit has not been called before - check your code!")
676
for call in self._undocmds:
679
undoInfo(swf=prevstate)
681
def addCmd(self, doCall, undoCall):
682
"""Add a command to the queue for later application
684
:param doCall: instance supporting __call__ interface, called on doIt
685
:param undoCall: instance supporting __call__ interface, called on undoIt"""
687
self._docmds.append(doCall) # push
688
self._undocmds_tmp.append(undoCall)
690
def addCmdAndCall(self, doCall, undoCall):
691
"""Add commands to the queue and execute it right away - either always use
692
this way to add your commands or the `addCmd` method, never mix them !
694
:return: return value of the doCall
695
:note: use this method if you need the return value of the doCall right away"""
696
prevstate = undoInfo(q=1, st=1)
700
self._docmds.append(doCall)
701
self._undocmds.insert(0, undoCall)
703
undoInfo(swf=prevstate)
707
class DGModifier(Operation):
708
"""Undo-aware DG Modifier - using it will automatically put it onto the API undo queue
710
:note: You MUST call doIt() before once you have instantiated an instance, even though you
711
have nothing on it. This requiredment is related to the undo queue mechanism
712
:note: May NOT derive directly from dg modifier!"""
713
__slots__ = ("_modifier",)
714
_modifier_class_ = api.MDGModifier # do be overridden by subclasses
717
"""Initialize our base classes explicitly"""
718
Operation.__init__(self)
719
self._modifier = self._modifier_class_()
721
def __getattr__(self , attr):
722
"""Always return the attribute of the dg modifier - it is fully compatible
723
to our operation interface"""
724
return getattr(self._modifier, attr)
727
"""Override from Operation"""
728
return self._modifier.doIt()
731
"""Override from Operation"""
732
return self._modifier.undoIt()
735
class DagModifier(DGModifier):
736
"""undo-aware DAG modifier, copying all extra functions from DGModifier"""
738
_modifier_class_ = api.MDagModifier