#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2020 , 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 not 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 ( )