Archive for the ‘wxHaskell’ Category

Building a text editor (Part 4)

April 6, 2010 Leave a comment

In this very short post we add copy/cut/paste support to the editor. These will be accessible from the menu as well as by using the usual CUA keystrokes (Ctrl-C, Ctrl-X, Ctrl-V).

Most of the required functionality is provided by the TextCtrl itself, so the code mainly consists of connecting up event handlers to bind to the TextCtrl functions.

The first issue we need to deal with is a wxHaskell bug – this will be fixed shortly, but in the meantime it is worth knowing about. The problem is that wxHaskellprovides the wrong constants for wxID_CUT, wxID_COPY and wxID_PASTE, which are the standard identifiers used by the text control.

As always, changed text id highlighted in red.

 > import Graphics.UI.WXCore hiding (wxID_CUT, wxID_COPY, wxID_PASTE)

We can now put the correct values in ourselves. These are ‘magic’ numbers, and you’ll have to trust me that they are correct – it is not particularly easy to work out the correct values without wading through lots of C++ code.

 > wxID_MYUNDO = 5107
 > wxID_MYREDO = 5108
 > wxID_CUT    = 5031
 > wxID_COPY   = 5032
 > wxID_PASTE  = 5033

We now add the menu items and their event handlers. Nothing new here: we’ve looked at menus before. These go into the GUI top level function, along with the other UI component definitions.

 > menuAppendSeparator mnuEdit
 > menuAppend mnuEdit wxID_CUT "C&ut\tCtrl-x" "Cut" False
 > menuAppend mnuEdit wxID_COPY "&Copy\tCtrl-c" "Copy" False
 > menuAppend mnuEdit wxID_PASTE "&Paste\tCtrl-v" "Paste" False

 > evtHandlerOnMenuCommand win wxID_CUT $ cut guiCtx
 > evtHandlerOnMenuCommand win wxID_COPY $ copy guiCtx
 > evtHandlerOnMenuCommand win wxID_PASTE $ paste guiCtx

We also require three new event handler functions:

— We just copy the selected text

The copy function simply uses the textCtrlCopy function provided by the control. Remember (see part 2) that we use GUICtx as a way to pass around the global state (the GUI widgets).

 > copy GUICtx{guiEditor = editor} =
 >   textCtrlCopy editor

The cut function also uses the standard editor functionality, but in this case we are modifying the contents of the TextCtrl and we must update the GUI with an undo action.

 > cut guiCtx@GUICtx{guiEditor = editor} =
 >   textCtrlCut editor >> updatePast guiCtx

The paste function also modifies the undo history.

 > paste guiCtx@GUICtx{guiEditor = editor} =
 >   textCtrlPaste editor >> updatePast guiCtx
Categories: wxHaskell Tags:

Building a text editor (Part 3)

March 18, 2010 3 comments

(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.

As in the previous post, new code is highlighted in red.

> data GUIContext = GUICtx { guiWin    :: Frame (),
> guiEditor :: TextCtrl (),
> guiFile   :: Var (Maybe FilePath),
> guiTimer  :: TimerEx (),       -- ^ A timer to detect user actions
> guiPast   :: Var [String],     -- ^ For Undo history
> guiFuture :: Var [String]      -- ^ For Redo history
> }

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.

> wxID_MYUNDO = 5107
> wxID_MYREDO = 5108

Most of the changes to the top level GUI function require little comment as we have discussed the details in earlier installments.

> step3 :: IO ()
> step3 =
>   do
>     win <- frame [text := "wxhNotepad - Step 3", visible := False]
>     editor <- textCtrl win [font := fontFixed,
>                             text := "Undo / Redo support is ready by " ++
>                                     "default on text controls..."]
>     filePath <- varCreate Nothing

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.

>     refreshTimer <- timer win []
>     past <- varCreate []
>     future <- varCreate []
>     let guiCtx = GUICtx win editor filePath refreshTimer past future
>     timerOnCommand refreshTimer $ updatePast guiCtx
>     set editor [on keyboard := \_ -> restartTimer guiCtx >> propagateEvent]
>     updatePast guiCtx

The remainder of the changes in the GUI top level relate to the extra menu entries to support undo and redo. These follow the pattern described in the previous post.

>     mnuFile <- menuPane [text := "File"]
>     mnuEdit <- menuPane [text := "Edit"]
>     menuAppend mnuFile wxID_OPEN "&Open...\tCtrl-o" "Open Page" False
>     menuAppend mnuFile wxID_SAVE "&Save\tCtrl-s" "Save Page" False
>     menuAppend mnuFile wxID_SAVEAS "Save &as...\tCtrl-Shift-s" "Save Page as" False
>     menuAppend mnuFile wxID_CLOSE "&Close\tCtrl-W" "Close Page" False
>     menuAppend mnuEdit wxID_MYUNDO "&Undo\tCtrl-z" "Undo last action" False
>     menuAppend mnuEdit wxID_MYREDO "&Redo\tCtrl-Shift-z" "Redo last undone action" False
>     evtHandlerOnMenuCommand win wxID_OPEN $ openPage guiCtx
>     evtHandlerOnMenuCommand win wxID_SAVE $ savePage guiCtx
>     evtHandlerOnMenuCommand win wxID_SAVEAS $ savePageAs guiCtx
>     evtHandlerOnMenuCommand win wxID_CLOSE $ windowClose win False >> return ()
>     evtHandlerOnMenuCommand win wxID_MYUNDO $ undo guiCtx
>     evtHandlerOnMenuCommand win wxID_MYREDO $ redo guiCtx
>     set win [menuBar := [mnuFile, mnuEdit]]
>     set win [layout := fill $ widget editor,
>              clientSize := sz 640 480]
>     focusOn editor
>     set win [visible := True]

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).

> openPage guiCtx@GUICtx{guiWin = win, guiEditor = editor, guiFile = filePath} =
>   do
>   maybePath <- fileOpenDialog win True True "Open file..."
>                               [("Haskells (*.hs)",["*.hs"]),
>                                ("Texts (*.txt)", ["*.txt"]),
>                                ("Any file (*.*)",["*.*"])]
>                               "" ""
>   case maybePath of
>     Nothing   -> return ()
>     Just path -> do
>                  clearPast guiCtx
>                  textCtrlLoadFile editor path
>                  updatePast guiCtx
>                  set win [text := "wxhnotepad - " ++ path]
>                  varSet filePath $ Just path

There is a new event handler for the ‘undo’ menu item.

> undo guiCtx@GUICtx{guiEditor = editor, guiPast = past, guiFuture = future} =
>   do
>     -- Now we try to detect if there's something new and kill the timer
>     updatePast guiCtx
>     history <- varGet past
>     case history of
>       []            -> return () -- Nothing to be undone
>       [t]           -> return () -- Just the initial state, nothing to be undone
>       tnow:tlast:ts -> do
>                          -- We add an action to be done on redo
>                          varUpdate future (tnow:)
>                          -- we remove it from the history
>                          varSet past $ tlast:ts
>                          -- and set the editor accordingly
>                          set editor [text := tlast]

The ‘redo’ menu item is handled similarly…

> redo guiCtx@GUICtx{guiEditor = editor, guiPast = past, guiFuture = future} =
>   do
>     -- First of all, we try to detect if there's something new
>     updatePast guiCtx
>     coming <- varGet future
>     case coming of
>       []   -> return () -- Nothing to be redone
>       t:ts -> do
>         -- remove it from the coming actions
>         varSet future ts
>         -- move it to the history actions
>         varUpdate past (t:)
>         -- and set the editor accordingly
>         set editor [text := t]

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 clearPast function clears the undo (past) and redo(future) lists.

> clearPast GUICtx{guiPast = past, guiFuture = future} =
>  do
>    varSet past []
>    varSet future []

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.

> restartTimer guiCtx@GUICtx{guiWin = win, guiTimer = refreshTimer} =
>  do
>    started <- timerStart refreshTimer 1000 True
>    if started
>        then return ()
>        else do
>           errorDialog win "Error" "Can't start more timers"
>           wxcAppExit

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.

> killTimer GUICtx{guiTimer = refreshTimer} = timerStop refreshTimer

Note: there is a memory leak in this code

Categories: wxHaskell Tags:

Ever expanding TODO lists

February 16, 2010 5 comments

There was a short discussion on the wxhaskell-users list yesterday about a fairly long outstanding wxHaskell bug: the fact that you cannot restart a wxHaskell GUI from within GHCi.

This post is not about that bug, but about a comment from Eric Kow:

 people are actually quite understanding about things being done slowly if
 they have a clear idea where they're headed, ie. what's being prioritised,
 what's not; what things are blocked on, etc.

Eric is quite right, of course, so this post is a quick dump of my wxHaskell TODO list. Sadly, at the moment I seem to be the only person doing any wxHaskell core development (though I’d like to namecheck some great help from Brian Lewis on Cabalizing wxHaskell a couple of months back), so it is probably the wxHaskell TODO list. The list is in approximate priority order, although I reserve the right to change it unilaterally if I find I have an urgent need for something!

  1. Update wxHaskell to work with GHC 6.12.1 once the next Haskell Platform is released (I don’t have the time to go putting together a complete platform myself from source, so I’ll wait on the Platform)
  2. Finish the series on wxhNotepad. I see this as extremely important in helping wxHaskell beginners to get going. The next two posts are mostly finished, and you should see them soon.
  3. Solve the problem (a consequence of Cabalization) that, at least on Mac and Windows, it is no longer possible to use wxHaskell at all in GHCi due to a dependency on libstdc++ (which exists only as a static library on Mac and Windows). This is really a GHC/GHCi bug, but we need to deal with the consequences.
  4. Write improved documentation and/or blog articles on specific aspects of wxHaskell. Layout is a strong contender for more work.
  5. Investigate why wxHaskell cannot be reloaded in GHCi. Actually, I have a pretty good idea what the problem is. The wxWidgets library uses C++ static constructors and destructors in a few key places. When wxHaskell returns, what it actually does is to return from wxApp::OnInit(). Unfortunately, on return from this function, some of the internal structures of wxWidgets are corrupted and this can only be resolved by ensuring that the static destructors are executed and that the static constructors are re-run. I believe that the solution will probably involve dynamically unloading the wxWidgets library and reloading it. Since this is something which is almost completely OS-dependent, it will require me immersing myself in arcane details of dynamic linking for Windows, Linux and Mac to get it working.
  6. Get the Styled Text Control back into wxHaskell, but now as a separate (Cabal installable) module. This will act as a demonstration/tutorial for bringing any wxWidgets component which can be compiled as a separate library into wxHaskell
  7. Look at the list of bugs – both in the buglist and those accumulated in wxhaskell-users
  8. Write a guide to make it easier for others to contribute to wxHaskell. I’m well aware that the amount of effort I can contribute to the project is not really enough to keep it thriving, and I’d like to lower the barrier to entry.

I find it slightly sad that the Haskell community doesn’t really seem able to support even one full-featured GUI binding (from what I can see, Gtk2hs development is also very slow at the moment). It’s not very glamorous work, but I believe it is an important part of making Haskell useful to a wider range of people.

Categories: wxHaskell

Building a text editor (Part 2)

January 31, 2010 2 comments

In this installment, we will extend the text editor with some essential basic functionality: a menu bar and the ability to load and save files. Code which is new or changed from part 1 is highlighted in red (that’s most of the code in this case!)

The top level GUI function

Something which rapidly becomes an issue when coding real wxHaskell applications is the fact that there’s quite a bit of state which you find yourself passing around to many of the functions. In particular, you will often find that, say, and event handler for one control will want to perform some action(s) on other controls. You can, of course, pass all of this information around as curried function parameters, but it quickly gets out of hand.

The most common approach in wxHaskell applications is to create one or more types which hold the required state information and keep these in some suitable data structure. This way you only have one value to curry rather than several. GUIContext is the type we use in this application. It’s not really needed at this point in the evolution of our editor, but it will be useful later.

The Var type is conceptually rather similar to an MVar or an IORef. It’s a place to put mutable data, with the benefit that support is already built-in to wxHaskell. As with MVar and the like, it must be created (using varCreate) before it can be used.

  • guiWin contains the Frame which owns the controls in the text editor.
  • guiEditor is the textCtrl instance we use to display and edit text.
  • guiFile is a mutable variable used to hold the path to the file being edited. It is a Maybe type as a newly created file has no filename until it has been saved.
 > data GUIContext = GUICtx { guiWin    :: Frame (),
 >                            guiEditor :: TextCtrl (),
 >                            guiFile   :: Var (Maybe FilePath) > }
 > step2 :: IO ()
 > step2 =
 >  do
 >  win <- frame [text := "wxhNotepad - Step 2", visible := False]
 >  editor <- textCtrl win [font := fontFixed,
 >                          text := "Now the user can open a file, save it" ++
 >                                  "or save it with another name.\n" ++
 >                                  "Our program is smart enough to remember" ++
 >                                  "the path of the last opened/saved file\n" ++
 >                                  "Note that we're *not* catching any " ++
 >                                  "filesystem errors here.  That's left " ++
 >                                  "as homework for you :P"]
 >  filePath <- varCreate Nothing
 >  let guiCtx = GUICtx win editor filePath

In the last installment, we saw how to use the declarative approach to creating a menu in Graphics.UI.WX.Menu. You can also use the lower-level functions in Graphics.UI.WXCore, and that is what we do here. If you follow the documentation link, you will see that the documentation is extremely minimal. Typically is just gives the function type and a (very abbreviated) usage. To really understand what the function does, you will most likely need to refer to the wxWidgets C++ documentation for the equivalent function, so we’re going to see what that means in practice.

The first thing to realize is that you probably want a minimal ability to read C++, at least enough to understand a function signature and to guess how the parameters map to the Haskell function signature. It is also useful to have a notion of how object oriented design works, since there’s no getting away from the fact that wxWidgets, which underlies wxHaskell, is heavily object-oriented in its design.

Let’s take the example of menuAppend.

The wxHaskell documentation says: menuAppend :: Menu a -> Id -> String -> String -> Bool -> IO () with the further comment “usage: (menuAppend obj id text help isCheckable)”.

Now, there is a naming convention which is followed fairly consistently, and that is that the start of any function name in Graphics.UI.WXCore.WxcClassesXX is the same as the name of the equivalent wxWidgets class with the “wx” prefix lopped off (all wxWidgets classes and names are prefixed “wx” as the design of the library predates C++ namespaces). Once you have read the last sentence through a couple of times, you’ll realize that all I really meant to say is that we should go to the wxWidgets documentation and look for the class wxMenu. Furthermore (and again, this is a consistent naming convention), I am most likely interested in a function which performs an Append() operation on a wxMenu.

When we go to the documentation for wxMenu::Append() we see that there are three variants of wxMenu::Append(). For those who aren’t familiar with C++, I should explain that C++ lets a single function name be overloaded with different parameter types. Haskell doesn’t allow this, so our menuAppend function can only be the equivalent of one of these type signatures.

  • wxMenuItem* Append(int id, const wxString& item = “”, const wxString& helpString = “”, wxItemKind kind = wxITEM_NORMAL) Adds a string item to the end of the menu.
  • wxMenuItem* Append(int id, const wxString& item, wxMenu *subMenu, const wxString& helpString = “”) Adds a pull-right submenu to the end of the menu. Append the submenu to the parent menu after you have added your menu items, or accelerators may not be registered properly.
  • wxMenuItem* Append(wxMenuItem* menuItem) Adds a menu item object. This is the most generic variant of Append() method because it may be used for both items (including separators) and submenus and because you can also specify various extra properties of a menu item this way, such as bitmaps and fonts.

I hope it doesn’t require a “super C++ guru” to realise that the first of the list items above is the closest to our menuAppend function. Those who have really been paying attention may notice that there are actually several functions which start menuAppend, including menuAppendSub and menuAppendItem, and these are actually wrappers for the other two variants of wxMenu::Append(). The only other thing you need to observe to line the parameters up between the C++ and the Haskell versions of the functions is that there is an initial obj parameter in all of the Haskell functions. This is because you would normally use an object pointer or reference in C++ so that the C++ code:

mnuFile->Append(wxID_OPEN, "&Open...\tCtrl-o", "Open Page", 0);

is effectively rendered in Haskell as:

menuAppend mnuFile wxID_OPEN "&Open...\tCtrl-o" "Open Page" False

One other aspect of using the functions in Graphics.UI.WXCore.WxcClassesXX is that we have to care about having unique menu identifiers. The standard menu items (Open, Close, Save and the like) have standard identifiers, and we use them here.

 > mnuFile <- menuPane [text := "File"]
 > menuAppend mnuFile wxID_OPEN "&Open...\tCtrl-o" "Open Page" False
 > menuAppend mnuFile wxID_SAVE "&Save\tCtrl-s" "Save Page" False
 > menuAppend mnuFile wxID_SAVEAS "Save &as...\tCtrl-Shift-s" "Save Page as" False
 > menuAppend mnuFile wxID_CLOSE "&Close\tCtrl-W" "Close Page" False

When creating menu entries the WXCore way, we need to attach event handlers to each item. The evtHandlerOnMenuCommand :: EvtHandler a -> Id -> IO () -> IO () function associates a menu ID in a given window with an event handler.

It’s worth examining evtHandlerOnMenuCommand a little more closely. The first thing to notice is that I said that we associate an event handler with a window and a unique identifier. If you check back over the code, win is of type Frame (), but evtHandlerOnMenuCommand wants an EvtHandler a how does this work?

The underlying wxWidgets library is a piece of OO design in C++, and if you check the wxWidgets documentation for wxFrame (which is what a Frame is under the hood), it tells us that the inheritance hierarchy looks like the following (read ‘->’ as “inherits from”):

wxFrame -> wxTopLevelWindow -> wxWindow -> wxEvtHandler -> wxObject

This, in OO C++ jargon, means that a wxFrame is-a wxEvtHandler. Another way of saying this is that you can treat a wxFrame as a wxEvtHandler.

That’s all very well for C++, but we are working in Haskell, and Haskell doesn’t have objects. It turns out, though, that we can model the hierarchy of an OO design using types (although the approach doesn’t really support multiple inheritance very well).

If you go to the documentation for Frame (in Graphics.UI.WXCore.WxcClassTypes), you will find that the type of Frame turns out to be:

type Frame a = TopLevelWindow (CFrame a)
type TFrame a = TTopLevelWindow (CFrame a)
type CFrame a

This looks a little like the C++ inheritance hierarchy. Checking further:

type TopLevelWindow a = Window (CTopLevelWindow a)
type TTopLevelWindow a = TWindow (CTopLevelWindow a)
type CTopLevelWindow a
type Window a = EvtHandler (CWindow a)
type TWindow a = TEvtHandler (CWindow a)
type CWindow a

Haskell being a nice predictable and pure language, we can do some substitutions, and it turns out that our Frame () can be expressed as:

Frame () = EvtHandler ( CFrame () )

Since the CFrame a constructor doesn’t really do anything (it is called a “type witness”), all of this means that our Frame () is-a EvtHandler a, and we can use it in evtHandlerOnMenuCommand.

The other aspect of the event handlers is that, at least in this case, no useful information is provided by the event, so if you need to pass any information to the event handler, you most likely need to do this by currying the event handler function. In this case, while evtHandlerOnMenuCommand expects an event handler to have type IO (), the event handlers for the menu in this application typically include a curried guiCtx parameter which holds information about the controls and files used in the application.

> evtHandlerOnMenuCommand win wxID_OPEN $ openPage guiCtx
> evtHandlerOnMenuCommand win wxID_SAVE $ savePage guiCtx
> evtHandlerOnMenuCommand win wxID_SAVEAS $ savePageAs guiCtx
> evtHandlerOnMenuCommand win wxID_CLOSE $ windowClose win False >> return ()

The remainder of the function is pretty similar to step 1, except that we need to add the menu bar to the Frame by setting the menuBar property.

> set win [menuBar := [mnuFile]]
> set win [layout := fill $ widget editor, clientSize := sz 640 480]
> focusOn editor
> set win [visible := True]

Event Handlers

Much of the functionality of a GUI application in most widget toolkits is executed in response to events. A wxHaskell application is no exception to this rule, and much of the logic of any application you develop will reside in its event handlers.

The event handling functions below all have type GUIContext -> IO (), with the GUIContext carrying shared state information.

The openPage function is responsible for loading a file into the editor pane. In common with most GUI toolkits, wxHaskell provides the ability for the programmer to use standard dialog boxes for key functions such as loading and saving files, and some of the convenience functions provided make the use of these pretty straightforward. The fileOpenDialog function has the following signature:

fileOpenDialog :: Window a -> Bool -> Bool -> String
               -> [(String, [String])] -> FilePath
               -> FilePath -> IO (Maybe FilePath)

The parameters, in order, have the following purposes:

  • Parent window identifier
  • Bool which, if true, indicates that the application should remember the last selected directory.
  • Bool which, if true, indicates that read-only files can be selected
  • String containing the text for the dialog box frame
  • A list of tuples which define the filename selections offered by the dialog box. Each tuple (String, [String]) is a pair in which the first element is a String containing a description of the file type and the second element is a list of wildcard patterns denoting acceptable file extensions.
  • A String indicating the path to the initial directory (often set as the empty string, which indicates no default directory)
  • A String containing the default filename to choose (also often set to the empty string, which indicates no initial selection)

The result of invoking fileOpenDialog is a Maybe FilePath. If we have Nothing, then no file was selected and we simply return without doing anything.

If we have a Just path in which path is the complete path to the selected file, then we load the selected file into the text control, set the title of the Frame to include the filename loaded in the text control and remember the file location in the filePath mutable variable.

There are two ways in which you could load text into the TextCtrl, guiEditor. The first is to use normal Haskell file handling functions to load the file contents into a string and assign the text property of guiEditor to contain the string. The second, used here, is to use the WXCore function textCtrlLoadFile. I’ll also point out, once more, that the text property of a Frame (in this case guiWin) sets the text at the top of the Frame window.

> openPage GUICtx{guiWin = win, guiEditor = editor, guiFile = filePath} =
> do
>   maybePath <- fileOpenDialog win True True "Open file..."
>                               [("Haskells (*.hs)",["*.hs"]),
>                                ("Texts (*.txt)", ["*.txt"]),
>                                ("Any file (*.*)",["*.*"])]
>                               "" ""
>   case maybePath of
>     Nothing   -> -- The user cancelled... nothing to do
>                  return ()
>     Just path -> do
>                  textCtrlLoadFile editor path
>   set win [text := "wxhnotepad - " ++ path]
>   varSet filePath $ Just path

The function to save a file is very similar in structure to openPage. There is a convenience function fileSaveDialog which lets you select the name of a saved file using the standard file dialog for your platform, and a function textCtrlSaveFile which lets you save the contents of a text control to a file.

> savePageAs GUICtx{guiWin = win, guiEditor = editor, guiFile = filePath} =
> do
>   maybePath <- fileSaveDialog win True True "Save file..."
>                               [("Haskells (*.hs)",["*.hs"]),
>                                ("Texts (*.txt)", ["*.txt"]),
>                                ("Any file (*.*)",["*.*"])]
>                               "" ""
>   case maybePath of
>     Nothing   -> return ()
>     Just path -> do
>                  textCtrlSaveFile editor path
>                  set win [text := "wxhnotepad - " ++ path]
>                  varSet filePath $ Just path

The savePage function is a simple wrapper around savePageAs for the case when we want to save a file to the existing filename. The current filename is fetched from the filePath mutable variable. We use a guard to verify that we do, in fact, have a filename set allowing the save operation to be executed, and if we do not, we call savePageAs to pull up a dialog box.

> savePage guiCtx@GUICtx{guiWin = win, guiEditor = editor, guiFile = filePath} =
> do
>   maybePath <- varGet filePath
>   case maybePath of
>     Nothing   -> savePageAs guiCtx
>     Just path -> textCtrlSaveFile editor path >> return ()

An interesting reader exercise would be to modify the menu so that the savePage menu is only enabled when a filename is set. You will probably want the menuItemEnable function to assist in this modification. In this case the menu item would only be available when we have a filename, and the call to varGet filePath should always return a filename.

That completes step 2 of the text editor. Next time we will add undo / redo functionality.

Categories: wxHaskell Tags:

Building a text editor (Part 1)

January 31, 2010 5 comments

Edit: added cabal install information. Thanks Slaava!

This is the first post in a new series, aimed at wxHaskell beginners.

I did not write the code in this post. It was written by Fernando Benavides especially for this blog, and I’m extremely grateful for his help. The code is BSD3 licensed and under his copyright. You can fetch it as a cabal package (update: cabal install wxhnotepad), which is the best way to follow this tutorial. The fact that Fernando wrote the code means that in case of any disagreement between my commentary and the code, you should believe the code 🙂

I should also mention that Fernando is the author of λpage, which he describes as a Haskell scrapbook – you can think of it as a GUI-based and more featureful GHCi. It has several neat features: Hayoo integration and the ability to determine the kind of types are two stand-outs for me. I enjoyed checking it out, and find myself using it quite often. See what you think.

The first step in the tutorial is just about the most minimal wxHaskell application imaginable: a text editor without the ability to load or save text, no search and no replace. It’s a good start though, and in the next few posts it will develop into a usable notepad replacement.

Fernando has written the code in a particularly neat way – there are actually six versions of the editor, and you can call any of them from the main launcher screen. We’ll start with the launcher, since it is the main module for the program.

The Launcher

There are two libraries which make up wxHaskell:

  • WXCore, a fairly thin wrapper around wxWidgets (the C++ library which does most of the rendering). The functions in WXCore tend to require a style of programming similar to C++, although we at least benefit from Haskell type inference when using them.
  • WX, a higher-level library which enables a more declarative style of programming. As a general rule, you will probably want to use WX functions whenever possible and drop down to WXCore when WX does not do what you need. You will actually need to do this relatively often, as there are quite a few useful features in WXCore which do not have equivalents in WX.

At the top of any wxHaskell program, you will almost certainly want to import WXCore and WX, so:

> module Main where
> import Graphics.UI.WX
> import Graphics.UI.WXCore
> import Step1
> import Step2
> import Step3
> import Step4
> import Step5
> import Step6

Like every other Haskell program, wxHaskell applications start with main.

In this case we have a very simple main program, but in a more complex application you might have come command line option handling, configuration file handling or similar. However, the essential part of a wxHaskell application is a call to start with a sequence of IO actions which make up the GUI logic of your program.

For wxHaskell application programming, that’s all you really need to know about start-up, but for the curious (or those who have done some wxWidgets programming in C++), what is happening under the hood is that start creates an instance of a wxHaskell application class derived from wxApp and this instance uses the closure represented by gui in the implementation of wxApp::onInit().

> main :: IO ()
> main = start gui

The launcher is very simple. It consists of a Frame which has a menu and nothing else.

Any wxHaskell application requires a top level container window. This is almost always a Frame, although it is possible to use a Dialog as a container if you have a very simple form-type GUI in mind. If you want a menu, you will definitely need a Frame, and we want a menu.

It is worth a closer look at how the Frame is created and configured, since this is a model for creating many windows and controls in wxHaskell. The wxHaskell documentation for frame says that is has type frame :: [Prop (Frame ())] -> IO (Frame ()), and that it creates a top level frame window.

What does this [Prop (Frame ())] mean? Well, wxHaskell has a mechanism which lets you perform much of the configuration of the different window and widget types in a declarative fashion by setting (or reading back) Attributes. This is much nicer than mucking about with lots of individual functions, and tends to create code which is clear and easy to understand.

The precise meaning of [Prop (Frame ())] is that you may provide a list of Properties which are applicable to a Frame. Most of these properties have been grouped into typeclasses, and the documentation usually indicates which are supported by a given type (the usually here should be taken to say that if you try this for Frame, you will be disappointed to find out that the documentation doesn’t mention this at all…)

  • The text attribute is typically used to manipulate some form of meaningful text in a control. In the case of a Frame it sets the text in the window frame; in the case of a static text or text control it sets the text in the control.
  • The visible attribute is supported by most controls – it determines whether the control is visible or not.
  • The on attribute is more interesting. It is typically followed by an event name, and the pair is used to configure an event handler.

Thus, the short snippet of code below creates a Frame with the caption “wxhNotepad”, which is initially invisible and which calls a function, wxcAppExit when the Frame closing event is fired.

One other point of note: the set function can be used to set or change the properties of a window or control which has already been created (there is a related get function for reading values back as well). In this case we could have put the closing event handler into the property list we used when creating the frame, but in some cases (e.g. when you need to know the control identity to configure things correctly), you will need to use this
two (or more) phase approach.

 > gui :: IO ()
 > gui =
 >   do
 >     win <- frame [text := "wxhNotepad", visible := False]
 >     set win [on closing := wxcAppExit]

The say function is a convenience for launching a small information dialog box.

 > let say title desc = infoDialog win title desc

The main purpose of the launcher is to support the menu which lets you use the editor in each of its stages of development. There are a couple of ways of creating a menu (we’ll see the lower-level way later).

The simplest is to use menuPane to create a menu heading (e.g. ‘File’), menuItem to create an item in a menu (e.g. ‘Open’) and menuBar to set the menu for a frame. There are convenience functions for certain menu entries, notably the ‘Help’, ‘About’ and ‘Quit’ menu items, as these may require special handling on certain OS platforms, and using the convenience function ensures that you do this in the way expected on each OS.

It is worth quickly noting that for menu items, the text property is used for the menu text. Notice also that menu shortcuts and accelerators can be defined in the menu text itself. The wxWidgets documentation for wxMenu::Append() describes this format in detail.

 > mnuSteps <- menuPane [text := "Steps"]
 > menuItem mnuSteps [on command := step1,
 >    text := "Step &1 - Just a Text Field\tCtrl-1"]
 > menuItem mnuSteps [on command := step2,
 >    text := "Step &2 - Open / Save / Save As...\tCtrl-2"]
 > menuItem mnuSteps [on command := step3,
 >    text := "Step &3 - Undo / Redo...\tCtrl-3"]
 > menuItem mnuSteps [on command := step4,
 >    text := "Step &4 - Cut / Copy / Paste...\tCtrl-4"]
 > menuItem mnuSteps [on command := step5,
 >    text := "Step &5 - Find / Replace...\tCtrl-5"]
 > menuItem mnuSteps [on command := step6,
 >    text := "Step &6 - Toolbar / Statusbar / Context Menus\tCtrl-6"]
 > menuQuit mnuSteps [on command := wxcAppExit]
 > mnuHelp <- menuHelp []
 > menuAbout mnuHelp [on command := say "About wxHNotepad"
 >       "Author: Fernando Brujo Benavides\nWebsite:"]

Once we have created a menu, it needs to be assigned to our Frame, and this is done with the menuBar property mentioned earlier. Notice that the menuBar takes a list of menuPane instances.

We finally make the Frame visible. There’s a neat trick to note here: what we are trying to do is to create a Frame which contains nothing but a menu. This is no problem on OSX, but Windows and Linux don’t really support it. However, if you set the clientSize property of the Frame to (0,0) then you get much the same effect.

 > set win [menuBar := [mnuSteps, mnuHelp],
 > visible := True, clientSize := sz 0 0]

Text editor: step 1

This is the absolutely minimal, first stage text editor. It consists of nothing more than a text control inside a Frame. No menu. No dialogs and no real functionality. Perhaps the only aspect of interest is to note that
it is easy to make a wxHaskell application with more than one Frame, should you wish to.

Since this is the second time we have mentioned clientSize, it is probably worth a moment to explain what this means. There are several different ways to look at the size of a window. Two of the
most important are:

  • clientSize The size available for displaying information inside a window – i.e. if the clientSize is (640, 480), this means that there are 640 x 480 (width x height) pixels available for displaying information. Borders, window decorations and so on take up additional space.
  • virtualSize The size which would be needed to display all of the information contained in the window. Often the clientSize and virtualSize are the same, but if there is more information in the window than can be displayed fully (i.e. if you need a scroll bar) then the clientSize will be smaller than the virtualSize.

There is one new call in the snippet below: textCtrl constructs a text control. As parameters it requires a parent window (which in this case will be the Frame, win) and a list of properties. All non top-level windows require their parent as a parameter, since wxWidgets keeps windows in a hierarchy rooted at the top-level.

 > step1 =
 > do
 > win <- frame [text := "wxhNotepad - Step 1", visible := False]
 > editor <- textCtrl win [font := fontFixed,
 >           text := "This is our first step in the " ++
 >           "developing of our text editor.\n" ++
 >           "Just a big text area where " ++
 >           "the user can read and write text\n" ++
 >           "That's not much, but it's just the " ++
 >           "beginning..."]

In the following snippet, we use the Layout mechanism to help us to fit our textCtrl inside the frame.

Layout is a conceptually elegant mechanism which allows you to describe the way in which the windows in an application are arranged and organized in a declarative manner.

The reality is that some aspects of the way that Layout works are rather complex and subtle (so much so that Layout will probably be the subject of a blog article or two of its own in the near future), but in this demonstration, Layout will work very well for us. I’d just ask that you don’t get discouraged if you have issues with more complex arrangements of controls using Layout.

Layout is specified using the layout property. It consists of a sequence of combinator functions which must ultimately resolve to a Attr w Layout type. For the moment, we just need to know that:

  • widget requires a control as a parameter. It indicates how the specified control should be placed in the Layout.
  • fill is a Layout transformer which indicates that the Layout to which it is applied should stretch and expand into the available area.

The documentation for Layout will give a better feel for what is happening, if you would like to understand more, but please bear in mind that this is a fairly complex subject, since what Layout is actually doing is to create sizer instances from the Layout specification, and sizers are a fairly large subject in themselves.

 > set win [layout := fill $ widget editor,
 >          clientSize := sz 640 480]
 > focusOn editor
 > set win [visible := True]
Categories: wxHaskell Tags:

Custom Controls in wxHaskell (part 5)

January 19, 2010 Leave a comment

Window Painting

Most of the windows in the control will use their default paint functions, but since the two viewports for outputting diff information are based on a Window type, they will require special handling.

The behaviour required in a Window is slightly different depending on whether the original file or the changed file is being rendered. To indicate, we have a specific type, which
is curried into the selected on paint handler for each viewport.

 > data DVFileType = DVorig | DVchanged deriving Show

Now we can define a suitable custom paint handler. On entry, win is the viewport window, DC is a suitable device context which is provided by the framework and the rectangle represents the minimum area which needs to be updated.

When performing actions which might take a while (even for quite small values of ‘a while’), it is good practice to display a busy cursor

 > onPaint parent file_type win dc r@(Rect x y w h) =
 >     wxcBeginBusyCursor >>
 >     getDisplayableText parent >>= \render_info ->
 >     renderDiff file_type parent win dc render_info >>
 >     wxcEndBusyCursor

The getDisplayableText function was described in the previous installment, and is responsible for working out which text to render. In this installment, we are more interested in the specifics of rendering the text.

The renderDiff function is responsible for rendering text to the device context which was passed in via the onPaint function. To do this effectively, we need to determine the space taken up by the text characters. Since this depends on the selected font, point size, whether bold and/or italic text is required and even (although not relevant to this example) the device to which the text is being rendered – the sizing may be slightly different on a printed page compared to the display, for example – wxHaskell provides a function, getFullTextExtent, which will help you to obtain this information:

getFullTextExtent :: DC a -> String -> IO (Size, Int, Int)

What getFullTextExtent does is to determine the dimensions of a given string using the currently selected font (including bold/italic, point size etc.). The Size parameter gives the rectangle from the font baseline into which the string will render. The first integer is the descent (i.e. the space taken below the font baseline by the descenders on characters such as y and p. The second integer is any additional vertical space (external leading) – this is usually zero.

For optimum display of text, you should really call getFullTextExtent on each string to render. This is relatively slow, but accurate. In this case, however, we don’t really care about the absolute accuracy, and are optimizing for use with monospace fonts. I use a small function, getFontMetrics which can be useful where speed of rendering is more important than precision.

The basic idea is to call getFullTextExtent on some different characters (I use “W”, “y” and “l”) to determine the (probably) maximum space required to render a single character in a given font. This works well in parctice on most fonts, although I would expect it to be unsuitable for some of the more fancy font designs.

With this in mind, the operation of renderDiff is fairly straightforward. All text is rendered starting from (0, 0) in the client area (remember – we are managing scroll bars manually), clipping any initial characters which are not required (clipText) and starting each new line the maximum number of vertical pixels required by the font plus an optional, user-defined vertical space (to separate the lines a little more, for enhanced readability).

The text to display (a list of String) is zipped with the y axis positions for each line of text (y_posns).

 > renderDiff file_type parent win dc (RI diff_lines fst_col width) =
 >     dvGetState parent >>= \(DVS _ _ _ _ _ _ _ _ (DVF fs col bgcol spc) _ ) ->
 >     getFontMetrics win fs >>= \(FontMetrics x_max y_max desc el) ->
 >     get win clientSize >>= \(Size w h) ->
 >     set win [ color := col, bgcolor := bgcol ] >>
 >     set dc [ font := fs ] >>
 >     let disp_diff = map clipText diff_lines
 >         x_pos     = 0
 >         y_posns   = map (\n -> n * (y_max + desc + spc)) [0..]
 >         diff_pos  = zip disp_diff y_posns in
 >     mapM_ (\(diff, y_pos) -> renderLine file_type diff (Point x_pos y_pos) dc) diff_pos
 >         where
 >           clipText (DiffLine t s) = DiffLine t (take width (drop fst_col s))

The renderLine function renders a single line, selecting the appropriate renderer, and depending on whether this is the original or the updated file.

 > renderLine file_type (DiffLine t str) point dc =
 >     (getRenderer t) file_type str point dc

Determine which renderer function to use. There are four options: text we do not want to render at all is rendered using nullRenderer; text which has been added is rendered using addRenderer; text which has been deleted is rendered using delRenderer and text which is the same in both files is rendered using ctxtRenderer.

 > getRenderer OrigFileLine   = nullRenderer
 > getRenderer ChangeFileLine = nullRenderer
 > getRenderer RangeLine      = nullRenderer
 > getRenderer AddLine        = addRenderer
 > getRenderer DeleteLine     = delRenderer
 > getRenderer CommonLine     = ctxtRenderer
 > getRenderer _              = nullRenderer

Render Add line. This is presently configured to print added text (i.e. text which is present in the modified file but not the original) in green. Note that the current implementation uses the somewhat unsatisfactory approach of using silver ‘#’ characters to indicate text which is not present. This is an obvious area for improvement.

 > addRenderer DVorig _ point dc =
 >     let spaces = replicate 100 '#' in
 >     setTextColours dc colour_silver colour_silver >>
 >     drawText dc spaces point []
 > addRenderer DVchanged txt point dc =
 >     setTextColours dc colour_green colour_white >>
 >     drawText dc txt point []

Render Delete line. This is presently configured to show deleted text (present in original but not in modified file) in red.

 > delRenderer DVorig txt point dc =
 >     setTextColours dc colour_red colour_white >>
 >     drawText dc txt point []
 > delRenderer DVchanged _ point dc =
 >     let spaces = replicate 100 '#' in
 >     setTextColours dc colour_silver colour_silver >>
 >     drawText dc spaces point []

Render Unchanged line

 > ctxtRenderer _ txt point dc =
 >     setTextColours dc colour_black colour_white >>
 >     drawText dc txt point []

Null renderer can be used when there is nothing to render.

 > nullRenderer _ _ _ _ =  return ()

Helper function for setting text colours

 > setTextColours dc fg bg = dcSetTextForeground dc fg >>
 >                           dcSetTextBackground dc bg

The virtual canvas

The control implementation is almost complete. The only thing which remains is to complete the management of the scroll bars and the associated virtual canvas size. These simplify scrolling calculations, and are re-calculated when the diff text changes.

Given the font metrics for a device context on a file display windows we can work out the size of a virtual window which would be large enough to contain the entire diff text. Notice that this implementation only really works for monospace fonts.

We set both of the diff client windows to have the virtual size of this notional canvas and we work within a viewport onto this virtual canvas. We also configure the scroll bar
so that they range over the virtual canvas correctly. The key aspect here is that the scroll bars are resized depending on the proportion of the underlying virtual canvas which can be displayed in the client window

 > setVirtualCanvas w =
 >     dvGetState w >>= \(DVS _ _ _ vp1 vp2 vs hs _ (DVF fs _ _ spacer) m) ->
 >     get vp1 clientSize >>= \cs1 ->
 >     get vp2 clientSize >>= \cs2 ->
 >     getFontMetrics vp1 fs >>= \(FontMetrics x_max y_max desc el) ->
 >     let textSize    = calcTextSize m
 >         char_height = y_max + desc + spacer
 >         char_width  = x_max + el
 >         canvas_sz   = sz (char_width * (sizeW textSize)) (char_height * (sizeH textSize)) in
 >     set vp1 [ virtualSize := canvas_sz ] >>
 >     set vp2 [ virtualSize := canvas_sz ] >>
 >     adjustScrollbars w

Adjust the scroll bars to represent the correct view over the data in the viewport. For the purposes of calculation, assume that both viewports are the same size (they
will be clipped anyway, so worst case is that we do a little more work than strictly required).

Scroll bars are configured with a range (number of rows/columns of text) and a thumb size (number of rows/columns visible in the viewport). We take care to save the current thumb position before adjusting.

 > adjustScrollbars w =
 >     dvGetState w >>= \(DVS _ _ _ _ _ vs hs _ _ map) ->
 >     calcViewportTextSize w >>= \(Size x_cs_txt y_cs_txt) ->
 >     let (Size x_txt y_txt) = calcTextSize map in
 >     -- Save scroll position before updating
 >     scrollBarGetThumbPosition vs >>= \vs_pos ->
 >     scrollBarGetThumbPosition hs >>= \hs_pos ->
 >     scrollBarSetScrollbar vs vs_pos y_cs_txt y_txt y_cs_txt True >>
 >     scrollBarSetScrollbar hs hs_pos x_cs_txt x_txt x_cs_txt True


I chose to define helpers to allow me to use a small subset of the standard colour names/spaces defined in the W3C CSS specification.

 > colour_silver  = colorRGB 192 192 192
 > colour_black   = colorRGB   0   0   0
 > colour_green   = colorRGB   0 128   0
 > colour_white   = colorRGB 255 255 255
 > colour_red     = colorRGB 255   0   0
Categories: wxHaskell Tags: ,

Custom Controls in wxHaskell (part 4)

January 13, 2010 1 comment

Deciding what to render

Our DiffCtrl has two viewport Window instances on which we will eventually be rendering the diff text. In this section, we consider what actually needs to be rendered.

The background is that most GUI toolsets support the concept of a viewport which provides a window over all of the data in a window. There is too much information to be able to fit everything into the area available to display, so we see only a part of it. In wxWidgets (and therefore wxHaskell), the key ideas are the virtualSize which represents the size a window would need to be to fit all of the displayable information into it and the clientSize which is the size actually available for displaying information in the application.

One option available to us is to simply render all of the information into the virtual canvas. This is fine, and works OK for controls which have automatic management of scroll bars (incidentally, if you do not set the virtualSize and clientSize parameters for such controls, Layout often fails since any size control size up to the virtualSize is possible and the Layout algorithm has a tendency to decide that something like a 1 x 1 control size is a good choice – almost certainly not what you had in mind).

There is a problem with rendering everything to the virtual canvas, and that is that rendering can take a while. If you have a very large data set, and a small window on it, you are expending processing time just to throw away most of what you did (because wxWidgets only displays the part of the rendered information which can be seen on the client Window). For reasons of responsiveness, many applications therefore don’t bother to render what you cannot see, and only render what is visible.

In this application, we are managing what is displayed in Windows manually, as well as keeping manual control of the scroll bars. Therefore, we really have to take care about what is going to be rendered to the Window instances ourselves.

A digression: unified diff output

Below is a fragment of the output from a unified diff command:

--- /path/to/original ''timestamp''
+++ /path/to/new      ''timestamp''
@@ -1,3 +1,4 @@
+This is an important notice! It should therefore be located at the beginning of this document!
 This part of the document has stayed the same from version to
@@ -5,16 +11,10 @@
 be shown if it doesn't change.  Otherwise, that would not be helping to
-compress the size of the changes.Determine the viewable text, i.e. the text which should be displayed in the viewport
  • The — prefix indicates that the line contains the name and timestamp of the ‘original’ file in the diff.
  • The +++ prefix indicates that the line contains the name and timestamp of the ‘modified’ file in the diff.
  • The @@ prefix indicates which lines have changed.
  • The + prefix indicates a line which has been added to the original to create the modified file.
  • The – prefix indicates a line which has been deleted from the original to create the modified file.
  • Lines starting with a space character are unchanged between the original and modified files.

It is worthwhile to point out that we don’t want to render filename, timestamp or line number information in the Window. Therefore, the number of lines of information returned from the diff command is not the same as the number of lines we need to render.

We can express this with a suitable data type:

> data DiffLineType = CommonLine
>                   | AddLine
>                   | DeleteLine
>                   | RangeLine
>                   | OrigFileLine
>                   | ChangeFileLine
>                   | UnknownLine
>                     deriving (Eq, Show)

It is very straightforward to determine the type of information represented in a diff line. It is probably an error if we don’t have one of the prefixes given above. Since I’m not sure how I will deal with such an error for now, UnknownLine has been created.

> getDiffLineType :: String -> DiffLineType
> getDiffLineType ('-':'-':'-':_) = OrigFileLine
> getDiffLineType ('+':'+':'+':_) = ChangeFileLine
> getDiffLineType ('@':'@':_)     = RangeLine
> getDiffLineType ('+':_)         = AddLine
> getDiffLineType ('-':_)         = DeleteLine
> getDiffLineType (' ':_)         = CommonLine
> getDiffLineType _               = UnknownLine

Now we have everything we need to parse the raw output from the diff command into something a little easier to work with. The lines from a diff run are represented in a data type containing the line type and the remaining text.

> data DiffLine = DiffLine DiffLineType String
>                 deriving Show

Parse diff lines into DiffLine data structures.

> parseDiffLines = map parseDiffLine
>     where
>       parseDiffLine s =
>           case getDiffLineType s of
>             OrigFileLine   -> DiffLine OrigFileLine   (drop 3 s)
>             ChangeFileLine -> DiffLine ChangeFileLine (drop 3 s)
>             RangeLine      -> DiffLine RangeLine      (drop 2 s)
>             AddLine        -> DiffLine AddLine        (tail s)
>             DeleteLine     -> DiffLine DeleteLine     (tail s)
>             CommonLine     -> DiffLine CommonLine     (tail s)
>             UnknownLine    -> DiffLine UnknownLine    (tail s)

It is now straightforward to determine whether a line from the diff command should be displayed in the viewport or not. Displayable returns True if a line is displayable in a viewport.

 > displayable AddLine    = True
 > displayable DeleteLine = True
 > displayable CommonLine = True
 > displayable _          = False

We are now in a position to determine how many lines of displayable text the diff command generated for us.

> countDiffLines map = Map.fold countDiffLines' 0 map
>     where
>     countDiffLines' (DiffLine typ _) acc | displayable typ = acc + 1
>                                          | otherwise       = acc

What can I show you today?

This is basically a question of working out the row and column of the displayable lines of text at which we start display, and the row and column at which we finish display.

Working out which columns to display is the more straightforward calculation, so I chose to represent this in a RenderInfo type which contains a line of diff text, the first column to be displayed and the length of text to display. Note that when making these calculations, if the calculations as integers are inexact (they often are), just render slightly more information than your display area. The surplus will be clipped, but it will clip showing (for example) partial characters, which is what you want.

In passing, I should note that since the first column and maximum to render are the same for all lines of text, I could probably have done without this data type.

 > data RenderInfo = RI { ri_txt      :: ![DiffLine]  -- | Text to render
 >                      , ri_firstcol :: !Int         -- | First column to be used in text
 >                      , ri_length   :: !Int         -- | Length of text to render
 >                      } deriving Show

The getDisplayableText function uses calcViewportTextExtent to determine the size of a rectangle (in rows and columns of characters) on which text can be displayed, and the selectLines function to select the correct number of displayable lines.

 > getDisplayableText win =
 >     calcViewportTextExtent win >>= \r@(Rect x _ w _) ->
 >     dvGetState win >>= \(DVS _ _ _ _ _ _ _ _ _ map) ->
 >     let (_, diff_lines) = Map.fold (selectLines r) (0,[]) map
 >         disp_diff  = RI diff_lines x w in
 >     return disp_diff

The selectLines function selects those lines which will be displayed on the viewport, based on the following criteria:

  • displayable lines with line number < y are above the viewport and not displayed
  • displayable lines between line number y and line number y + h are displayed
  • other lines are not displayed

In addition, note that as not all lines are displayable, we need to keep a separate account of the displayable line number as we iterate over the map. This is why we use (Int, [DiffLine]) as our accumulator, rather than just [DiffLine].

 > selectLines :: Rect -> DiffLine -> (Int, [DiffLine]) -> (Int, [DiffLine]
 > selectLines (Rect _ y _ h) (DiffLine typ str) acc@(disp_line_no, strs)
 >                 | (disp_line_no < y)     && (displayable typ) = (disp_line_no + 1, strs)
 >                 | (disp_line_no < y + h) && (displayable typ) = (disp_line_no + 1, (DiffLine typ str) : strs)
 >                 | otherwise                                   = acc

As noted earlier, calcViewportTextExtent determines a rectangle over the complete displayable diff text which will be displayed. This is straightforward: the scroll bars give the starting row/column from their current position. We know the end row/column by calculating the viewport text size. Thus we obtain a starting row/column and an end row/column to display.

I should note that the viewport text size is the number of rows and columns of text which will fit in the viewport. This is dependent on the size of text and font used, which is why it has been calculated separately from working out which rows/columns of text to display.

 > calcViewportTextExtent w =
 >     dvGetState w >>= \(DVS _ _ _ _ _ vs hs _ _ map) ->
 >     scrollBarGetThumbPosition vs >>= \top_left_y ->
 >     scrollBarGetThumbPosition hs >>= \top_left_x ->
 >     calcViewportTextSize w >>= \(Size bot_right_raw_x bot_right_raw_y) ->
 >     let (Size max_x max_y) = calcTextSize map
 >         width  = min bot_right_raw_x (max_x - top_left_x)
 >         height = min bot_right_raw_y (max_y - top_left_y)
 >         textExtent  = Rect top_left_x top_left_y width height in
 >     putStrLn ("calcViewportTextExtent textExtent:" ++ (show textExtent)) >>
 >     return textExtent

Calculate the number of rows and columns required to completely fit the diff text (i.e. the number of lines and the length of the longest line).

 > calcTextSize :: Map.Map Int DiffLine -> Size
 > calcTextSize m = sz (longestString m) (countDiffLines m)
 >     where
 >       longestString m = Map.fold (\(DiffLine _ str) max_now -> max (length str) max_now) 0 m

Calculate how much text we can fit into the viewport. This is straightforward: on each axis, the ratio of virtual canvas extent to viewport canvas extent (i.e. in device context units) is the same as the ratio of the total number of rows/columns to the viewable rows/columns.

Notice that the virtualSize is given a minimum size of (1,1). This is to ensure that no divide by zero error is possible when calculating x_cs_txt and y_cs_txt.

 > calcViewportTextSize w =
 >     dvGetState w >>= \(DVS _ _ _ vp1 _ _ _ _ _ map) ->
 >     get vp1 clientSize  >>= \(Size x_cs y_cs) ->
 >     get vp1 virtualSize >>= \(Size x_vs y_vs) ->
 >     let (Size x_vs_txt y_vs_txt) = calcTextSize map
 >         x_vs'    = max 1 x_vs
 >         y_vs'    = max 1 y_vs
 >         x_cs_txt = min ((x_cs * x_vs_txt) `div` x_vs') x_vs_txt
 >         y_cs_txt = min ((y_cs * y_vs_txt) `div` y_vs') y_vs_txt
 >         cs_txt   = sz x_cs_txt y_cs_txt in
 >     return cs_txt
Categories: wxHaskell Tags: ,