7.1 Menús y Toolbars (barras de herramientas)

Hay APIs específicos para menús y toolbars, pero normalmente trabajarás conjuntamente con ambos usando el UIManager para definir acciones que posteriormente situarás en menús y barras de herramientas. Cada acción se puede asociar con varios 'proxy widgets'. De este modo puedes gestionar la activación de la acción en vez de establecer una respuesta independiente a la acción de menú y al elemento del toolbar. Además puedes habilitar o deshabilitar ambos items con la acción.

actionNew ::
   String              --  nombre : nombre único para la acción
-> String              --  etiqueta : lo que será mostrado en los elementos del menú y en los botones
-> Maybe String        --  tooltip : un tooltip para la acción
-> Maybe String        --  stockId : el stock item que se mostrará
-> IO Action

Como puedes ver, una acción puede ser cualquier cosa. Cuando el usuario activa una acción, ya sea pulsando en el widget asociado o a través de una tecla de aceleración (lo vemos más adelante), se emite una señal. Se especifica lo que debe suceder con:

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

Una Action tiene métodos y atributos. Por ejemplo, puedes ocultar una acción o hacerla insensible con:

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

Sin embargo, las acciones de agrupan juntas, y una acción sólo puede ser visible (o sensible) si su grupo es visible (o sensible). Los grupos de acciones se crean con:

actionGroupNew :: String -> IO ActionGroup

El argumento es el nombre del ActionGroup y se usa cuando se asocian los key bindings con las acciones. Para añadir acciones a un grupo, cuando no se usan teclas de aceleración ni stocks items, usamos:

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

Si se usa una tecla de aceleración, o un stock item:

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

Si usas un stock item, el argumento Maybe String debe ser Nothing. Si no usas stock item, pero no especificas un acelerador, usa Just "". En cualquier otro caso la cadena debe estar en un formato que pueda ser analizado (lo veremos más adelante). Puedes establecer la visibilidad y sensibilidad de un ActionGroup con:

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

Como se ha dicho, una acción en un grupo sólo será visible (sensible) si lo es ella y el grupo al que pertenece.

Ahora puedes usar las acciones si las fijas (binding) a uno o más widgets proxy, por ejemplo en un menú y en un toolbar. Por supuesto que puedes asociarla a un único elemento, pero la idea tras el diseño de acciones es la reusabilidad. Puedes hacer esto mediante un String en formato XML.

Los elementos XML disponibles son los siguientes: ui, menubar, menu, menuitem, toolbar, toolitem y popup. Los elementos menuitem y toolitem requieren un atributo de acción, y este se establece en el nombre único que recibió la acción cuando fue creada. Los elementos menubar y toolbar también pueden tener acciones asociadas con ellos, pero son opcionales. Todos los elementos pueden tener nombres, y esos son también opcionales. Necesitamos los nombres para distinguir los widgets del mismo tipo y con la misma dirección, por ejemplo dos barras de herramientas justo por debajo de root (raíz) (dentro de los elementos ui (interfaz de usuario)).

Además dispones de elementos separadores, colocadores y aceleradores. Los separadores son líneas en las barras de herramientas y en las barras de menús. Los colocadores se utilizan para agrupar elementos y sub árboles y los aceleradores definen las teclas de aceleración. La referencia de GTK+ advierte que no deben confundirse los aceleradores con los mnemónicos. Los mnemónicos se activan a través de una letra en la etiqueta, mientras que los aceleradores se activan a través de una combinación de teclas que tú especificas.

Nota: Desafortunadamente los aceleradores para los menús y toolbars no me han funcionado como se anuncia. Quizá esto sea debido a GTK+, Gtk2Hs, la plataforma, o quizá debido a que no he entendido algo. En cualquier caso intentalo a ver si tienes más suerte.

La sección Graphics.UI.Gtk.ActionMenuToolbar.UIManager en la documentación de la API contiene un DTD (Document Type Definition - Definición de tipo de documento) para la cadena XML, así como alguna información adicional sobre su formato.

Aquí tienes un ejemplo de la cadena XML, que hemos usado en el ejemplo inferior. Las barras al principio y final de cada línea se necesitan para que GHCi y GHC sepan que la línea continúa y que debe entender las " como parte de la cadena y no como delimitadores de la misma. La indentación no tiene un especial significado aquí.

 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>"

Todos los atributos de la acción son cadenas que hemos definido antes, cuando hemos creado las acciones (puedes verlo en el listado completo del programa al final del texto).

Ahora la definición debe ser procesada por un gestor de ui. Para crear uno:

uiManagerNew :: IO UIManager

Para añadir la cadena XML:

uiManagerAddUiFromString :: UIManager -> String -> IO MergeId

Después, los grupos de acción, que ya han sido creados, deben ser insertados:

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

Si sólo tienes un grupo de acción, la posición será 0, en cualquier otro caso tienes que especificar el índice en la lista que ya tienes.

Ahora ya puedes conseguir todos los widgets que necesites de tu UIManager y la dirección (path) (incluyendo nombres si fuera necesario) en tu definición XML.

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

De la definición anterior, por ejemplo, poedemos conseguir una barra de manú y una barra de herramientas con:

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

El empaquetado ha sido incluído en el snippet anterior, para demostrar que esto debes hacerlo tú. Este es el ejemplo con el código:

Menus and Toolbars

Hemos colocado una acción como insensible, para mostrar como se hace. También hemos añadido un acelerador a la acción de salir (exit), que emplea el stockitem stockQuit pero ahora muestra el acelerador Ctrl + E. Según el manual de referencia de GTK+, las teclas de aceleración se definen: <Control>a, <Shift><Alt>F1, <Release>z, etcétera. Como ya he indicado, las teclas de aceleración se muestran, pero en mi configuración no funcionan. fíjate que en este ejemplo hemos usado mapM_ en vez de la combinación sequence_ y map de los capítulos precedentes.

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)