I'm trying to implement undo/redo into something I'm working on, and after a bit of reading I've thrown this trial run together. I
think I'm following a loose approximation of command pattern? Not totally sure.
Essentially, every destructive thing the user can do is enacted by calling an ACTION__
class__
actionType function with the parameters to be used. The ACTION function handles calling the function proper and adding data to the history.
Every entry in the history corresponds to one action, and has a descriptive name, pointers to ACTION_UNDO/REDO functions (specific to the type of action performed), and a pointer to some memory where the parameters required for undoing/redoing have been placed.
ACTION_UNDO/REDO functions, when called, dig up those parameters from memory and call an appropriate function (going through the functions like this, rather than just setting values directly, should hopefully keep consistent any other elements indirectly affected by a given action).
The history is a simple stack (edit: well, sort of; you can move through it without popping things off - until you perform a new action, which loses all actions in your current position's "future"). When the total number of actions has been used (128 here), the array is rotated and the earliest actions kicked out to make room for new ones.
Possible issues
- Inflexibility: I imagine trying to overload or do much fancy with passing pointers or arrayptr's will break this.
- Bloat: You'll need a lot of probably redundant-seeming code to handle all possible actions.
- Logic: Complicated actions might resist easy implementation in this manner. You need to make sure you store the correct data when the action is performed so it can be succesfully reversed and recreated later.
- Fiddliness: Peeking and poking bytes.
This example: Use
Click textboxes to focus them, and type (alphanumeric, symbols, and backspace) to enter text.
Click the undo/redo buttons, or press ctrl+z/ctrl+y to undo/redo.
Actions are recorded in the console. Console is set-up to sit next to the program on a second monitor; comment out lines 7-8 to cancel that.
set display mode 1440, 900, 32, 1
sync on
sync rate 60
sync
open console 80, 30, 256
position window get console handle(), desktop width(), 0
maximize window get console handle()
`print to console
type t_textbox
str as string
x
y
focus
endtype
type t_history
name as string
undo as dword
redo as dword
param as dword
endtype
dim aTextbox(10) as t_textbox
global textbox_focus = 0
dim aHistory(128) as t_history
global history_last = 0
global history_current = 1
global keyBuffer as string = ""
for n = 1 to 10
if n = 1 then aTextbox(n).str = "Type something."
if n = 5 then aTextbox(n).str = "I am number 5 D:"
aTextbox(n).x = screen width() * 0.4
aTextbox(n).y = n * 60
next n
global mx, my, mc, mcOld
DO
// update input.
mx = mousex()
my = mousey()
mcOld = mc
mc = mouseclick()
keyBuffer = entry$()
clear entry buffer
if mcOld = 0 and mc = 1
textbox_focus = 0
// undo/redo button pressing.
if mx > 100 and my > 600 and mx < 150 and my < 650 then history__undoAction()
if mx > 150 and my > 600 and mx < 200 and my < 650 then history__redoAction()
// textbox focus switching.
for n = 1 to 10
if mx > aTextbox(n).x and my > aTextbox(n).y and mx < aTextbox(n).x + 384 and my < aTextbox(n).y + 32
textbox_focus = n
endif
next n
endif
// text entry.
if keyBuffer <> ""
for n = 1 to len(keyBuffer)
k$ = mid$(keyBuffer, n)
if asc(k$) >=32 and asc(k$) <= 171
// alphanumeric plus standard symbols.
if textbox_focus <> 0 then ACTION__textbox__addText(textbox_focus, k$)
else: if asc(k$) = 8
// backspace.
if textbox_focus <> 0 then ACTION__textbox__removeText(textbox_focus)
else: if asc(k$) = 26
// ctrl+z.
history__undoAction()
else: if asc(k$) = 25
// ctrl+y.
history__redoAction()
else
print asc(k$)
sync
wait key
endif:endif:endif:endif
next n
endif
// draw undo/redo buttons.
box outline 100, 600, 150, 650 : center text 125, 610, "Undo" : center text 125, 654, "(" + str$(history_current-1) + ")"
box outline 150, 600, 200, 650 : center text 175, 610, "Redo" : center text 175, 654, "(" + str$(history_last+1-history_current) + ")"
// draw textboxes.
for n = 1 to 10
box outline aTextbox(n).x, aTextbox(n).y, aTextbox(n).x + 384, aTextbox(n).y + 32, rgb(128, 128, 128)
if textbox_focus = n
box outline aTextbox(n).x - 1, aTextbox(n).y - 1, aTextbox(n).x + 385, aTextbox(n).y + 33, rgb(96, 128, 222)
box outline aTextbox(n).x - 2, aTextbox(n).y - 2, aTextbox(n).x + 386, aTextbox(n).y + 34, rgb(96, 128, 222)
x = aTextbox(n).x + 2 + text width(aTextbox(n).str)
if timer() %% 1000 < 500 then line x, aTextbox(n).y + 2, x, aTextbox(n).y + text height("A")
endif
text aTextbox(n).x + 2, aTextbox(n).y + 2, aTextbox(n).str
next n
print "history_last"
print history_last
print aHistory(history_last).name
print
print "history_current"
print history_current
print aHistory(history_current).name
SYNC
CLS 0
LOOP
// /////////////////////////////////////////////////////////////////////////////////////////////////////
// HISTORY ////////////////////////////////////////////////////////////////////////////////////
// /////////////////////////////////////////////////////////////////////////////////////////////////////
function history__addAction(name as string)
if history_current > history_last
if history_current = 128
// used up maximum undos; start dropping earliest undoes to make room.
rotate array aHistory(0)
else
inc history_last
inc history_current
endif
else
history_last = history_current
inc history_current
endif
aHistory(history_last).name = name
print console "HISTORY: ADD ACTION: " + name : print console
endfunction history_last
function history__undoAction()
if history_current = 1 then exitfunction
dec history_current
call function ptr aHistory(history_current).undo, aHistory(history_current).param
print console "HISTORY: UNDO ACTION: " + aHistory(history_current).name : print console
endfunction
function history__redoAction()
if history_current > history_last then exitfunction
call function ptr aHistory(history_current).redo, aHistory(history_current).param
print console "HISTORY: REDO ACTION: " + aHistory(history_current).name : print console
inc history_current
endfunction
// /////////////////////////////////////////////////////////////////////////////////////////////////////
// TEXTBOX ////////////////////////////////////////////////////////////////////////////////////
// /////////////////////////////////////////////////////////////////////////////////////////////////////
// TEXTBOX ACTIONS /////////////////////////////////////////////////////////////////////////////////////
// /////////////////////////////////////////////////////////////////////////////////////////////////////
// addText /////////////////////////////////////////////////////////////////////////////////////////////
function ACTION__textbox__addText(textbox, str as string)
local addr as dword
// try the action.
textbox__addText(textbox, str)
// add action to history.
action = history__addAction("Text edit - Added '" + str + "' (textbox " + str$(textbox) + ")")
// store parameters and func ptr in memory and return address to them.
// params: textbox (integer, 4 bytes) + str (one char string, 1 byte) = 5 bytes. Round up to 8.
addr = alloc(8)
poke integer addr, textbox
poke string addr + 4, str
aHistory(action).undo = get ptr to function("ACTION_UNDO__textbox__addText")
aHistory(action).redo = get ptr to function("ACTION_REDO__textbox__addText")
aHistory(action).param = addr
endfunction
function ACTION_UNDO__textbox__addText(param as dword)
local textbox
textbox = peek integer(param)
// undo by removing the text.
textbox__removeText(textbox)
endfunction
function ACTION_REDO__textbox__addText(param as dword)
local textbox, str as string
textbox = peek integer(param)
str = peek string(param + 4)
// redo by re-adding text.
textbox__addText(textbox, str)
endfunction
// removeText //////////////////////////////////////////////////////////////////////////////////////////
function ACTION__textbox__removeText(textbox)
local addr as dword
local str as string
// try the action.
str = textbox__removeText(textbox)
if str = "" then exitfunction
// add action to history.
action = history__addAction("Text edit - Removed '" + str + "' (textbox " + str$(textbox) + ")")
// store parameters and func ptr in memory and return address to them.
// params: textbox (integer, 4 bytes) + str (one char string, 1 byte) = 5 bytes. Round up to 8.
addr = alloc(8)
poke integer addr, textbox
poke string addr + 4, str
aHistory(action).undo = get ptr to function("ACTION_UNDO__textbox__removeText")
aHistory(action).redo = get ptr to function("ACTION_REDO__textbox__removeText")
aHistory(action).param = addr
endfunction
function ACTION_UNDO__textbox__removeText(param as dword)
local textbox, str as string
textbox = peek integer(param)
str = peek string(param + 4)
// undo by re-adding the text.
textbox__addText(textbox, str)
endfunction
function ACTION_REDO__textbox__removeText(param as dword)
local textbox
textbox = peek integer(param)
// redo by re-removing the text.
textbox__removeText(textbox)
endfunction
// TEXTBOX METHODS /////////////////////////////////////////////////////////////////////////////////////
// /////////////////////////////////////////////////////////////////////////////////////////////////////
function textbox__addText(textbox, str as string)
// add text.
aTextbox(textbox).str = aTextbox(textbox).str + str
endfunction
function textbox__removeText(textbox)
local char as string
// get the char that will be removed.
char = right$(aTextbox(textbox).str, 1)
// remove text.
aTextbox(textbox).str = left$(aTextbox(textbox).str, len(aTextbox(textbox).str) - 1)
endfunction char
Any feedback much appreciated