mrv.maya.undo
Covered: 453 lines
Missed: 40 lines
Skipped 251 lines
Percent: 91 %
  2
"""
  3
Contains the undo engine allowing to adjust the scene with api commands while
  4
providing full undo and redo support.
  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)
 12
Limitations
 13
-----------
 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.
 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.
 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
 44
"""
 45
__docformat__ = "restructuredtext"
 47
import sys
 48
import os
 50
__all__ = ("undoable", "forceundoable", "notundoable", "MuteUndo", "StartUndo", "endUndo", "undoAndClear", 
 51
           "UndoRecorder", "Operation", "GenericOperation", "GenericOperationStack", "DGModifier", 
 52
           "DagModifier")
 54
_undo_enabled_envvar = "MRV_UNDO_ENABLED"
 55
_should_initialize_plugin = int(os.environ.get(_undo_enabled_envvar, True))
 59
def __initialize():
 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)
 68
	import __builtin__
 69
	setattr(__builtin__, 'undoable', undoable)
 70
	setattr(__builtin__, 'notundoable', notundoable)
 71
	setattr(__builtin__, 'forceundoable', forceundoable)
 73
	return _should_initialize_plugin
 82
import maya.OpenMaya as api
 83
import maya.cmds as cmds
 84
import maya.mel as mel
 87
isUndoing = api.MGlobal.isUndoing
 88
undoInfo = cmds.undoInfo
 94
if not hasattr(sys, "_maya_stack_depth"):
 95
	sys._maya_stack_depth = 0
 96
	sys._maya_stack = []
 98
_maya_undo_enabled = int(os.environ.get(_undo_enabled_envvar, True))
100
if not _maya_undo_enabled:
101
	undoInfo(swf=0)
105
if _should_initialize_plugin:
106
	import maya.OpenMayaMPx as mpx
107
	class UndoCmd(mpx.MPxCommand):
108
		kCmdName = "storeAPIUndo"
109
		fId = "-id"
111
		def __init__(self):
112
			mpx.MPxCommand.__init__(self)
113
			self._operations = None
116
		def doIt(self,argList):
117
			"""Store out undo information on maya's undo stack"""
120
			if sys._maya_stack_depth == 0:
121
				self._operations = sys._maya_stack
122
				sys._maya_stack = list()					# clear the operations list
123
				return
128
			msg = "storeAPIUndo may only be called by the top-level function"
129
			self.displayError(msg)
130
			raise RuntimeError(msg)
132
		def redoIt(self):
133
			"""Called on once a redo is requested"""
134
			if not self._operations:
135
				return
137
			for op in self._operations:
138
				op.doIt()
140
		def undoIt(self):
141
			"""Called once undo is requested"""
142
			if not self._operations:
143
				return
146
			for op in reversed(self._operations):
147
				op.undoIt()
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
159
		@staticmethod
160
		def creator():
161
			return mpx.asMPxPtr(UndoCmd())
165
		@staticmethod
166
		def createSyntax():
167
			syntax = api.MSyntax()
170
			syntax.addFlag(UndoCmd.fId, "-callInfo", syntax.kString)
172
			syntax.enableEdit()
174
			return syntax
177
	def initializePlugin(mobject):
178
		mplugin = mpx.MFnPlugin(mobject)
179
		mplugin.registerCommand(UndoCmd.kCmdName, UndoCmd.creator, UndoCmd.createSyntax)
182
	def uninitializePlugin(mobject):
183
		mplugin = mpx.MFnPlugin(mobject)
184
		mplugin.deregisterCommand(UndoCmd.kCmdName)
192
def _incrStack():
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
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",)
214
	def __init__(self):
215
		self.prevstate = cmds.undoInfo(q=1, st=1)
216
		cmds.undoInfo(swf = 0)
218
	def __del__(self):
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"""
227
	__slots__ = ("id",)
228
	def __init__(self, id = None):
229
		self.id = id
230
		_incrStack()
232
	def __del__(self):
233
		if self.id:
234
			_decrStack(self.id)
235
		else:
236
			_decrStack()
239
def startUndo():
240
	"""Call before you start undoable operations
242
	:note: prefer the @undoable decorator"""
243
	_incrStack()
245
def endUndo():
246
	"""Call before your function with undoable operations ends
248
	:note: prefer the @undoable decorator"""
249
	_decrStack()
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.
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()
263
	for op in reversed(operations):
264
		op.undoIt()
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.
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")
292
	_is_recording = False
293
	_disable_undo = False 
295
	def __init__(self):
296
		self._orig_stack = None
297
		self._recorded_commands = None
298
		self._undoable_helper = None
299
		self._undo_called =False
301
	def __del__(self):
302
		try:
303
			self.stopRecording()
304
		except AssertionError:
306
			pass
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:
320
			return
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
328
			cmds.undoInfo(swf=1)
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 
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:
347
			return
349
		try:
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
361
			if self._disable_undo:
362
				self.__class__._disable_undo = False
363
				cmds.undoInfo(swf=0)
365
		finally:
367
			self._undoable_helper = None
371
	def undo(self):
372
		"""Undo all stored operations
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")
382
		for op in reversed(self._recorded_commands):
383
			op.undoIt()
385
		self._undo_called = True
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")
393
		for op in self._recorded_commands:
394
			op.doIt()
398
		self._undo_called = False
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
410
		for op in self._recorded_commands:
411
			op.doIt()
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
420
		for op in reversed(self._recorded_commands):
421
			op.undoIt()
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:
436
	>>> @undoable
437
	>>> def func():
438
	>>> 	pass
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
448
	name = "unnamed"
449
	if hasattr(func, "__name__"):
450
		name = 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"""
455
		_incrStack()
456
		try:
457
			return func(*args, **kwargs)
458
		finally:
459
			_decrStack(name)
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):
476
		disable = False
477
		if not undoInfo(q=1, st=1):
478
			disable = True
479
			undoInfo(swf=1)
481
		try:
482
			return undoable_func(*args, **kwargs)
483
		finally:
484
			if disable:
485
				undoInfo(swf=0)
489
	if hasattr(func, "__name__"):
490
		forcedUndo.__name__ = func.__name__
492
	forcedUndo.__doc__ = func.__doc__
494
	return forcedUndo
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:
504
		return func
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)
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 !"""
534
	__slots__ = tuple()
536
	def __init__(self):
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)
548
	def doIt(self):
549
		"""Do whatever you do"""
550
		raise NotImplementedError
552
	def undoIt(self):
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")
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
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"""
582
		self._dofunc = func
583
		self._doargs = args
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
592
	def doIt(self):
593
		"""Execute the doit command
595
		:return: result of the doit command"""
596
		try:
597
			return self._dofunc(*self._doargs, **self._dokwargs)
598
		except:
599
			self._doitfailed = True
601
	def undoIt(self):
602
		"""Execute undoit if doit did not fail"""
603
		if self._doitfailed:
604
			return
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")
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 !
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)
641
		try:
642
			if self._undocmds_tmp:
645
				for i,call in enumerate(self._docmds):
646
					try:
647
						call()
648
					except:
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
656
				self._undocmds_tmp = None			# free memory
657
			else:
658
				for call in self._docmds:
659
					call()
662
		finally:
663
			undoInfo(swf=prevstate)
665
	def undoIt(self):
666
		"""Call all undoIt commands stored in our instance after temporarily disabling the undo queue"""
668
		prevstate = undoInfo(q=1, st=1)
669
		undoInfo(swf=False)
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!")
676
			for call in self._undocmds:
677
				call()
678
		finally:
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)
697
		undoInfo(swf=False)
699
		rval = doCall()
700
		self._docmds.append(doCall)
701
		self._undocmds.insert(0, undoCall)
703
		undoInfo(swf=prevstate)
704
		return rval
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
716
	def __init__(self):
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)
726
	def doIt(self):
727
		"""Override from Operation"""
728
		return self._modifier.doIt()
730
	def undoIt(self):
731
		"""Override from Operation"""
732
		return self._modifier.undoIt()
735
class DagModifier(DGModifier):
736
	"""undo-aware DAG modifier, copying all extra functions from DGModifier"""
737
	__slots__ = tuple()
738
	_modifier_class_ = api.MDagModifier