Tutoriel Gtk2Hs

Tutoriel Gtk2Hs 7.2 - Menus contextuels, actions radios et bascules

Les menus sont normalement simplement ajoutés à une fenêtre, mais ils peuvent aussi être affichés temporairement en réponse à un clic de la souris. Par exemple, un menu de contexte peut être affiché quand l'utilisateur clique sur le bouton droit de la souris.

On peut obtenir un menu contextuel en utilisant le nœud popup. Par exemple :

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

Construire un menu contextuel nécessite les mêmes étapes qu'un menu ou une barre de menus. Une fois que l'on a créé les actions et mis celles-ci dans un ou plusieurs groupes, on crée l'interface graphique, on ajoute la chaîne de caractère XML puis les groupes. On extrait alors les widgets. Dans l'exemple ci-dessus, nous avons créé 4 actions avec les noms listés ci-dessus. Un menu contextuel ne s'affichant pas pendant une capture d'écran, il n'y a pas d'image disponible.

Les menus contextuels ne s'empaquettent pas et pour les afficher, il faut utiliser la fonction :

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

Ceci est documenté dans Graphics.UI.Gtk.MenuComboToolbar.Menu dans la documentation de l'API. Dans l'exemple, on fait apparaître le menu en cliquant le bouton droit de la souris, et le second argument peut être Nothing. La fonction est la même q'avec la boite d'évènement du chapitre 6.2. Mais ici, on peut utiliser la fenêtre elle-même au lieu de la boite d'évènement.

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

Le seul bémol est que le widget retourné par le ui manager est du type Widget est que la fonction menuPopup prend un argument d'un type qui est une instance de MenuClass. Il faut donc utiliser:

castToMenu :: GObjectClass obj => obj -> Menu

Cette fonction est également documentée dans la section Graphics.UI.Gtk.MenuComboToolbar.Menu. Le listing complet de l'exemple est:

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)

Il y a une autre façon d'utiliser les actions, sans les créer explicitement avec le type ActionEntry:

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

L'utilisation de ces champs a été décrite dans le chapitre 7.1. La fonction actionEntryCallback doit être fournie par le programmeur et sera éxécutée quand cette action est activée.

Pour ajouter une liste d'entrée a une action, on utilise:

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

Le groupe est alors inséré en utilisant uiManagerInsertActionGroup comme précédemment

Des fonctions similaires éxistent pour RadioAction et ToggleAction. les actions radios permettent à l'utilisateur de choisir à partir d'un ensemble de possibilités dont une seule peut être activée. C'est pour cette raison qu'il est logique de les définir toutes ensemble:

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

Les cinq premiers champs sont encore utilisés. radioActionValue identifie chacune des sélections possibles. Un ajout à un groupe se fait avec:

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

Le paramètre Int est la valeur de l'action à activer initialement, ou -1 si il ne doit en y avoir aucune.

La fonction de type (RadioAction -> IO ()) est éxécuté à chaque fois qu'une action est activée.

Les actions toggle ont une valeur Bool est chacune peut être définie ou non. Le widget ToggleActionEntry est définie par:

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

L'exemple ci-dessous présente l'utilisation des actions à bascule ainsi que des actions radio.

Note: The toggleActionCallback function has the wrong value on my platform; the workaround is, of course, to use the not function.

Gtk
 
Gtk2Hs
 
RadioAction
 
and
 
ToggleAction

Les boutons radio pourraient servir à controler un mode de surbrillance comme dans l'éditeur de texte gedit. Le premier menu a un bouton et deux sous-menus qui contiennent les éléments restant. En outre, un des boutons radio se trouve dans une barre d'outils. Cette disposition est controlée par la première définition XML.

Les actions bascule sont des items dans un autre menu, et deux de ceux là sont aussi placés dans la barre d'outils. Cet agencement est déterminé par la seconde définition XML.

Ce qui est intéressant, c'est que le uiManager peut ajouter ces définitions ui juste en les ajoutant comme montré ci-dessous. Vous pouvez donc définir vos menus dans des modules séparés et les combiner facilement dans le module principal. D'après la documentation, le ui manager est assez efficace pour cela, et bien sur vous pouvez aussi utiliser les noms dans les définitions XML pour différencier les chemins. Mais rappelons que la chaine String qui décrit un nom d'action doit être unique pour chaque action.

Il est également possible de supprimer des menus et des barres d'outils en utilisant les fonctions MergeId et uiManagerRemoveUi. Dans ce sens, vous pouvez gérer les menus et les barres d'outils de façon dynamique.

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