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