Home > wxHaskell > Building a text editor (Part 2)

Building a text editor (Part 2)

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:
  1. October 18, 2010 at 7:16 pm

    I’m having trouble – I think because of lazy IO. When I open a file the text doesn’t always seem to get to the editor window. I’ve tired Scrict IO but it still doesn’t seem to work. Any clues you can give me?

  2. October 19, 2010 at 12:43 pm

    It wasn’t lazy IO the problem was special characters – the notepad won’t open files with any special characters. I got rid of them then it was OK. Can I get rid of this restriction somehow?

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )


Connecting to %s

%d bloggers like this: