The last major thing left to do on my editor was the undo / redo typing feature that we find in most editors.
Thought this would be easy as there is a dll file to assist with this, however it turns out there are several versions of this plus a different one depending on the o/s version you are running - and that's messy!
Also, what undo / redo functions I could get working weren't working the way I wanted them to.
All this meant I was looking a possible banana skin, do I decided to have a go myself and I will explain what I have found...
1. All my research on this states that the best way to do undo / redo is to have two stacks (arrays).
2. When something is typed it is added to the "undo" stack.
3. When you press a designated key (here I chose F1 for undo), the redo stack is incremented by 1 and the undo details are copied to it.
4. The undo stack is then decremented by 1.
5. When you press another designated key (here I chose F2 for redo), the process is reversed.
So here is a sample of working code (and by the way I have added extra fields as I might need these details later)....
Let's start with two user defined variables:
type Undos2
int Pos[1000]
int LineNo[1000]
int Offset[1000]
string Typed[1000]
string Was[1000]
int Press[1000]
endtype
def Undo2 as Undos2
type Redos2
int Pos[1000]
int LineNo[1000]
int Offset[1000]
string Typed[1000]
string Was[1000]
int Press[1000]
endtype
def Redo2 as Redos2
And two counter variables...
int Undo2Count = 0
int Redo2Count = 0
So now we are all setup to start once we have sub classed our edit control (because we need to detect certain things).
In our sub classed edit handler we need to record what has been typed...
case WM_CHAR
if wParam <> 8 'Not a back space key press
CONTROLCMD w1,1,@EDGETSELECTION,varStart2,varEnd2
Undo2Count ++
Undo2.Press[Undo2Count] = 1
Undo2.Typed[Undo2Count] = CharTyped
Undo2.Pos[Undo2Count] = varStart2
xLine = CONTROLCMD(w1,1,@RTLINEFROMCHAR,-1)
Undo2.LineNo[Undo2Count] = xLine
CONTROLCMD(w1,1,@RTGETLINE,xLine,line$,512)
Offset = SendMessage(rHandle,EM_LINEINDEX,xLine,0)
Undo2.Offset[Undo2Count] = Offset
Was = (varStart - Offset) + 1
Undo2.Was[Undo2Count] = mid$(Line$,Was,1)
endif
We record everything except a back space key press (will explain that one later).
And now for our F1 (undo) and F2 (redo) keys...
Again in the sub classed edit handler we have this when one of these keys is pressed:
case WM_KEYDOWN
if wParam = 112 'F1 key (Undo)
if Undo2Count > 0
if Undo2.Typed[Undo2Count] = chr$(13) + chr$(10)
SendMessage(rHandle,EM_SETSEL,Undo2.pos[Undo2Count],Undo2.pos[Undo2Count]+2)
CONTROLCMD w1,1,@RTDELETESEL
else
select Undo2.Press[Undo2Count]
Case 1 'Char
SendMessage(rHandle,EM_SETSEL,Undo2.pos[Undo2Count],Undo2.pos[Undo2Count]+1)
CONTROLCMD w1,1,@RTDELETESEL
case 2 'Backspace
case& 3 'Delete key
SendMessage(rHandle,EM_SETSEL,Undo2.pos[Undo2Count],Undo2.pos[Undo2Count])
CONTROLCMD w1,1,@RTREPLACESEL,Undo2.Was[Undo2Count]
endselect
endif
Redo2Count ++
Redo2.Press[Redo2Count] = Undo2.Press[Undo2Count]
Redo2.Typed[Redo2Count] = Undo2.Typed[Undo2Count]
Redo2.Pos[Redo2Count] = Undo2.Pos[Undo2Count]
Redo2.LineNo[Redo2Count] = Undo2.LineNo[Undo2Count]
Redo2.Offset[Redo2Count] = Undo2.Offset[Undo2Count]
Redo2.Was[Redo2Count] = Undo2.Was[Undo2Count]
Undo2.Press[Undo2Count] = 0
Undo2.Typed[Undo2Count] = ""
Undo2.pos[Undo2Count] = -1
Undo2.LineNo[Undo2Count] = -1
Undo2.Offset[Undo2Count] = -1
Undo2.Was[Undo2Count] = ""
Undo2Count --
if Undo2Count < 0 then Undo2Count = 0
endif
endif
if wParam = 113 'F2 key (Redo)
if Redo2Count > 0
select Redo2.Press[Redo2Count]
case 1 'Char
CONTROLCMD w1,1,@RTSETSELECTION,Redo2.pos[Redo2Count],Redo2.pos[Redo2Count]
int varStart222,varEnd222
CONTROLCMD w1,1,@EDGETSELECTION,varStart222,varEnd222
CONTROLCMD w1,1,@RTREPLACESEL,Redo2.Typed[Redo2Count]
int xLine22 = CONTROLCMD(w1,1,@RTLINEFROMCHAR,Redo2.pos[Redo2Count])
Offset = SendMessage(rHandle,EM_LINEINDEX,xLine22,0)
case 2 'Backspace
SendMessage(rHandle,EM_SETSEL,Redo2.pos[Redo2Count]-1,Redo2.pos[Redo2Count])
CONTROLCMD w1,1,@RTDELETESEL
case 3 'Delete
SendMessage(rHandle,EM_SETSEL,Redo2.pos[Redo2Count],Redo2.pos[Redo2Count]+1)
CONTROLCMD w1,1,@RTDELETESEL
endselect
Undo2Count ++
Undo2.Press[Undo2Count] = Redo2.Press[Redo2Count]
Undo2.Typed[Undo2Count] = Redo2.Typed[Redo2Count]
Undo2.Pos[Undo2Count] = Redo2.Pos[Redo2Count]
Undo2.LineNo[Undo2Count] = Redo2.LineNo[Redo2Count]
Undo2.Offset[Undo2Count] = Redo2.Offset[Redo2Count]
Undo2.Was[Undo2Count] = Redo2.Was[Redo2Count]
Redo2.Press[Redo2Count] = 0
Redo2.Typed[Redo2Count] = ""
Redo2.pos[Redo2Count] = -1
Redo2.LineNo[Redo2Count] = -1
Redo2.Offset[Redo2Count] = -1
Redo2.Was[Redo2Count] = ""
Redo2Count --
if Redo2Count < 0 then Redo2Count = 0
endif
endif
Notice that I am incrementing one stack and decrementing the other!
Back space explained:
There is a subtle difference between pressing the back space and the delete keys - however it is an important one.
When you press the back space, the character to the LEFT of the cursor is deleted, as opposed to the delete key which deleted the one to the RIGHT of the cursor.
Also, the WM_CHAR detects the back space, but not the delete key press.
To combat these I also have in WM_KEYDOWN:
if wParam = 46 'Delete
int xLine2
xLine2 = CONTROLCMD(w1,1,@RTLINEFROMCHAR,-1)
CONTROLCMD(w1,1,@RTGETLINE,xLine2,line2$,512)
int varStart22,varEnd22
CONTROLCMD w1,1,@EDGETSELECTION,varStart22,varEnd22
Offset = SendMessage(rHandle,EM_LINEINDEX,xLine2,0)
int Wass = (varStart - Offset) + 1
Undo2Count ++
Undo2.Press[Undo2Count] = 3
Undo2.Typed[Undo2Count] = chr$(46)
Undo2.Pos[Undo2Count] = varStart22
Undo2.Offset[Undo2Count] = Offset
int Was = (varStart - Offset) + 1
Undo2.Was[Undo2Count] = mid$(Line2$,Wass,1)
RETURN CallWindowProcA(GetPropA(hwnd,"edit_handler"),hwnd,uMsg,wParam,lParam)
endif
if wParam = 8 'Backspace
xLine2 = CONTROLCMD(w1,1,@RTLINEFROMCHAR,-1)
CONTROLCMD(w1,1,@RTGETLINE,xLine2,line2$,512)
CONTROLCMD w1,1,@EDGETSELECTION,varStart2,varEnd2
Undo2Count ++
Undo2.Press[Undo2Count] = 2
Undo2.Pos[Undo2Count] = varStart2
Undo2.LineNo[Undo2Count] = xLine2
Offset = SendMessage(rHandle,EM_LINEINDEX,xLine2,0)
Undo2.Offset[Undo2Count] = Offset
Was = (varStart2 - Offset)
Undo2.Was[Undo2Count] = mid$(Line2$,Was,1)
Undo2.Typed[Undo2Count] = chr$(8)
RETURN CallWindowProcA(GetPropA(hwnd,"edit_handler"),hwnd,uMsg,wParam,lParam)
endif
There are other things I need to do yet, as this only works for typing not things like mouse "copy", "cut", "delete" etc....
Hope you find this interesting, if you have some questions please ask away!
Thanks for reading this,
Andy.
:)
Isn't it kinda neat how your brain gets to going digging deeper and deeper into each one of these facets to something that started out as a general idea. And also the amount of time you can spend in researching, testing, and debugging, and then identifying additional aspects that need to be addressed.
I can fully appreciate what you are doing because of the time I spent over the years on the IDE, IWB+, and my other offerings, both free and not.
Thanks Larry, the appreciation is mutual.
Yes it is impressive what the brain can do, it certainly makes me get up in the morning and carry on with this one.
The undo / redo is yet the latest feature to be added in, and not for the first time when I have added something new in it can cause something that was previously working okay to go wrong or the other way round.
If we consider this:
1. My program has auto complete of key words.
2. Auto complete has been working for a long time.
I add in undo / redo and it stops working correctly when a key word is selected.
If we remember anything typed is being added to the undo stack and how the auto complete works...
1. I want to type OPENCONSOLE in my editor.
2. I type op (the first two characters of OPENCONSOLE.
3. OPENCONSOLE appears on screen.
4. I press ENTER to select it.
5. op is replaced in the rich edit text with OPENCONSOLE.
This is how the auto complete works - however I found that selecting a key word as above now makes undo / redo go wrong - why?
In the undo stack we actually have opOPENCONSOLE not OPENCONSOLE - the op might have disappeared on screen but it has automatically been added to the undo stack and we need to remove it.
In my normal (not the sub classed handler for the rich edit) I have for CASE 1 (the rich edit)
SELECT @NOTIFYCODE
CASE @ENENTERKEY '<---- Enter key pressed
I know I have selected a key word by pressing ENTER so now I have to remove the letters "op" and add in "OPENCONSOLE"
I do this by adding in here:
Undo2Count = (Undo2Count - lEn(TheWord))+2
for a = 1 to Len(InfoA)
int xLine,Was
Undo2Count ++
Undo2.Press[Undo2Count] = 1
Undo2.Typed[Undo2Count] = mid$(InfoA,a,1)
Undo2.Pos[Undo2Count] = a + (ttx-1)
xLine = CONTROLCMD(w1,1,@RTLINEFROMCHAR,-1)
Undo2.LineNo[Undo2Count] = xLine
CONTROLCMD(w1,1,@RTGETLINE,xLine,line$,512)
Offset = SendMessage(rHandle,EM_LINEINDEX,xLine,0)
Undo2.Offset[Undo2Count] = Offset
Was = (varStart - Offset) + 1
Undo2.Was[Undo2Count] = mid$(Line$,Was,1)
next a
The string variable "TheWord" has "op" stored in it, and "InfoA" has "OPENCONSOLE" in it.
So I decrease the undo count by the length of what I have typed (2 characters --> len of TheWord) and then add in each letter of OPENCONSOLE to the undo stack - this then fixes the undo / redo.
This is an example of how an existing feature can have you going around in circles for days until you realise why. :o
To wrap up the undo / redo we need to clear out the redo stack when the undo count becomes zero and something else is typed.
In the rich edit sub class handler we amend these cases to include Redo2Count = 0 when this becomes the case...
CASE WM_KEYDOWN
if wParam = 46 'Delete
if Undo2Count = 0
Redo2Count = 0
endif
if wParam = 8 'Backspace
if Undo2Count = 0
Redo2Count = 0
endif
CASE WM_CHAR
if Undo2Count = 0
Redo2Count = 0
endif
Banana skin now in the bin! 8)
Andy.
To finish off the undo / redo I now had to look at mouse right click options such as cut, copy and paste.
When we are doing these, we are mainly selecting a block of text rather than a single character so another approach is need.
The solution was to create two new arrays - one for undo, and one for redo to store blocks of text.
TYPE UndoBlockText
INT UndoCount[1000]
INT StartPos[1000]
INT EndPos[1000]
INT Length[1000]
INT LineNo[1000]
ISTRING BlockText[100000,1000]
ENDTYPE
DEF UndoBlock as UndoBlockText
TYPE RedoBlockText
INT UndoCount[1000]
INT StartPos[1000]
INT EndPos[1000]
INT Length[1000]
INT LineNo[1000]
ISTRING BlockText[100000,1000]
ENDTYPE
DEF RedoBlock as RedoBlockText
Now if we take a "cut" mouse option for example where a block of text has been highlighted we can add the details we need:
CASE @IDMENUPICK
SELECT @CODE
CASE 201 'Cut
IF LEN(CutLine)
SENDMESSAGE(rHandle,EM_EXGETSEL,0,cr)
Undo2Count ++
Undo2.Press[Undo2Count] = 4
Undo2.Length[Undo2Count] = len(ClipText)
UndoBlock.BlockText[0,Undo2Count] = ClipText
UndoBlock.StartPos[Undo2Count] = cr.cpMin
UndoBlock.EndPos[Undo2Count] = cr.cpMax
UndoBlock.Length[Undo2Count] = len(ClipText)
UndoBlock.LineNo[Undo2Count] = cLine-1
ENDIF
Note here, I am still adding some details to the original undo array, but also adding some details to the new extra undo array (The variable "ClipText" is a large ISTRING that contains the selected text).
And now for the undo for a "cut" request:
In the sub classed rich edit handler under WM_KEYDOW (for my undo key "F1") I have this
CASE 4 'Undo Cut
SENDMESSAGE(rHandle,EM_SETSEL,UndoBlock.StartPos[Undo2Count],UndoBlock.StartPos[Undo2Count])
CONTROLCMD w1,1,@RTREPLACESEL,UndoBlock.BlockText[0,Undo2Count]
Redo2Count ++
Redo2.Press[Redo2Count] = Undo2.Press[Undo2Count]
RedoBlock.UndoCount[Redo2Count] = UndoBlock.UndoCount[Undo2Count]
RedoBlock.StartPos[Redo2Count] = UndoBlock.StartPos[Undo2Count]
RedoBlock.EndPos[Redo2Count] = UndoBlock.EndPos[Undo2Count]
RedoBlock.Length[Redo2Count] = UndoBlock.Length[Undo2Count]
RedoBlock.LineNo[Redo2Count] = UndoBlock.LineNo[Undo2Count]
RedoBlock.BlockText[0,Redo2Count] = UndoBlock.BlockText[0,Undo2Count]
Undo2Count --
IF Undo2Count < 0 then Undo2Count = 0
And for the redo:
CASE 4 'Redo Cut
SENDMESSAGE(rHandle,EM_SETSEL,RedoBlock.StartPos[Redo2Count],RedoBlock.EndPos[Redo2Count])
CONTROLCMD w1,1,@RTDELETESEL
Undo2Count ++
Undo2.Press[Undo2Count] = Redo2.Press[Redo2Count]
UndoBlock.UndoCount[Undo2Count] = RedoBlock.UndoCount[Redo2Count]
UndoBlock.StartPos[Undo2Count] = RedoBlock.StartPos[Redo2Count]
UndoBlock.EndPos[Undo2Count] = RedoBlock.EndPos[Redo2Count]
UndoBlock.Length[Undo2Count] = RedoBlock.Length[Redo2Count]
UndoBlock.LineNo[Undo2Count] = RedoBlock.LineNo[Redo2Count]
UndoBlock.BlockText[0,Undo2Count] = RedoBlock.BlockText[0,Redo2Count]
Redo2Count --
IF Redo2Count < 0 then Redo2Count = 0
As you can see, the method of incrementing one array and decrementing the other and copying the details across works nicely - of course you have to consider other requests, but this is how you do it (or at least how I do it).
This added code now finishes off the undo / redo functions.
Thanks for reading,
Andy.