April 18, 2024, 10:56:55 PM

News:

Own IWBasic 2.x ? -----> Get your free upgrade to 3.x now.........


Undo / Redo - how to do it ourselves

Started by Andy, July 12, 2020, 04:20:06 AM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Andy

July 12, 2020, 04:20:06 AM Last Edit: July 12, 2020, 04:21:53 AM by Andy
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.
:)
Day after day, day after day, we struck nor breath nor motion, as idle as a painted ship upon a painted ocean.

LarryMc

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.
LarryMc
Larry McCaughn :)
Author of IWB+, Custom Button Designer library, Custom Chart Designer library, Snippet Manager, IWGrid control library, LM_Image control library

Andy

July 12, 2020, 11:10:05 PM #2 Last Edit: July 13, 2020, 03:26:52 AM by Andy
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.
Day after day, day after day, we struck nor breath nor motion, as idle as a painted ship upon a painted ocean.

Andy

July 23, 2020, 04:28:55 AM #3 Last Edit: July 23, 2020, 04:42:28 AM by 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.
Day after day, day after day, we struck nor breath nor motion, as idle as a painted ship upon a painted ocean.