#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This application is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import logging; logger = logging.getLogger(__name__); logger.info("import") from contextlib import contextmanager class History(object): """Keep track of changes in the session. From file load to quit. All functions that need undo and redo follow the same idiom: A fuzzy and vague user function starts the process. It may depend on the context, like the cursor pitch or the current key signature. This function itself, e.g. Note.sharpen, saves its exact old values and instances and returns a lambda: undo function with these exact values already inserted. The undo function must be able to undo itself. Redo is undo for undo, it uses the same system. Each undo function needs to register an undo itself. In other words: The user function, like "sharpen", is only called once. After that an internal setter function, without context and state, handles undo and redo by using exact values on the exact instances (for example note instances). We still set and reset the cursor position in undo, but that doesn't mean getting the right context or track-state back. It is only to enable the user to edit from the same place again and also to find the right track to instruct the callbacks to update. The history does not know, and subsequently does not differentiate, between apply-to-selection or single-item commands. Each history function does a set of operations. The history itself is agnostic about them. """ def __init__(self): self.commandStack = [] # elements are (description, listOfFunctions). listOfFunctions is a real python list. self.redoStack = [] #same as self.commandStack self.doNotRegisterRightNow = False #prevents self.undo from going into an infinite loop. For example if you undo api.delete it will register insertItem again (which has a register itself with delete as undo function, which will...). There is only one history in the session so this attribute is shared by all undo functions. self.duringRedo = False self.apiCallback_historySequenceStarted = None self.apiCallback_historySequenceStopped = None @contextmanager def sequence(self, descriptionString): """Convenience function. All registrations from now on will be undone at once. call stopSequence when done. This is meant for high level scripts and not for user-interaction. This context can also be used to force a custom name for the undo description, even if it is only one step. """ self.apiCallback_historySequenceStarted() def sequenceRegister(registeredUndoFunction, descriptionString, listOfFunctions): """Modifies a list in place""" listOfFunctions.append(registeredUndoFunction) listOfFunctions = [] originalRegister = self.register self.register = lambda registeredUndoFunction, descriptionString, l=listOfFunctions: sequenceRegister(registeredUndoFunction, descriptionString, l) yield self._register(descriptionString, listOfFunctions) self.register = originalRegister self.apiCallback_historySequenceStopped() def register(self, registeredUndoFunction, descriptionString): """Register a single undo function but use the same syntax as many functions""" self._register(descriptionString, [registeredUndoFunction,]) def _register(self, descriptionString, listOfFunctions): assert type(listOfFunctions) is list, (type(listOfFunctions), listOfFunctions) if self.doNotRegisterRightNow: self.redoStack.append((descriptionString, listOfFunctions)) else: self.commandStack.append((descriptionString, listOfFunctions)) if not self.duringRedo: self.redoStack = [] #no redo after new commands. def clear(self): """User-scripts which do not implement their own undo must call this to not disturb the history. Better no history than a wrong history.""" self.commandStack = [] self.redoStack = [] def undo(self): if self.commandStack: self.doNotRegisterRightNow = True descriptionString, listOfFunctions = self.commandStack.pop() with self.sequence(descriptionString): for registeredUndoFunction in reversed(listOfFunctions): registeredUndoFunction() self.doNotRegisterRightNow = False #print ("pop", len(self.commandStack)) def redo(self): if self.redoStack: self.duringRedo = True descriptionString, listOfFunctions = self.redoStack.pop() with self.sequence(descriptionString): for registeredUndoFunction in reversed(listOfFunctions): registeredUndoFunction() self.duringRedo = False def asList(self): return [descr for descr, listOfFunctions in self.commandStack], [descr for descr, listOfFunctions in self.redoStack] def setterWithUndo(self, object, attribute, value, descriptionString, callback): oldValue = getattr(object, attribute) setattr(object, attribute, value) undoFunction = lambda oldValue=oldValue: self.setterWithUndo(object, attribute, oldValue, descriptionString, callback) self.register(undoFunction, descriptionString = descriptionString) callback()