You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

149 lines
6.2 KiB

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2022, 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 <http://www.gnu.org/licenses/>.
"""
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()