The Visual FoxPro textbox isn’t exactly a highly featured control as is – in Help Builder I had to bend over backwards to make it work as a text based editor for long text that includes formatting. But at the same time I’ve not been able to find a decent replacement either. Most ActiveX based text controls are either buggy as hell (at least in VFP) or they are very, very slow if you hook up any COM event code to the key processing which Help Builder needs to do.
For the most part the stock Fox TextBox works fine – except for two things:
1 . There’s a bug in the control that causes the control to wrap funky if linefeeds happen to fall into a very specific area in the right margin. This can cause linefeeds to get ‘eaten’ by the text box which bunches up text that can all of a sudden jump when the textbox gets resized or other text gets entered to push the linefeed off a margin. This may seem like a really obscure bug, but if your doing lots of text editing you’ll run into this one pretty quickly. According to Calvin Hsia this one will be fixed in VFP 9.0 SP1 …
2. Undo behavior. Visual FoxPro’s TextBox doesn’t do undo real well – the undo buffer gets blown away whenever there’s any sort of programmatic update to the value. This includes control source binding, setting the value explicitly, changing SelText or even pasting text into the control. It also clears if you tab out of the control and immediately come back to it. All of that is really very limited and non-standard behavior.
Until SP1 ships I can’t do anything about #1 but I decided maybe I can rig my own Undo buffering in my custom TextBox control. While talking to Calvin at Southwest Fox I was begging him to put better undo behavior into the textbox, but the undo behavior is deeply rooted in the Fox runtime and changing that behavior would likely break a lot of existing code. So no help here. So, Calvin suggested, write your own…
At first I thought – yeah, right. Managing Undo buffers with Fox code is likely going to be pretty slow and very memory intensive, because you basically have to save the entire buffer of the control value as the InteractiveChange and ProgrammaticChange events don’t provide you with information on what’s changed, so there’s no easy way to capture just what’s changed to play back.
After some thought I tried it anyway just to see how things would work out and here’s what I ended up with:
- Optional custom Undo behavior
- Undo that survives, programmatic changes
- Undo that survives focusing to another control
- Undo that gets cleared only on Refresh or an explicit clear of the Undo buffer
- Redo that lets you undo the undo
Here’s the control relevant code that deals with the Undo behavior on my TextBox subclass
DEFINE CLASS wwhtmleditbox AS editbox
OLEDropMode = 1
FontName = "Tahoma"
FontSize = 8
Alignment = 0
AllowTabs = .T.
Height = 188
ScrollBars = 2
Width = 443
oundobuffer = .NULL.
*-- Last time the UndoBuffer was updated in seconds. Internally used value that keeps every character from getting added to the undo buffer._
nlastundoupdate = 0
*-- Flag used to disable Programmatic changes to the Undo buffer.
lundonoprogrammaticchange = .F.
lundotracking = .F.
oredobuffer = .NULL.
Name = "wwhtmleditbox"
PROCEDURE Init
this.oUndoBuffer = CREATEOBJECT("wwNameValueCollection")
this.oRedoBuffer = CREATEOBJECT("wwNameValueCollection")
ENDPROC
PROCEDURE undo
IF THIS.lundotracking AND THIS.oUndoBuffer.Count > 0
THIS.lUndoNoProgrammaticChange = .T.
this.oRedoBuffer.FastAdd(TRANSFORM(this.SelStart),this.Value)
lcValue = this.oUndoBuffer.aItems[this.oUndoBuffer.Count,2]
IF lcValue = this.Value AND this.oUndoBuffer.Count > 1
THIS.Value = this.oUndoBuffer.aItems[this.oUndoBuffer.Count-1,2]
this.oUndoBuffer.Remove(this.oUndoBuffer.Count)
ELSE
this.Value = lcValue
ENDIF
this.SelStart = VAL(this.oUndoBuffer.aItems[this.oUndoBuffer.Count,1])
THIS.lUndoNoProgrammaticChange = .F.
this.oUndoBuffer.Remove(this.oUndoBuffer.Count)
ENDIF
ENDPROC
PROCEDURE redo
IF THIS.lundotracking AND THIS.oRedoBuffer.Count > 0
THIS.lUndoNoProgrammaticChange = .T.
this.Value = this.oRedoBuffer.aItems[this.oReDoBuffer.Count,2]
this.SelStart = VAL(this.oRedoBuffer.aItems[this.oRedoBuffer.Count,1])
THIS.lUndoNoProgrammaticChange = .F.
this.oRedoBuffer.Remove(this.oRedoBuffer.Count)
ENDIF
ENDPROC
PROCEDURE KeyPress
LPARAMETERS nKeyCode, nShiftAltCtrl
*** Don't want ESC to wipe out content of field.
IF nKeyCode = 27
*** Eat the key
NODEFAULT
ENDIF
*** Ctrl-Z
IF THIS.lUndoTracking
IF nKeyCode = 26
*** Must check for Ctrl-<- which is also 26
DECLARE INTEGER GetKeyState IN WIN32API INTEGER
IF GetKeyState(0x25) > -1
THIS.Undo()
NODEFAULT
ENDIF
ENDIF
*** Redo Ctrl-R
IF nKeyCode = 18
THIS.Redo()
NODEFAULT
ENDIF
ENDIF
ENDPROC
PROCEDURE ProgrammaticChange
IF THIS.lUndoTracking AND !THIS.lUndoNoProgrammaticChange
this.oUndoBuffer.FastAdd(TRANSFORM(this.SelStart),this.Value)
this.oRedoBuffer.Clear()
ENDIF
ENDPROC
PROCEDURE InteractiveChange
IF THIS.lUndoTracking
*** Update only in half second intervals,
*** so if you type a bunch of words it goes in batch
IF SECONDS() - THIS.nLastUndoUpdate < 1
this.nLastUndoUpdate = SECONDS()
RETURN
ENDIF
*** Only write undo on last word boundary
IF LASTKEY() = 32 OR LASTKEY() = 13 OR LASTKEY() = 44 OR ;
LASTKEY() = 46 OR LASTKEY() = 9
this.oUndoBuffer.FastAdd(TRANSFORM(this.SelStart),this.Value)
this.oRedoBuffer.Clear()
this.nLastUndoUpdate = SECONDS()
ENDIF
ENDIF
ENDPROC
PROCEDURE Refresh
IF THIS.lUndoTracking
THIS.oUndobuffer.Clear()
ENDIF
ENDPROC
ENDDEFINE
Note this code has one dependency I’m not including here. I’m using a custom NameValueCollection class which stores name and value in an array. You can change that code to use a real Collection and an object to store the buffer value and the SelStart position.
The idea is essentially that every InteractiveChange and ProgrammaticChange is monitored and potentially writes out the value to the UndoBuffer collection. The InteractiveChange is staggered so that it only writes out data if the user is not actively typing and if the cursor is on a word boundary. This reduce the amount of values that get stored tremendously. It looks like other applications like Word use a similar approach although Word’s behavior is a bit different.
Notice also this shitty code:
IF nKeyCode = 26
*** Must check for Ctrl-<- which is also 26
DECLARE INTEGER GetKeyState IN WIN32API INTEGER
IF GetKeyState(0x25) > -1
THIS.Undo()
NODEFAULT
ENDIF
ENDIF
In their infinit wisdom somebody decided to map the keycode of 26 to both Ctrl-Z and Ctrl-Left Arrow, so there's no easy way to to tell the character. Instead you have to do another check on the key code to see if it's a the left arrow (0x25). If it is this return -127 or -128. Talk about HACK, but it works. It was kinda fun for a few minutes to have Ctrl-LeftArrow tied to the Undo key - Good thing I decided to build in Redo behavior from the start ...
I’ve plugged this into Help Builder now and as I’m working on the Web Connection 5.0 docs I’m heavily using the features with some fairly long topics. So far the behavior is looking good. I don’t see any big performance issues for either memory or anything noticeable during typing.
I guess I can check that one of my list of VFP wishlist items I was never going to get …
Other Posts you might also like