Undo/redo buffer
Author: Steffen Ploetz
Introduction
An undo/redo function is a "must" in a modern application. If you plan this function from the outset, it means almost no additional effort, but helps a lot in structuring your code.
There are four components to consider:
-
1 The "generic" action, which is an atomic operation that should be ready for undo/redo.
-
2 The undo/redo buffer, which manages the actions.
-
3 Your specific actions, which implement application-specific functionality.
-
4 The use of the undo/redo buffer in your application.
The "generic" action
The "generic" action is the base class for all application-specific actions that are to be supported by the undo/redo functionality. It also provides the user-friendly display name for the undo/redo action. Some applications offer more than undo/redo buttons and require a display name.
The undo/redo buffer
The undo/redo buffer manages the actions. The typical procedure is as follows:
-
The application creates an (application-specific) action.
-
The application calls the
Do()
method of the action.
-
The application registers the action to the undo/redo buffer.
The undo/redo buffer then manages the action completely independently and executes an undo or a redo if necessary.
The application-specific actions
The application-specific actions are derived from the "generic" action. They inherit the user-friendly display name and provide the state before the action (equal to the state after the undo-action) and the state after the action as well as the application-specific functionality.
The use of the undo/redo buffer
The use of the undo/redo buffer includes the provision of the buffer and the calling of its methods.
Code sample
The "generic" action code sample
The method
_SetName()
is hidden (by the preceding underline). It can be called anywhere in the code if you know its name, but is not actively offered. We want to use this method in derived classes only. Since
Gambas does not offer
protected
methods, this is a tolerable compromise.
' Gambas class file
Export
'' Provides the user-friendly display name of this action.
Property Read Name As String
'' Provides the flag, determinimg whether the application of this action needs a redraw.
Property Read NeedsRedraw As Boolean
' Stores the user-friendly display name of this action.
Private $sName As String
'' Creates a new instance of this class.
''
'' The **nameValue** defines the name of the action. It might be displayed.
'' The **needsRedrawValue** defines whether the application of this action needs a redraw.
Public Sub _new(Optional nameValue As String = "??", Optional needsRedrawValue As Boolean = False)
$sName = "??"
$bNeedsRedraw = needsRedrawValue
End
' Stores the flag, determinimg whether the application of this action needs a redraw.
Private $bNeedsRedraw As Boolean
'' Destroys the current instance of this class.
Public Sub _free()
$sName = Null
End
' This method implements a property.
Private Function Name_Read() As String
Return $sName
End
' This method implements a property.
Private Function NeedsRedraw_Read() As Boolean
Return $bNeedsRedraw
End
' This method is hidden.
Public Sub _SetName(nameValue As String)
$sName = nameValue
End
'' This is to be overridden by any derived class.
Public Sub Do()
' Intentionally left blank.
End
'' This is to be overridden by any derived class.
Public Sub Undo()
' Intentionally left blank.
End
The undo/redo buffer code sample
There are implementations that work with separate lists for
Undo()
and
Redo()
. This implementation manages both with one list and uses the index
$iCurrentUndo
to differentiate between
Undo()
and
Redo()
.
Only
Undo()
can be executed for actions from the start of the buffer up to and including the index
$iCurrentUndo
. Only
Redo()
can be executed for all actions after the index
$iCurrentUndo
.
If a new action is registered into the buffer, it is registered at the index after
$iCurrentUndo
and all actions after the index
$iCurrentUndo
must be discarded.
The
CanUndo()
and
CanRedo()
methods can be used to activate or deactivate the
Undo and
Redo buttons in the GUI, depending on whether the buffer with the current index
$iCurrentUndo
supports none, one or both functions.
' Gambas class file
Export
'' Buffer the actions. Any object, derived from **UndoRedoAction** is fine.
Private $aActions As UndoRedoAction[]
'' Store the usage of the undo-redo-buffer.
Private $iCount As Integer
'' Store the index of the current action.
Private $iCurrentUndo As Integer
'' Gets the number of actions, currently stored in the undo-redo-buffer.
Property Read Count As Integer
'' Constructs a new undo-redo-buffer.
Public Sub _new()
$aActions = New UndoRedoAction[8]
$iCount = 0
$iCurrentUndo = -1
End
'' Released this undo-redo-buffer.
Public Sub _free()
For index As Integer = 0 To $iCount - 1
$aActions[index] = Null
Next
$aActions = Null
End
'' Counts the number of actions, registered to the buffer.
Private Function Count_Read() As Integer
Return $iCount
End
'' Adds a new action to the buffer (it's Do() method is not called here).
'' Action will be added after the last undo action in the buffer.
'' Redo actions after the new action are removed.
''
'' The **action** represents an atomic operation that can be undone / redone.
Public Sub AddUndoAction(action As UndoRedoAction)
If action = Null Then Return
If $iCount >= $aActions.Length Then
$aActions.Resize($aActions.Length + 4)
Endif
While $iCount > $iCurrentUndo + 1
$iCount = $iCount - 1
$aActions[$iCount] = Null
Wend
$iCount = $iCount + 1
$iCurrentUndo = $iCurrentUndo + 1
$aActions[$iCurrentUndo] = action
End
'' Checks whether the current position of undo-redo buffer
'' represents a valid undo action
Public Function CanUndo() As Boolean
Return ($iCurrentUndo >= 0) And ($iCurrentUndo < $iCount)
End
'' Checks whether the current position of undo-redo buffer
'' represents a valid (re-) do action
Public Function CanRedo() As Boolean
Return ($iCurrentUndo < $iCount - 1)
End
'' Executes the undo action at the current position of undo-redo buffer
'' and moves the current position one step back
Public Sub Undo()
If CanUndo() <> True Then Return
$aActions[$iCurrentUndo].Undo()
$iCurrentUndo = $iCurrentUndo - 1
End
'' Executes the (re-) do action at the current position of undo-redo buffer
'' and moves the current position one step forward
Public Sub Redo()
If CanRedo() <> True Then Return
$iCurrentUndo = $iCurrentUndo + 1
$aActions[$iCurrentUndo].Do()
End
An application-specific action code sample
This application-specific action is very simple (for demonstration purposes) and sets the background color of a control element. It needs the control and the color states before and after the
Do()
action.
' Gambas class file
Inherits UndoRedoAction
' Store the object, to apply the action to.
Private $oControl As Control
' Store the state before the **Do()** action.
Private $iOriginalState As Integer
' Store the state after the **Do()** action.
Private $iNewState As Integer
'' Constructs a new action.
Public Sub _new(controlValue As Control, originalState As Integer, newState As Integer)
$oControl = controlValue
$iOriginalState = originalState
$iNewState = newState
Super._SetName("ControlSetColor(" & $oControl.Name & ")")
End
'' Executes the do action.
Public Sub Do()
If $oControl <> Null Then
$oControl.Background = $iNewState
Endif
End
'' Executes the undo action.
Public Sub Undo()
If $oControl <> Null Then
$oControl.Background = $iOriginalState
Endif
End
The use of the undo/redo buffer code sample
' Gambas class file
'' Provide the undo/redo buffer.
Private $oUndoRedoBuffer As UndoRedoBuffer = New UndoRedoBuffer
'' Help to create distinct actions.
Private $nCount As Integer = 0
'' Demonstrates how to create an action, call **Do()** and register the action to the undo/redo buffer.
Public Sub TestActions_Click()
Dim newColor As Integer
If $nCount % 4 = 0 Then
newColor = Color.SoftYellow
Else If $nCount % 4 = 1 Then
newColor = Color.SoftBlue
Else If $nCount % 4 = 2 Then
newColor = Color.Green
Else
newColor = Color.Red
Endif
Dim action As ControlSetColor_Action = New ControlSetColor_Action(Redo, Redo.Background, newColor)
$oUndoRedoBuffer.AddUndoAction(action)
action.Do()
$nCount = $nCount + 1
End
'' Demonstrates the **Redo** functionality.
Public Sub btnRedo_Click()
If $oUndoRedoBuffer.CanRedo Then
If $oUndoRedoBuffer.Redo() Then
Draw(ctrlPictureBox.Image)
ctrlPictureBox.Refresh
Endif
Endif
btnUndo.Enabled = $oUndoRedoBuffer.CanUndo
btnRedo.Enabled = $oUndoRedoBuffer.CanRedo
End
'' Demonstrates the **Undo** functionality.
Public Sub btnUndo_Click()
If $oUndoRedoBuffer.CanUndo Then
If $oUndoRedoBuffer.Undo() Then
Draw(ctrlPictureBox.Image)
ctrlPictureBox.Refresh
Endif
Endif
btnUndo.Enabled = $oUndoRedoBuffer.CanUndo
btnRedo.Enabled = $oUndoRedoBuffer.CanRedo
End
Page revisions
Undo/redo buffer by: Steffen Ploetz - March 28th, 2024
- updated: April 1st, 2024 by Steffen Ploetz
- Property "NeedsRedraw" added