Building a text editor (Part 3)

(Update: The original version of this installment included code which caused a memory leak. Fernando has fixed the code, and I recommend that you look at the latest version of wxhnotepad (cabal fetch wxhnotepad). I have updated the tutorial text below.)

This is the third installment of a series looking at wxhNotepad, a wxHaskell application written by Fernando Benavides, and this is where things start to become more interesting as we are adding some real functionality – in this case undo and redo functionality.

The wxHaskell TextCtrl () does, in fact, contain undo / redo support. It is not really ideal for a text editor, however, since it allows ‘undo’ on a new text file, for example.

GUI top level

The first thing to do is to extend the GUIContext type to maintain undo and redo history, each of which we express as a list of strings. There is also a TimerEx (), and this requires a little more explanation. The details will become clearer as we work through the code, but basically we use the timer as a means to help us to group ‘undoable’ and ‘redoable’ actions into larger units than characters.

Because we are not using the built-in undo/redo functionality in the TextCtrl, we need to define some unique menu identifiers. These must not conflict with identifiers in use elsewhere in the application. The numbers used are ‘magic’ – there is currently no convenient way to ensure that there is no such conflict – something which should probably be fixed in a future release of wxHaskell.

We create a timer instance in much the same way as any wxHaskell control. The timer interval is set to 1000 seconds (10^6ms).

The timer is used in an interesting way. Each time a keyboard event is received, restartTimer is called before sending the keyboard event on to the TextCtrl (we do this with the call to propagateEvent, which passes any interecpted event on to other controls which might wish to respond to it). In restartTimer a shorter (1 sec) timer interval is used to group input into chunks a little longer than a single character, so that undo and redo work on more meaningful chunks of text.

Once the timer has been created, we call updatePast to begin recording the undo/redo stream.

File Handling Events

The changes to the file handling are very minimal – just a couple of lines in openPage so that the undo history is cleared when a new file is opened, and then populated with the initial file text (since this represents the first state of the ‘undo’ action list).

The updatePast function adds a new event to the undo history list. When updatePast is called, both the history and current editor text (tnow) are fetched and compared. If the head of the history list is not the same as the editor text, record the change.

> updatePast guiCtx@GUICtx{guiEditor = editor, guiPast = past, guiFuture = future} =
> do
> tnow <- get editor text
> history <- varGet past
> case history of
> [] -> varSet past [tnow]
> t:_ -> if t /= tnow -- if there was a real change, we recorded it
> then do
> varUpdate past (tnow:)
> varSet future [] -- we clean the future because the user is writing a new one
> else -- otherwise we ignore it
> return ()
> -- Now that history is up to date, we let the timer rest until new activity
> -- is detected
> killTimer guiCtx

The restartTimer function is used as part of a mechanism to try to separate undo/redo information into useful chunks instead of individual characters. This is done by starting a short duration (1 sec) timer which gets reset each time there is input from the user. The result is that we have a ‘timer’ which times out whenever the user pauses typing for more than a second. This is called from the textCtrl keyboard event handler, and allows us to group keyboard input according to natural pauses in typing.

The killTimer function starts a new timer of very long duration (something around 20 minutes). Should the timer expire, no action is taken, as the purpose here is simply to wait for some keyboard activity.

Share this:

Like this:

Related

Hi Jeremy… I’ve seen the memory leak with the timers and I saw it in (the project from where wxhnotepad was born) too. I’m currently trying a different approach there (where I use WXCore functions like timerCreate, timerStop, timerStart, etc. but every once in a while it fails with a GHC error and brings the whole app down.
If I finally master the TimerEx management, I’ll update wxhnotepad and so you can update this post ;)
Anyway… as usual… excellent article, you understand what I did even better than I do :P