Package mrv :: Package maya :: Module undo
[hide private]
[frames] | no frames]

Source Code for Module mrv.maya.undo

  1  # -*- coding: utf-8 -*- 
  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)) 
56 57 #{ Initialization 58 59 -def __initialize():
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 # assure our decorator is available ! 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 #} END initialization 78 79 80 #{ Undo Plugin 81 # when we are here, these have been imported already 82 import maya.OpenMaya as api 83 import maya.cmds as cmds 84 import maya.mel as mel 85 86 # cache 87 isUndoing = api.MGlobal.isUndoing 88 undoInfo = cmds.undoInfo 89 90 91 # Use sys as general placeholder that will only exist once ! 92 # Global vars do not really maintain their values as modules get reinitialized 93 # quite often it seems 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 # 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" 109 fId = "-id" 110
111 - def __init__(self):
112 mpx.MPxCommand.__init__(self) 113 self._operations = None
114 115 #{ Command Methods
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 123 return 124 # END if stack 0 125 126 127 # still here ? 128 msg = "storeAPIUndo may only be called by the top-level function" 129 self.displayError(msg) 130 raise RuntimeError(msg)
131
132 - def redoIt(self):
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
140 - def undoIt(self):
141 """Called once undo is requested""" 142 if not self._operations: 143 return 144 145 # run in reversed order ! 146 for op in reversed(self._operations): 147 op.undoIt()
148
149 - def isUndoable(self):
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 # END command methods 158 159 @staticmethod
160 - def creator():
161 return mpx.asMPxPtr(UndoCmd())
162 163 164 # Syntax creator 165 @staticmethod
166 - def createSyntax():
167 syntax = api.MSyntax() 168 169 # id - just for information and debugging 170 syntax.addFlag(UndoCmd.fId, "-callInfo", syntax.kString) 171 172 syntax.enableEdit() 173 174 return syntax
175
176 177 - def initializePlugin(mobject):
178 mplugin = mpx.MFnPlugin(mobject) 179 mplugin.registerCommand(UndoCmd.kCmdName, UndoCmd.creator, UndoCmd.createSyntax)
180
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
186 187 #} END plugin 188 189 190 #{ Utilities 191 192 -def _incrStack():
193 """Indicate that a new method level was reached""" 194 sys._maya_stack_depth += 1
195
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 201 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+"\"")
206
207 -class MuteUndo(object):
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",)
214 - def __init__(self):
215 self.prevstate = cmds.undoInfo(q=1, st=1) 216 cmds.undoInfo(swf = 0)
217
218 - def __del__(self):
219 cmds.undoInfo(swf = self.prevstate)
220
221 222 -class StartUndo(object):
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",)
228 - def __init__(self, id = None):
229 self.id = id 230 _incrStack()
231
232 - def __del__(self):
233 if self.id: 234 _decrStack(self.id) 235 else: 236 _decrStack()
237
238 239 -def startUndo():
240 """Call before you start undoable operations 241 242 :note: prefer the @undoable decorator""" 243 _incrStack()
244
245 -def endUndo():
246 """Call before your function with undoable operations ends 247 248 :note: prefer the @undoable decorator""" 249 _decrStack()
250
251 -def undoAndClear():
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 # run in reversed order ! 263 for op in reversed(operations): 264 op.undoIt()
265
266 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 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 # prevents recursive access 292 _is_recording = False 293 _disable_undo = False 294
295 - def __init__(self):
296 self._orig_stack = None 297 self._recorded_commands = None 298 self._undoable_helper = None 299 self._undo_called =False
300
301 - def __del__(self):
302 try: 303 self.stopRecording() 304 except AssertionError: 305 # invalid isntances shouldn't bark 306 pass
307 # END exception handling 308 309 #{ Interface 310
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. 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 # force undo enabled 326 if not undoInfo(q=1, st=1): 327 self.__class__._disable_undo = True 328 cmds.undoInfo(swf=1) 329 # END undo info handling 330 331 self._undoable_helper = StartUndo() # assures we have a stack 332 333 self.__class__._is_recording = True 334 self._orig_stack = sys._maya_stack 335 sys._maya_stack = list() # will record future commands 336 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)
340
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 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 # restore previous undo queue state 361 if self._disable_undo: 362 self.__class__._disable_undo = False 363 cmds.undoInfo(swf=0) 364 # END handle undo 365 finally: 366 # tigger deletion 367 self._undoable_helper = None
368 # END assure we finish our undo 369 370
371 - def undo(self):
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 # END for each operation to undo 385 self._undo_called = True
386
387 - def redo(self):
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 # END for each operation to redo 396 397 # this reverts the effect of the undo 398 self._undo_called = False
399 400 #} END interface 401 402 #{ Internal
403 - def doIt(self):
404 """Called only if the user didn't call undo""" 405 if self._undo_called or not self._recorded_commands: 406 return 407 408 # we have not been called, and now the user hits redo on the whole 409 # operation 410 for op in self._recorded_commands: 411 op.doIt()
412 # END for each operation 413
414 - def undoIt(self):
415 """called only if the user didnt call undo""" 416 if self._undo_called or not self._recorded_commands: 417 return 418 419 # we have not be undone, hence we are part of the default undo queue 420 for op in reversed(self._recorded_commands): 421 op.undoIt()
422 # END for each operation
423 #} END internal 424 425 #} END utilities 426 427 428 #{ Decorators 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 # END try finally 461 # END wrapFunc 462 463 undoableDecoratorWrapFunc.__name__ = name 464 undoableDecoratorWrapFunc.__doc__ = func.__doc__ 465 return undoableDecoratorWrapFunc 466
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. 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 # END undo info handling 481 try: 482 return undoable_func(*args, **kwargs) 483 finally: 484 if disable: 485 undoInfo(swf=0)
486 # END exception handling 487 # END forced undo function 488 489 if hasattr(func, "__name__"): 490 forcedUndo.__name__ = func.__name__ 491 # END assume name 492 forcedUndo.__doc__ = func.__doc__ 493 494 return forcedUndo 495
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. 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 # END exception handling 516 # END wrapFunc 517 518 if hasattr(func, "__name__"): 519 notundoableDecoratorWrapFunc.__name__ = func.__name__ 520 # END assume name 521 notundoableDecoratorWrapFunc.__doc__ = func.__doc__ 522 return notundoableDecoratorWrapFunc 523
524 #} END decorators 525 526 527 #{ Operations 528 529 -class Operation(object):
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
536 - def __init__(self):
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 # sanity check ! 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 # END sanity check 546 sys._maya_stack.append(self)
547 # END if not undoing and undo is enabled
548 - def doIt(self):
549 """Do whatever you do""" 550 raise NotImplementedError
551
552 - def undoIt(self):
553 """Undo whatever you did""" 554 raise NotImplementedError
555
556 557 -class GenericOperation(Operation):
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
568 - def __init__(self):
569 """intiialize our variables""" 570 Operation.__init__(self) 571 self._dofunc = None 572 self._doargs = None 573 self._dokwargs = None 574 self._doitfailed = False # keep track whether we may actually undo something 575 576 self._undofunc = None 577 self._undoargs = None 578 self._undokwargs = None
579
580 - def setDoitCmd(self, func, *args, **kwargs):
581 """Add the doit call to our instance""" 582 self._dofunc = func 583 self._doargs = args 584 self._dokwargs = kwargs
585
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
591
592 - def doIt(self):
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
601 - def undoIt(self):
602 """Execute undoit if doit did not fail""" 603 if self._doitfailed: 604 return 605 606 self._undofunc(*self._undoargs, **self._undokwargs)
607
608 609 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 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
628 - def __init__(self):
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 !
634 635
636 - def doIt(self):
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 # 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): 646 try: 647 call() 648 except: 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) 652 raise 653 else: 654 self._undocmds.insert(0, self._undocmds_tmp[i]) # push front 655 # END for each call 656 self._undocmds_tmp = None # free memory 657 else: 658 for call in self._docmds: 659 call() 660 # END for each do calll 661 # END if undo cmds have been verified 662 finally: 663 undoInfo(swf=prevstate)
664
665 - def undoIt(self):
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) 669 undoInfo(swf=False) 670 671 # sanity check 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) # push 688 self._undocmds_tmp.append(undoCall)
689
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 ! 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
706 707 -class DGModifier(Operation):
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 # do be overridden by subclasses 715
716 - def __init__(self):
717 """Initialize our base classes explicitly""" 718 Operation.__init__(self) 719 self._modifier = self._modifier_class_()
720
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)
725
726 - def doIt(self):
727 """Override from Operation""" 728 return self._modifier.doIt()
729
730 - def undoIt(self):
731 """Override from Operation""" 732 return self._modifier.undoIt()
733
734 735 -class DagModifier(DGModifier):
736 """undo-aware DAG modifier, copying all extra functions from DGModifier""" 737 __slots__ = tuple() 738 _modifier_class_ = api.MDagModifier
739 740 741 #} END operations 742