1
2 """
3 Contains the undo engine allowing to adjust the scene with api commands while
4 providing full undo and redo support.
5
6 Features
7 --------
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)
11
12 Limitations
13 -----------
14
15 - You cannot mix mel and API proprely unless you use an MDGModifier.commandToExecute
16
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.
20
21 - WORKAROUND: Mark these methods with @notundoable and assure they are not
22 called by an undoable method
23
24 - calling MFn methods on a node usually means that undo is not supported for it.
25
26 Configuration
27 -------------
28 To globally disable the undo queue using cmds.undo will disable tracking of opeartions, but will
29 still call the mel command.
30
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.
34
35 If the mrv undo queue is disabled, MPlugs will not store undo information anymore
36 and do not incur any overhead.
37
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
44 """
45 __docformat__ = "restructuredtext"
46
47 import sys
48 import os
49
50 __all__ = ("undoable", "forceundoable", "notundoable", "MuteUndo", "StartUndo", "endUndo", "undoAndClear",
51 "UndoRecorder", "Operation", "GenericOperation", "GenericOperationStack", "DGModifier",
52 "DagModifier")
53
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
61
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)
66
67
68 import __builtin__
69 setattr(__builtin__, 'undoable', undoable)
70 setattr(__builtin__, 'notundoable', notundoable)
71 setattr(__builtin__, 'forceundoable', forceundoable)
72
73 return _should_initialize_plugin
74
75
76
77
78
79
80
81
82 import maya.OpenMaya as api
83 import maya.cmds as cmds
84 import maya.mel as mel
85
86
87 isUndoing = api.MGlobal.isUndoing
88 undoInfo = cmds.undoInfo
89
90
91
92
93
94 if not hasattr(sys, "_maya_stack_depth"):
95 sys._maya_stack_depth = 0
96 sys._maya_stack = []
97
98 _maya_undo_enabled = int(os.environ.get(_undo_enabled_envvar, True))
99
100 if not _maya_undo_enabled:
101 undoInfo(swf=0)
102
103
104
105 if _should_initialize_plugin:
106 import maya.OpenMayaMPx as mpx
108 kCmdName = "storeAPIUndo"
109 fId = "-id"
110
112 mpx.MPxCommand.__init__(self)
113 self._operations = None
114
115
116 - def doIt(self,argList):
117 """Store out undo information on maya's undo stack"""
118
119
120 if sys._maya_stack_depth == 0:
121 self._operations = sys._maya_stack
122 sys._maya_stack = list()
123 return
124
125
126
127
128 msg = "storeAPIUndo may only be called by the top-level function"
129 self.displayError(msg)
130 raise RuntimeError(msg)
131
133 """Called on once a redo is requested"""
134 if not self._operations:
135 return
136
137 for op in self._operations:
138 op.doIt()
139
141 """Called once undo is requested"""
142 if not self._operations:
143 return
144
145
146 for op in reversed(self._operations):
147 op.undoIt()
148
150 """
151 :return: True if we are undoable - it depends on the state of our
152 undo stack
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
156
157
158
159 @staticmethod
162
163
164
165 @staticmethod
167 syntax = api.MSyntax()
168
169
170 syntax.addFlag(UndoCmd.fId, "-callInfo", syntax.kString)
171
172 syntax.enableEdit()
173
174 return syntax
175
180
183 mplugin = mpx.MFnPlugin(mobject)
184 mplugin.deregisterCommand(UndoCmd.kCmdName)
185
193 """Indicate that a new method level was reached"""
194 sys._maya_stack_depth += 1
195
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
201
202
203
204 if sys._maya_stack_depth == 0 and sys._maya_stack:
205 mel.eval("storeAPIUndo -id \""+name+"\"")
206
208 """Instantiate this class to disable the maya undo queue - on deletion, the
209 previous state will be restored
210
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",)
217
220
223 """Utility class that will push the undo stack on __init__ and pop it on __del__
224
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"""
227 __slots__ = ("id",)
231
237
240 """Call before you start undoable operations
241
242 :note: prefer the @undoable decorator"""
243 _incrStack()
244
246 """Call before your function with undoable operations ends
247
248 :note: prefer the @undoable decorator"""
249 _decrStack()
250
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.
256
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()
261
262
263 for op in reversed(operations):
264 op.undoIt()
265
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
270 undo queue.
271
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.
274
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.
278
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.
282
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")
290
291
292 _is_recording = False
293 _disable_undo = False
294
296 self._orig_stack = None
297 self._recorded_commands = None
298 self._undoable_helper = None
299 self._undo_called =False
300
307
308
309
310
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.
315
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:
320 return
321
322 if self._is_recording:
323 raise AssertionError("Another instance already started recording")
324
325
326 if not undoInfo(q=1, st=1):
327 self.__class__._disable_undo = True
328 cmds.undoInfo(swf=1)
329
330
331 self._undoable_helper = StartUndo()
332
333 self.__class__._is_recording = True
334 self._orig_stack = sys._maya_stack
335 sys._maya_stack = list()
336
337
338
339 self._orig_stack.append(self)
340
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
344
345 :note: this method may only be called once, subsequent calls have no effect"""
346 if self._recorded_commands is not None:
347 return
348
349 try:
350 if not self._is_recording:
351 raise AssertionError("startRecording was not called")
352
353 if self._orig_stack is None:
354 raise AssertionError("startRecording was not called on this instance, but on another one")
355
356 self.__class__._is_recording = False
357 self._recorded_commands = sys._maya_stack
358 sys._maya_stack = self._orig_stack
359
360
361 if self._disable_undo:
362 self.__class__._disable_undo = False
363 cmds.undoInfo(swf=0)
364
365 finally:
366
367 self._undoable_helper = None
368
369
370
372 """Undo all stored operations
373
374 :note: Must be called at the right time, otherwise the undo queue is in an
375 inconsistent state.
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")
381
382 for op in reversed(self._recorded_commands):
383 op.undoIt()
384
385 self._undo_called = True
386
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")
392
393 for op in self._recorded_commands:
394 op.doIt()
395
396
397
398 self._undo_called = False
399
400
401
402
404 """Called only if the user didn't call undo"""
405 if self._undo_called or not self._recorded_commands:
406 return
407
408
409
410 for op in self._recorded_commands:
411 op.doIt()
412
413
415 """called only if the user didnt call undo"""
416 if self._undo_called or not self._recorded_commands:
417 return
418
419
420 for op in reversed(self._recorded_commands):
421 op.undoIt()
422
423
424
425
426
427
428
429
430 -def undoable(func):
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
433 an undo event
434 To mark a function undoable, decorate it:
435
436 >>> @undoable
437 >>> def func():
438 >>> pass
439
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:
446 return func
447
448 name = "unnamed"
449 if hasattr(func, "__name__"):
450 name = func.__name__
451
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"""
455 _incrStack()
456 try:
457 return func(*args, **kwargs)
458 finally:
459 _decrStack(name)
460
461
462
463 undoableDecoratorWrapFunc.__name__ = name
464 undoableDecoratorWrapFunc.__doc__ = func.__doc__
465 return undoableDecoratorWrapFunc
466
468 """As undoable, but will enable the undo queue if it is currently disabled. It will
469 forcibly enable maya's undo queue.
470
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):
476 disable = False
477 if not undoInfo(q=1, st=1):
478 disable = True
479 undoInfo(swf=1)
480
481 try:
482 return undoable_func(*args, **kwargs)
483 finally:
484 if disable:
485 undoInfo(swf=0)
486
487
488
489 if hasattr(func, "__name__"):
490 forcedUndo.__name__ = func.__name__
491
492 forcedUndo.__doc__ = func.__doc__
493
494 return forcedUndo
495
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.
499
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:
504 return func
505
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)
510 undoInfo(swf = 0)
511 try:
512 return func(*args, **kwargs)
513 finally:
514 undoInfo(swf = prevstate)
515
516
517
518 if hasattr(func, "__name__"):
519 notundoableDecoratorWrapFunc.__name__ = func.__name__
520
521 notundoableDecoratorWrapFunc.__doc__ = func.__doc__
522 return notundoableDecoratorWrapFunc
523
530 """Simple command class as base for all operations
531 All undoable/redoable operation must support it
532
533 :note: only operations may be placed on the undo stack !"""
534 __slots__ = tuple()
535
537 """Operations will always be placed on the undo queue if undo is available
538 This happens automatically upon creation
539
540 :note: assure subclasses call the superclass init !"""
541 if _maya_undo_enabled and not isUndoing() and undoInfo(q=1, st=1):
542
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)
545
546 sys._maya_stack.append(self)
547
549 """Do whatever you do"""
550 raise NotImplementedError
551
553 """Undo whatever you did"""
554 raise NotImplementedError
555
558 """Simple oeration allowing to use a generic doit and untoit call to be accessed
559 using the operation interface.
560
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"""
564
565 __slots__ = ( "_dofunc", "_doargs", "_dokwargs", "_doitfailed",
566 "_undofunc", "_undoargs", "_undokwargs")
567
569 """intiialize our variables"""
570 Operation.__init__(self)
571 self._dofunc = None
572 self._doargs = None
573 self._dokwargs = None
574 self._doitfailed = False
575
576 self._undofunc = None
577 self._undoargs = None
578 self._undokwargs = None
579
581 """Add the doit call to our instance"""
582 self._dofunc = func
583 self._doargs = args
584 self._dokwargs = kwargs
585
587 """Add the undoit call to our instance"""
588 self._undofunc = func
589 self._undoargs = args
590 self._undokwargs = kwargs
591
593 """Execute the doit command
594
595 :return: result of the doit command"""
596 try:
597 return self._dofunc(*self._doargs, **self._dokwargs)
598 except:
599 self._doitfailed = True
600
602 """Execute undoit if doit did not fail"""
603 if self._doitfailed:
604 return
605
606 self._undofunc(*self._undoargs, **self._undokwargs)
607
611 """Operation able to undo generic callable commands (one or multiple). It would be used
612 whenever a simple generic operatino is not sufficient
613
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
619
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"""
625
626 __slots__ = ("_docmds", "_undocmds", "_undocmds_tmp")
627
629 """intiialize our variables"""
630 Operation.__init__(self)
631 self._docmds = list()
632 self._undocmds = list()
633 self._undocmds_tmp = list()
634
635
637 """Call all doIt commands stored in our instance after temporarily disabling the undo queue"""
638 prevstate = undoInfo(q=1, st=1)
639 undoInfo(swf=False)
640
641 try:
642 if self._undocmds_tmp:
643
644
645 for i,call in enumerate(self._docmds):
646 try:
647 call()
648 except:
649
650 del(self._docmds[i:])
651 self._undocmds_tmp = None
652 raise
653 else:
654 self._undocmds.insert(0, self._undocmds_tmp[i])
655
656 self._undocmds_tmp = None
657 else:
658 for call in self._docmds:
659 call()
660
661
662 finally:
663 undoInfo(swf=prevstate)
664
666 """Call all undoIt commands stored in our instance after temporarily disabling the undo queue"""
667
668 prevstate = undoInfo(q=1, st=1)
669 undoInfo(swf=False)
670
671
672 try:
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!")
675
676 for call in self._undocmds:
677 call()
678 finally:
679 undoInfo(swf=prevstate)
680
681 - def addCmd(self, doCall, undoCall):
682 """Add a command to the queue for later application
683
684 :param doCall: instance supporting __call__ interface, called on doIt
685 :param undoCall: instance supporting __call__ interface, called on undoIt"""
686
687 self._docmds.append(doCall)
688 self._undocmds_tmp.append(undoCall)
689
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 !
693
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)
697 undoInfo(swf=False)
698
699 rval = doCall()
700 self._docmds.append(doCall)
701 self._undocmds.insert(0, undoCall)
702
703 undoInfo(swf=prevstate)
704 return rval
705
708 """Undo-aware DG Modifier - using it will automatically put it onto the API undo queue
709
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
715
720
722 """Always return the attribute of the dg modifier - it is fully compatible
723 to our operation interface"""
724 return getattr(self._modifier, attr)
725
727 """Override from Operation"""
728 return self._modifier.doIt()
729
731 """Override from Operation"""
732 return self._modifier.undoIt()
733
736 """undo-aware DAG modifier, copying all extra functions from DGModifier"""
737 __slots__ = tuple()
738 _modifier_class_ = api.MDagModifier
739
740
741
742