6.2 Event Boxes and Button Boxes

An event in Gtk2Hs is something that is sent to a widget, by the main loop, usually as a result of an action performed by the user. The widget then responds by emitting a signal, and this is the 'signal' to the program to 'do something'. To the Gtk2Hs application programmer, however, an event is just a Haskell data type with named fields. Many of those are described in the Graphics.UI.Gtk.Gdk.Events section in the API documentation. Look, for example, at the widget signal:

onButtonPress :: WidgetClass w => w -> (Event -> IO Bool) -> IO (ConnectId w)

This is not to be confused with the signal emitted when a Button type widget has been pressed; the button here is a mouse button and the signal is emitted when a mouse button has been pressed when the mouse is over that widget. The handler is a function which takes an event, which has to have the Button constructor, and has a IO boolean value. The API lists the following fields for Button :

eventSent :: Bool
eventClick :: Click
eventTime :: TimeStamp
eventModifier :: [Modifier]
eventButton :: MouseButton
eventXRoot, eventYRoot :: Double

The first is used for the return. It occurs in all Event constructors like Motion, Expose, Key, Crossing, Focus, Configure, Scroll, WindowState and Proximity. From Events you can extract all kinds of information about what the user is doing. A simple example is this code snippet:

onButtonPress eb 
                 (\x -> if (eventButton x) == LeftButton 
                           then do widgetSetSensitivity eb False 
                                   return (eventSent x)
                           else return (eventSent x))

Here parameter eb is the widget covered by the mouse, and the anonymous function is of the type as described above. Something is done (see the example below) if the left mouse button has been pressed and then eventSent returns the appropriate boolean. If another mouse button has been pressed, nothing happens, and only the boolean is returned.

Now, some widgets don't have associated windows, so they just draw on their parents. Because of this, they cannot receive events and if they are incorrectly sized, they don't clip so you can get messy overwriting (but we won't discuss this further). An EventBox provides an X window for its child widget. It is a subclass of Bin which also has its own window and which is a subclass of ContainerClass .

To create a new EventBox widget, use:

eventBoxNew :: IO EventBox

To add a child we can just use the well known:

containerAdd :: (ContainerClass self, WidgetClass widget) => self -> widget -> IO ()

The window may be visible or invisible, and the event box may be above or below its child in the widget tree. This is determined by:

eventBoxVisibleWindow :: Attr EventBox Bool    -- default True
eventBoxAboveChild :: Attr EventBox Bool       -- default False

If you just want to trap events, then set the window to be invisible. If the eventBox is above its child, all events will go to it first. If it is below, windows in child widgets of the child will be reached first.

A Button Box is just a box which can be used to pack buttons in a standard way. There are two kinds, horizontal and vertical ones, and you construct them with:

hbuttonBoxNew :: IO HButtonBox
vButtonBoxNew :: IO VButtonBox

The functionality is in the ButtonBoxClass.

buttonBoxSetLayout :: ButtonBoxClass self => self -> ButtonBoxStyle -> IO ()

The style is one of the following: ButtonBoxDefaultStyle, ButtonBoxSpread, ButtonBoxEdge, ButtonBoxStart, ButtonBoxEnd . You don't pack buttons as in ordinary horizontal and vertical boxes, but you use the containerAdd function instead.

The second feature of button boxes is that you can define one or more of your buttons to be in a secondary group. These will then be treated differently when the button box is resized. For example, a help button can be kept visually apart from the others. The function is:

buttonBoxSetChildSecondary :: (ButtonBoxClass self, WidgetClass child) => self -> child -> Bool -> IO ()

This illustrates the use of event boxes and button boxes:

Slot Machine

The buttons are packed into a vertical button box, with the play button as a secondary child. This is also a mnemonic button, with Alt-P as the accellerator key. The images are placed into event boxes with visible windows, and their background color is set to a shade of green with:

widgetModifyBg eb StateNormal (Color 0 35000 0)

As mentioned in Chapter 5.3 the StateType can be StateNormal, StateActive, StatePrelight, StateSelected or StateInsensitive .

Note that the images above are not all the same size. This does not matter, but some care has to be taken to make the main window large enough. Otherwise borders will disappear when the pictures are switched.

When the user clicks the left mouse button when the mouse is over an event box, it will be set to insensitive with:

widgetSetSensitivity :: WidgetClass self => self -> Bool -> IO ()

This changes the StateType to StateInsensitive and the widget will no longer respond to any user events. Furthermore, its appearance changes. In the example we've also set the background color to a shade of grey.

Slot Machine Insensitive

We've used tooltips to tell the user the images can be frozen. As mentioned in Chapter 4.4 they don't always work in GHCi but they do in the compiled version. To flip the images randomly, we've used function RandomRIO, as in the previous chapter. You may wonder why a tuple of EventBox and Image has been used, instead of just getting the Image from the containerChild attribute of the event boxes. This is because it is a write only attribute, it can be set but not retrieved with get .

Finally, if the images are not available in your source code directory, or if you want to expand the slot machine with more slots, there is an ample supply of Brazilian fish at Peixes. They have been classified into salt water (água salgado) and fresh water (água doce) fish for your convenience.

import Graphics.UI.Gtk
import System.Random (randomRIO)

main :: IO ()
main= do
     initGUI
     window <- windowNew
     set window [windowTitle := "Slot Machine",
                 containerBorderWidth := 10,
                 windowDefaultWidth := 350, 
                 windowDefaultHeight := 400]                 
     hb1 <- hBoxNew False 0
     containerAdd window hb1
     vb1 <- vBoxNew False 0
     boxPackStart hb1 vb1 PackGrow 0
     vbb <- vButtonBoxNew
     boxPackStart hb1 vbb PackGrow 0
     resetb <- buttonNewWithLabel "Reset"
     containerAdd vbb resetb
     quitb <- buttonNewWithLabel "Quit"
     containerAdd vbb quitb
     playb <- buttonNewWithMnemonic "_Play"
     containerAdd vbb playb
     set vbb [buttonBoxLayoutStyle := ButtonboxStart, 
              (buttonBoxChildSecondary playb) := True ]

     let picfiles = ["./jacunda.gif", "./pacu.gif", "./tucunaream.gif"]
     evimls <- sequence (map (initEvent vb1) picfiles)
     tips <- tooltipsNew
     sequence_ $ map ((myTooltip tips) . fst) evimls

     onClicked playb (play evimls picfiles)

     onClicked resetb $ sequence_ (zipWith reSet evimls picfiles)

     onClicked quitb (widgetDestroy window)
     widgetShowAll window
     onDestroy window mainQuit
     mainGUI

initEvent :: VBox -> FilePath -> IO (EventBox, Image)
initEvent vb picfile = do
              eb <- eventBoxNew
              boxPackStart vb eb PackGrow 0
              slot <- imageNewFromFile picfile
              set eb[containerChild := slot, containerBorderWidth := 10 ]
              widgetModifyBg eb StateNormal (Color 0 35000 0)
              widgetModifyBg eb StateInsensitive (Color 50000 50000 50000)
              onButtonPress eb 
                 (\x -> if (eventButton x) == LeftButton 
                           then do widgetSetSensitivity eb False 
                                   return (eventSent x)
                           else return (eventSent x))
              return (eb, slot)

reSet :: (EventBox, Image) -> FilePath -> IO ()
reSet (eb, im) pf = do widgetSetSensitivity eb True                 
                       imageSetFromFile im pf  

play :: [(EventBox, Image)] -> [FilePath] -> IO ()
play eilist fplist = 
   do let n = length fplist
      rands <- sequence $ replicate n (randomRIO (0::Int,(n-1)))
      sequence_ (zipWith display eilist rands) where
                     display (eb, im) rn = do
                                  state <- widgetGetState eb
                                  if state == StateInsensitive 
                                     then return ()
                                     else imageSetFromFile im (fplist !! rn)   

myTooltip :: Tooltips -> EventBox -> IO ()
myTooltip ttp eb = tooltipsSetTip ttp eb "Click Left Mouse Button to Freeze" ""