7.1 Menus and Toolbars

There are specific APIs for Menus and toolbars, but you should usually deal with them together, using the UIManager to define actions which you can then arrange in menu and toolbars. Each action can be associated with several 'proxy widgets'. In this way you can handle activation of the action instead of responding to the menu and toolbar items separately. And you can enable or disable both the menu and toolbar item via the action.

actionNew ::
   String              --  name : a unique name for the action
-> String              --  label : what will be displayed in menu items and on buttons
-> Maybe String        --  tooltip : a tooltip for the action
-> Maybe String        --  stockId : the stock item to be displayed
-> IO Action

As you see, an action can be anything. When the user activates an action, through clicking on an associated widget or through an accellarator key (see later), a signal is emitted and you specify what happens with:

onActionActivate :: ActionClass self => self -> IO () -> IO (ConnectId self)

An Action has methods and attributes. For example, you can hide an action or make it insensitive with:

actionSetVisible :: ActionClass self => self -> Bool -> IO ()
actionSetSensitive :: ActionClass self => self -> Bool -> IO ()

However, actions are grouped together, and an action can only be visible (sensitive) if its group is visible (sensitive). Create a new action group with:

actionGroupNew :: String -> IO ActionGroup

The argument is the name of the ActionGroup and it is used when associating key bindings with the actions. To add actions to a group, when no accelerator key is used and no stock item:

actionGroupAddAction ActionClass action => ActionGroup -> action -> IO ()

If an accelerator key is used , or a stock item:

actionGroupAddActionWithAccel :: 
   ActionClass action => ActionGroup -> action -> Maybe String -> IO ()

If you use a stock item, the Maybe String argument should be Nothing. If you don't use a stock item, but you don't specify an accelerator, use Just "". Otherwise the string should be in a format that can be parsed (see later). You can set visibility and sensitivity of an ActionGroup with:

actionGroupSetVisible :: ActionGroup -> Bool -> IO ()
actionGroupSetSensitive :: ActionGroup -> Bool -> IO ()

As said, an action in a group can only be visible (sensitive) if both its own and its group attributes are set.

Now you can use these actions through binding them to more than one proxy widgets, for example in a menu as well as in a toolbar. Of course you can stick to just one widget, but the idea behind actions is reuse. You do this through a String in XML format.

The allowed XML elements are: ui, menubar, menu, menuitem, toolbar, toolitem and popup. The menuitem and toolitem elements require an action attribute, and this is set to the unique name you've given to the action when you created it. The menubar and toolbar elements can also have actions associated with them, but these are optional. All elements can have names, and these are also optional. Names are needed to distinguish widgets of the same type and the same path, for example two toolbars just beneath the root (the ui element).

Additionally you have separator, placeholder and accelerator elements. Separators appear as lines in tool bars and menu bars. Placeholders can be used to group elements and sub trees and accelerators define accelerator keys. The GTK+ reference points out that accelerator keys should not be confused with mnemonics. Mnemonics are activated through a letter in the label, accelerators are activated through a key combination you specify.

Note: Unfortunately the accelerators for action menus and toolbars do not seem to work as advertised. Whether this is due to GTK+, Gtk2Hs, the platform, or because I've missed something, is not clear to me. You'll just have to try it out!

The Graphics.UI.Gtk.ActionMenuToolbar.UIManager section in the API documentation contains a DTD (Document Type Definition) for the XML string, as well as some additional formatting information.

Here's an example of an XML String, which is used in the example below. The slashes at the end and beginning of each line are needed to tell GHCi and GHC that the string is continued there, and the quotes in the XML definition must also be escaped. The indentations have no special meaning here, of course.

 uiDecl = "<ui>\
\           <menubar>\
\            <menu action=\"FMA\">\
\              <menuitem action=\"NEWA\" />\
\              <menuitem action=\"OPNA\" />\
\              <menuitem action=\"SAVA\" />\
\              <menuitem action=\"SVAA\" />\
\              <separator />\
\              <menuitem action=\"EXIA\" />\
\            </menu>\
\           <menu action=\"EMA\">\
\              <menuitem action=\"CUTA\" />\
\              <menuitem action=\"COPA\" />\
\              <menuitem action=\"PSTA\" />\
\           </menu>\
\            <separator />\
\            <menu action=\"HMA\">\
\              <menuitem action=\"HLPA\" />\
\            </menu>\
\           </menubar>\
\           <toolbar>\
\            <toolitem action=\"NEWA\" />\
\            <toolitem action=\"OPNA\" />\
\            <toolitem action=\"SAVA\" />\
\            <toolitem action=\"EXIA\" />\
\            <separator />\
\            <toolitem action=\"CUTA\" />\
\            <toolitem action=\"COPA\" />\
\            <toolitem action=\"PSTA\" />\
\            <separator />\
\            <toolitem action=\"HLPA\" />\
\           </toolbar>\
\          </ui>"

All the action attributes are strings which were defined earlier, when the actions were created (see the complete source code below).

Now this definition or declaration must be processed by a ui manager. To create one:

uiManagerNew :: IO UIManager

To add the XML string:

uiManagerAddUiFromString :: UIManager -> String -> IO MergeId

Next the action groups, which have been created earlier, must be inserted:

uiManagerInsertActionGroup :: UIManager -> ActionGroup -> Int -> IO ()

If you only have one action group the position will be 0, otherwise you have to specify the index in the list you already have.

Now you can get all the widgets you want from your UIManager and the path (including names if necessary) in your XML definition.

uiManagerGetWidget :: UIManager -> String -> IO (Maybe Widget)

From the definition above, for example, we can get a menubar and a toolbar with:

maybeMenubar <- uiManagerGetWidget ui "/ui/menubar"
     let menubar = case maybeMenubar of
                        (Just x) -> x
                        Nothing -> error "Cannot get menubar from string." 
     boxPackStart box menubar PackNatural 0

     maybeToolbar <- uiManagerGetWidget ui "/ui/toolbar"
     let toolbar = case maybeToolbar of
                        (Just x) -> x
                        Nothing -> error "Cannot get toolbar from string." 
     boxPackStart box toolbar PackNatural 0

The packing has been included in the above snippet, to demonstrate that this still has to be done by you. This is the example with the code:

Menus and Toolbars

We've set one action to be insensitive, to show how it's done. We've also added an accelerator to the exit action, which takes the stockQuit stockitem, but now displays Ctl + E as the accelerator. According to the GTK+ reference manual, accelerator keys are defined as: <Control>a, <Shift><Alt>F1, <Release>z and so on. You'll have to see what GHCi accepts. As said before, accelerator keys are displayed, but don't work on my configuration. Note that in this example we've used the shorter mapM_ instead of the sequence_ and map combination of the previous chapters.

import Graphics.UI.Gtk

main :: IO ()
main = do
     initGUI
     window <- windowNew
     set window [windowTitle := "Menus and Toolbars",
                 windowDefaultWidth := 450, windowDefaultHeight := 200]

     box <- vBoxNew False 0
     containerAdd window box

     fma <- actionNew "FMA" "File" Nothing Nothing
     ema <- actionNew "EMA" "Edit" Nothing Nothing
     hma <- actionNew "HMA" "Help" Nothing Nothing

     newa <- actionNew "NEWA" "New"     (Just "Just a Stub") (Just stockNew)
     opna <- actionNew "OPNA" "Open"    (Just "Just a Stub") (Just stockOpen)
     sava <- actionNew "SAVA" "Save"    (Just "Just a Stub") (Just stockSave)
     svaa <- actionNew "SVAA" "Save As" (Just "Just a Stub") (Just stockSaveAs)
     exia <- actionNew "EXIA" "Exit"    (Just "Just a Stub") (Just stockQuit)
 
     cuta <- actionNew "CUTA" "Cut"   (Just "Just a Stub") (Just stockCut)    
     copa <- actionNew "COPA" "Copy"  (Just "Just a Stub") (Just stockCopy)
     psta <- actionNew "PSTA" "Paste" (Just "Just a Stub") (Just stockPaste)

     hlpa <- actionNew "HLPA" "Help"  (Just "Just a Stub") (Just stockHelp)

     agr <- actionGroupNew "AGR"
     mapM_ (actionGroupAddAction agr) [fma, ema, hma]
     mapM_ (\ act -> actionGroupAddActionWithAccel agr act Nothing) 
       [newa,opna,sava,svaa,cuta,copa,psta,hlpa]

     actionGroupAddActionWithAccel agr exia (Just "<Control>e")

     ui <- uiManagerNew
     uiManagerAddUiFromString ui uiDecl
     uiManagerInsertActionGroup ui agr 0

     maybeMenubar <- uiManagerGetWidget ui "/ui/menubar"
     let menubar = case maybeMenubar of
                        (Just x) -> x
                        Nothing -> error "Cannot get menubar from string." 
     boxPackStart box menubar PackNatural 0

     maybeToolbar <- uiManagerGetWidget ui "/ui/toolbar"
     let toolbar = case maybeToolbar of
                        (Just x) -> x
                        Nothing -> error "Cannot get toolbar from string." 
     boxPackStart box toolbar PackNatural 0

     actionSetSensitive cuta False

     onActionActivate exia (widgetDestroy window)
     mapM_ prAct [fma,ema,hma,newa,opna,sava,svaa,cuta,copa,psta,hlpa]

     widgetShowAll window
     onDestroy window mainQuit
     mainGUI
     
uiDecl=  "<ui>\
\           <menubar>\
\            <menu action=\"FMA\">\
\              <menuitem action=\"NEWA\" />\
\              <menuitem action=\"OPNA\" />\
\              <menuitem action=\"SAVA\" />\
\              <menuitem action=\"SVAA\" />\
\              <separator />\
\              <menuitem action=\"EXIA\" />\
\            </menu>\
\           <menu action=\"EMA\">\
\              <menuitem action=\"CUTA\" />\
\              <menuitem action=\"COPA\" />\
\              <menuitem action=\"PSTA\" />\
\           </menu>\
\            <separator />\
\            <menu action=\"HMA\">\
\              <menuitem action=\"HLPA\" />\
\            </menu>\
\           </menubar>\
\           <toolbar>\
\            <toolitem action=\"NEWA\" />\
\            <toolitem action=\"OPNA\" />\
\            <toolitem action=\"SAVA\" />\
\            <toolitem action=\"EXIA\" />\
\            <separator />\
\            <toolitem action=\"CUTA\" />\
\            <toolitem action=\"COPA\" />\
\            <toolitem action=\"PSTA\" />\
\            <separator />\
\            <toolitem action=\"HLPA\" />\
\           </toolbar>\
\          </ui>" </pre>"

prAct :: ActionClass self => self -> IO (ConnectId self)
prAct a = onActionActivate a $ do name <- actionGetName a
                                  putStrLn ("Action Name: " ++ name)