view clients/editor/scripts/undomanager.py @ 256:e893afb4963b

* Fixed HistoryManager to work with linear undo * Set UndoManager to use branched mode by default
author cheesesucker@33b003aa-7bff-0310-803a-e67f0ece8222
date Mon, 08 Jun 2009 16:57:39 +0000
parents 51cc05d862f2
children 8b125ec749d7
line wrap: on
line source

from events.signal import Signal
import collections
import pdb

actionAdded = Signal(providing_args=["action"])
preUndo = Signal()
postUndo = Signal()
preRedo = Signal()
postRedo = Signal()
cleared = Signal()
modeChanged = Signal(providing_args=["mode"])
changed = Signal()

class UndoManager:
	"""
	The undo manager provides advanced undo functionality.
	
	Add actions with addAction. If you want to add a lot of actions and 
	group them, use startGroup and endGroup. When you undo a group, you will
	undo all the actions within it.
	
	If branched mode is enabled, you will not overwrite the redostack when
	adding an action. Instead, a new branch will be created with the new actions.
	To navigate in branches, you can use nextBranch and redoBranch.
	
	Example
	=======
	# Init undomanager
	undomanager = UndoManager()
	
	def doSomething():
		# Adds an action to the undomanager
		undocallback = lambda: doSomethingElse()
		redocallback = lambda: doSomething()
		description = "Something was done somewhere in the program."
		action = UndoObject(undocallback, redocallback, "Did something", description, "icon.png")
		undomanager.addAction(action)
	
	def doLotOfActions():
		# Starts an actiongroup and adds three actions
		undomanager.startGroup("Did lot of actions", "Lot of actions was done somewhere in the program")
		doSomething()
		doSomething()
		doSomething()
		undomanager.endGroup()
	
	# This will create an actiongroup with three actions, and undo it
	doLotOfActions()
	undomanager.undo()
	"""

	def __init__(self, branchedMode = True):
		self._groups = []
		self._branched_mode = branchedMode
		
		def warn(msg):
			print "Warning: ",msg
		self.first_item = UndoStackItem(UndoObject(None, None))
		self.first_item.object.name = "First item"
		self.first_item.object.description = "First item in stack. Placeholder"
		self.first_item.object.undoCallback = lambda: warn("Tried to undo first item")
		self.first_item.object.redoCallback = lambda: warn("Tried to redo first item")
		
		self.current_item = self.first_item

	def startGroup(self, name="", description="", icon=""):
		"""
		Starts an undogroup. Subsequent items will be added to the group
		until endGroup is called. Undogroups can be nested.
		
		name, description and icon are information that can be used by
		scripts which analyze the undostack.
		"""
		undogroup = UndoGroup(name, description, icon)
		self._groups.append(undogroup)
		return undogroup
	
	def endGroup(self):
		"""
		Ends the undogroup.
		"""
		if len(self._groups) <= 0:
			print "Warning: UndoManager: No groups to end!"
			return
			
		group = self._groups.pop()
		self.addAction(group)

	def addAction(self, action):
		""" 
		Adds an action to the stack.
		
		If the redostack is not empty and branchmode is enabed,
		a new branch will be created. If branchmod is disabled,
		the redo branch will be cleared.
		"""
		
		if len(self._groups) > 0:
			self._groups[len(self._groups)-1].addObject(action)
		else:
			stackitem = UndoStackItem(action)
			
			stackitem.previous = self.current_item
			if self._branched_mode:
				stackitem.previous.addBranch(stackitem)
			else:
				stackitem.previous.next = stackitem
			
			self.current_item = stackitem
			
			actionAdded.send(sender=self, action=action)
			changed.send(sender=self)

	def clear(self):
		"""
		Clears the undostack.
		"""
		self._groups = []
		self.first_item.clearBranches()
		self.current_item = self.first_item
		
		cleared.send(sender=self)
		changed.send(sender=self)

	# Linear undo
	def undo(self, amount=1): 
		""" Undo [amount] items """
		if amount <= 0:
			return
	
		preUndo.send(sender=self)
		for i in range(amount):
			if self.current_item == self.first_item:
				print "Warning: UndoManager: Tried to undo non-existing action."
				break
			
			self.current_item.object.undo()
			self.current_item = self.current_item.previous
			
		postUndo.send(sender=self)
		changed.send(sender=self)
			
			
	def redo(self, amount=1):
		""" Redo [amount] items. """
		if amount <= 0:
			return
		
		preRedo.send(sender=self)
		for i in range(amount):
			if self.current_item.next is None:
				print "Warning: UndoManager: Tried to redo non-existing action."
				break
				
			self.current_item = self.current_item.next
			self.current_item.object.redo()
				
		postRedo.send(sender=self)
		changed.send(sender=self)

	def getBranchMode(self): 
		""" Returns true if branch mode is enabled """
		return self._branched_mode
		
	def setBranchMode(self, enable):
		""" Enable or disable branch mode """
		self._branched_mode = enable
		changed.send(sender=self)

	def getBranches(self): 
		""" Returns branches from current stack item. """
		return self.current_item.getBranches()
	def nextBranch(self): 
		""" Switch to next branch in current item """
		self.current_item.nextBranch()
		changed.send(sender=self)
		
	def previousBranch(self):
		""" Switch previous branch in current item """
		self.current_item.previousBranch()
		changed.send(sender=self)

class UndoObject:
	""" UndoObject contains all the information that is needed to undo or redo an action,
		as well as representation of it.
		
		ATTRIBUTES
		==========
		- name: Name used by scripts analyzing the undostack to represent this item
		- description: Description of this item
		- icon: Icon used to represent this item
		- redoCallback: Function used to redo this item
		- undoCallback: Function used to undo
		
	"""
	def __init__(self, undoCallback, redoCallback, name="", description="", icon=""):
		self.name = name
		self.redoCallback = redoCallback
		self.undoCallback = undoCallback
		self.description = description
		self.icon = icon
		
		self.undone = False

	def undo(self): 
		""" Undoes the action. Do not use directly! """
		if self.undone is True:
			print "Tried to undo already undone action!"
			return
			
		self.undone = True
		self.undoCallback()
		
	def redo(self): 
		""" Redoes the action. Do not use directly! """
		if self.undone is False:
			print "Tried to redo already redone action!"
			return
			
		self.undone = False
		self.redoCallback()
		
class UndoGroup:
	""" 
	Contains a list of actions. Used to group actions together.
	
	Use UndoManager.startGroup and UndoManager.endGroup.
	
	"""
	def __init__(self, name="", description="", icon=""):
		self.name = name
		self.description = description
		self.icon = icon
		
		self.undoobjects = []
		
	def addObject(self, object):
		""" Adds an action to the list """
		self.undoobjects.append(object)
	
	def getObjects(self):
		""" Returns a list of the actions contained """
		return self.undoobjects
		
	def undo(self):
		""" Undoes all actions. """
		for action in reversed(self.undoobjects):
			action.undo()
		
	def redo(self):
		""" Redoes all actions. """
		for action in self.undoobjects:
			action.redo()

class UndoStackItem:
	""" Represents an action or actiongroup in the undostack. Do not use directly! """
	def __init__(self, object):
		self._branches = []
		self._currentbranch = -1
		
		self.parent = None
		self.object = object
		self.previous = None
		self.next = None
		
	def getBranches(self):
		""" Returns a list of the branches """
		return self._branches;
	
	def addBranch(self, item):
		""" Adds a branch to the list and sets this item to point to that branch """
		self._branches.append(item)
		
		self._currentbranch += 1
		self.next = self._branches[self._currentbranch]
		self.next.parent = self
		
	def nextBranch(self):
		""" Sets this item to point to next branch """
		if len(self._branches) <= 0:
			return
		self._currentbranch += 1
		if self._currentbranch >= len(self._branches):
			self._currentbranch = 0
		self.next = self._branches[self._currentbranch]
		changed.send(sender=self)
			
	def previousBranch(self):
		""" Sets this item to point to previous branch """
		if len(self._branches) <= 0:
			return
			
		self._currentbranch -= 1
		if self._currentbranch < 0:
			self._currentbranch = len(self._branches)-1
		self.next = self._branches[self._currentbranch]
		changed.send(sender=self)
		
	def setBranchIndex(self, index):
		""" Set this item to point to branches[index] """
		if index < 0 or index >= len(self._branches):
			return
		self._currentbranch = index
		changed.send(sender=self)
		self.next = self._branches[self._currentbranch]
		
	def clearBranches(self):
		""" Removes all branches """
		self._branches = []
		self._currentbranch = -1
		self._next = None
		changed.send(sender=self)
	
	def setBranch(self, branch):
		""" Set this item to point to branch. Returns True on success. """
		for b in range(len(self._branches)):
			if self._branches[b] == branch:
				self._currentbranch = b
				self.next = self._branches[self._currentbranch]
				changed.send(sender=self)
				return True
		else:
			print "Didn't find branch!"
			return False