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