7.2 Menús emergentes (Popup), acciones Radio y acciones Toggle

Los Menús normalmente pertenecen a una ventana, pero pueden ser mostrados temporalmente como resultado de la pulsación de un botón del ratón. Por ejemplo, puede mostrarse un menú de contexto cuando el usuario pulsa el botón derecho de su ratón.

La disposición de un menú popup menu debe usar el nodo popup. Por ejemplo:

uiDecl = "<ui> \
\          <popup>\
\            <menuitem action=\"EDA\" />\
\            <menuitem action=\"PRA\" />\
\            <menuitem action=\"RMA\" />\
\            <separator />\
\            <menuitem action=\"SAA\" />\
\          </popup>\
\        </ui>"   

La construcción de un menú popup lleva los mismos pasos que la construcción de un menú o un toolbar.(pero... sigue leyendo). Una vez que has creado las acciones y las has puesto en uno o más grupos, creas el gestor de UI, le añades la cadena XML y le añades los grupos. Es el momento de extraer el(los) widget(s). En nuestro ejemplo de popup hemos creado 4 acciones con los nombres listados arriba. La ventana de popup no se muestra en un volcado de pantalla por lo que hemos omitido la imagen.

Como es un popup no hemos empaquetado el widget. Para mostralo, necesitamos la función:

menuPopup :: MenuClass self => self -> Maybe (MouseButton,TimeStamp)

Todo esto está en la documentación de la API referente al módulo Graphics.UI.Gtk.MenuComboToolbar.Menu. En el ejemplo, el menú aparece cuando pulsamos el botón derecho del ratón, el segundo argumento puede ser Nothing. La función es la misma que la que vimos en el capítulo 6.2. Aquí, sin embargo, podemos usar la ventana en vez de una caja de eventos.

onButtonPress window (\x -> if (eventButton x) == RightButton
                                    then do menuPopup (castToMenu pop) Nothing
                                            return (eventSent x)
                                    else return (eventSent x))

El único truco es que el widget devuelto por el gestor de ui es de tipo Widget y la función menuPopup necesita un argumento de un tipo que sea una instancia de MenuClass. Así que tenemos que usar:

castToMenu :: GObjectClass obj => obj -> Menu

Esta función también está documentada en la sección Graphics.UI.Gtk.MenuComboToolbar.Menu. El listado completo del ejemplo es:

import Graphics.UI.Gtk

main :: IO ()
main= do
     initGUI
     window <- windowNew
     set window [windowTitle := "Click Right Popup",
                 windowDefaultWidth := 250,
                 windowDefaultHeight := 150 ]

     eda <- actionNew "EDA" "Edit" Nothing Nothing
     pra <- actionNew "PRA" "Process" Nothing Nothing
     rma <- actionNew "RMA" "Remove" Nothing Nothing
     saa <- actionNew "SAA" "Save" Nothing Nothing

     agr <- actionGroupNew "AGR1" 
     mapM_ (actionGroupAddAction agr) [eda,pra,rma,saa]

     uiman <- uiManagerNew
     uiManagerAddUiFromString uiman uiDecl
     uiManagerInsertActionGroup uiman agr 0

     maybePopup <- uiManagerGetWidget uiman "/ui/popup"
     let pop = case maybePopup of 
                    (Just x) -> x
                    Nothing -> error "Cannot get popup from string"

     onButtonPress window (\x -> if (eventButton x) == RightButton
                                    then do menuPopup (castToMenu pop) Nothing
                                            return (eventSent x)
                                    else return (eventSent x))

     mapM_ prAct [eda,pra,rma,saa]

     widgetShowAll window
     onDestroy window mainQuit
     mainGUI

uiDecl = "<ui> \
\          <popup>\
\            <menuitem action=\"EDA\" />\
\            <menuitem action=\"PRA\" />\
\            <menuitem action=\"RMA\" />\
\            <separator />\
\            <menuitem action=\"SAA\" />\
\          </popup>\
\        </ui>"   

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

Hay otro modo de usar las acciones, sin crearlas específicamente, a partir del tipo de datos ActionEntry :

data ActionEntry = ActionEntry {
actionEntryName :: String
actionEntryLabel :: String
actionEntryStockId :: (Maybe String)
actionEntryAccelerator :: (Maybe String)
actionEntryTooltip :: (Maybe String)
actionEntryCallback :: (IO ())
}

El uso de estos campos es como su nombre indica y como ha sido descrito más arriba y en el capítulo 7.1. La función actionEntryCallback debe ser aportada por el programador, y será ejecutada cuando la acción a la que está asociada se active.

Añade una lista de entradas a un grupo de acción con:

actionGroupAddActions :: ActionGroup -> [ActionEntry] -> IO ()

Despés el grupo se inserta usando uiManagerInsertActionGroup como antes.

Hay funciones similares para RadioAction y ToggleAction . Las acciones Radio (Radio actions) permiten al usuario seleccionar entre varias posibilidades, de las que sólo una puede estar activa. Debido a esto tiene sentido definirlas todas juntas. La definición es:

data RadioActionEntry = RadioActionEntry {
radioActionName :: String
radioActionLabel :: String
radioActionStockId :: (Maybe String)
radioActionAccelerator :: (Maybe String)
radioActionTooltip :: (Maybe String)
radioActionValue :: Int
}

Los primeros 5 campos de nuevo se usan como se podría esperar. El radioActionValue (Valor de acción del radio) identifica cada una de las posibles selecciones. La incorporación al grupo se realiza con:

actionGroupAddRadioActions :: 
              ActionGroup -> [RadioActionEntry] -> Int -> (RadioAction -> IO ()) -> IO ()

El parámetro Int es el valor de la acción para ser activada inicialmente, o -1 si no va a ser ninguna.

Nota: En el siguiente ejemplo esto parece no tener efecto; la última acción está siempre seleccionada inicialmente.

La función de tipo (RadioAction -> IO ()) se ejecuta siempre que esa acción se activa.

Las acciones Toggle tienen un valor Bool y cada una puede establecerse o no. La ToggleActionEntry se define como:

data ToggleActionEntry = ToggleActionEntry {
toggleActionName :: String
toggleActionLabel :: String
toggleActionStockId :: (Maybe String)
toggleActionAccelerator :: (Maybe String)
toggleActionTooltip :: (Maybe String)
toggleActionCallback :: (IO ())
toggleActionIsActive :: Bool
}

El ejemplo que tenemos a continuación demuestra el uso de acciones toggle así como acciones radio.

Nota: La función toggleActionCallback tiene el valor equivocado en mi plataforma; el truco es, por supuesto, usar la función not.

RadioAction and ToggleAction

Los botones radio pueden controlar un modo "resaltado", como en el editor de texto gedit, del cual está copiado. El primer menú tiene un botón y dos submenús que contienen los items restantes. Además, uno de los botones radio es un elemento de un toolbar. Esta distribución está controlada completamente por la primera definición XML.

Las acciones toggle son elementos de otro menú, y dos de estos están también colocados en una barra de herramientas. Su distribución está determinada por la segunda definición XML.

Lo interesante es que el uiManager puede fusionar estas definiciones del ui, simplemente añadiendolas, como se verá más adelante. Así que puedes definir tus menús en módulos separados y combinarlos fácilmente más tarde en el módulo principal. De acuerdo a la documentación, el gestor de ui (ui manager) es suficientemente inteligente en esto, y por supuesto puedes usar nombres iguales en las definiciones XML que se diferencien en los caminos. Pero recuerda que la String que denota una acción, debe ser única para cada acción.

También es posible separar los menús y los toolbars, usando las funciones MergeId y uiManagerRemoveUi. De este modo puedes gestionar menús y toolbars dinámicamente.

import Graphics.UI.Gtk

main :: IO ()
main= do
     initGUI
     window <- windowNew
     set window [windowTitle := "Radio and Toggle Actions",
                 windowDefaultWidth := 400,
                 windowDefaultHeight := 200 ]
 
     mhma <- actionNew "MHMA" "Highlight\nMode" Nothing Nothing
     msma <- actionNew "MSMA" "Source"          Nothing Nothing
     mmma <- actionNew "MMMA" "Markup"          Nothing Nothing  

     agr1 <- actionGroupNew "AGR1"
     mapM_ (actionGroupAddAction agr1) [mhma,msma,mmma]
     actionGroupAddRadioActions agr1 hlmods 0 myOnChange

     vima <- actionNew "VIMA" "View" Nothing Nothing          

     agr2 <- actionGroupNew "AGR2"
     actionGroupAddAction agr2 vima
     actionGroupAddToggleActions agr2 togls

     uiman <- uiManagerNew
     uiManagerAddUiFromString uiman uiDef1
     uiManagerInsertActionGroup uiman agr1 0

     uiManagerAddUiFromString uiman uiDef2
     uiManagerInsertActionGroup uiman agr2 1

     mayMenubar <- uiManagerGetWidget uiman "/ui/menubar"
     let mb = case mayMenubar of 
                    (Just x) -> x
                    Nothing -> error "Cannot get menu bar."

     mayToolbar <- uiManagerGetWidget uiman "/ui/toolbar"
     let tb = case mayToolbar of 
                    (Just x) -> x
                    Nothing -> error "Cannot get tool bar."

     box <- vBoxNew False 0
     containerAdd window box
     boxPackStart box mb PackNatural 0
     boxPackStart box tb PackNatural 0

     widgetShowAll window
     onDestroy window mainQuit
     mainGUI

hlmods :: [RadioActionEntry]
hlmods = [
     RadioActionEntry "NOA" "None"    Nothing Nothing Nothing 0,   
     RadioActionEntry "SHA" "Haskell" (Just stockHome)  Nothing Nothing 1, 
     RadioActionEntry "SCA" "C"       Nothing Nothing Nothing 2,
     RadioActionEntry "SJA" "Java"    Nothing Nothing Nothing 3,
     RadioActionEntry "MHA" "HTML"    Nothing Nothing Nothing 4,
     RadioActionEntry "MXA" "XML"     Nothing Nothing Nothing 5]

myOnChange :: RadioAction -> IO ()
myOnChange ra = do val <- radioActionGetCurrentValue ra
                   putStrLn ("RadioAction " ++ (show val) ++ " now active.")

uiDef1 = " <ui> \
\           <menubar>\
\              <menu action=\"MHMA\">\
\                 <menuitem action=\"NOA\" />\
\                 <separator />\
\                 <menu action=\"MSMA\">\
\                    <menuitem action= \"SHA\" /> \
\                    <menuitem action= \"SCA\" /> \
\                    <menuitem action= \"SJA\" /> \
\                 </menu>\
\                 <menu action=\"MMMA\">\
\                    <menuitem action= \"MHA\" /> \
\                    <menuitem action= \"MXA\" /> \
\                 </menu>\
\              </menu>\
\           </menubar>\
\            <toolbar>\
\              <toolitem action=\"SHA\" />\
\            </toolbar>\
\           </ui> "            

togls :: [ToggleActionEntry]
togls = let mste = ToggleActionEntry "MST" "Messages" Nothing Nothing Nothing (myTog mste) False   
            ttte = ToggleActionEntry "ATT" "Attributes" Nothing Nothing Nothing (myTog ttte)  False 
            erte = ToggleActionEntry "ERT" "Errors" (Just stockInfo) Nothing Nothing (myTog erte)  True 
        in [mste,ttte,erte]

myTog :: ToggleActionEntry -> IO ()
myTog te = putStrLn ("The state of " ++ (toggleActionName te) 
                      ++ " (" ++ (toggleActionLabel te) ++ ") " 
                      ++ " is now " ++ (show $ not (toggleActionIsActive te)))
uiDef2 = "<ui>\
\          <menubar>\
\            <menu action=\"VIMA\">\
\             <menuitem action=\"MST\" />\
\             <menuitem action=\"ATT\" />\
\             <menuitem action=\"ERT\" />\
\            </menu>\
\          </menubar>\
\            <toolbar>\
\              <toolitem action=\"MST\" />\
\              <toolitem action=\"ERT\" />\
\            </toolbar>\
\         </ui>"