4.2 Adjustments, Scale and Range

Gtk2Hs has various widgets that can be visually adjusted by the user using the mouse or the keyboard, such as the range widgets, described in the range widget section. There are also a few widgets that display some adjustable portion of a larger area of data, such as the text widget and the viewport widget.

Obviously, an application needs to be able to react to changes the user makes in range widgets. One way to do this would be to have each widget emit its own type of signal when its adjustment changes. But you may also want to connect the adjustments of several widgets together, so that adjusting one adjusts the others. The most obvious example of this is connecting a scrollbar to a panning viewport or a scrolling text area.

The adjustment object can be used to store the configuration parameters and values of range widgets, such as scrollbars and scale controls. Because Adjustment is derived from GObject and Object, adjustments can emit signals, which can be used not only to allow your program to react to user input on adjustable widgets, but also to propagate adjustment values transparently between adjustable widgets.

Many of the widgets which use adjustment objects, like ScrolledWindow, can create their own adjustments, but you create one yourself with:

adjustmentNew :: Double        -- value         - The initial value of the range
              -> Double        -- lower         - The minimum value of the range
              -> Double        -- upper         - The maximum value of the range
              -> Double        -- stepIncrement - The smaller of two possible increments
              -> Double        -- pageIncrement - The larger of two possible increments
              -> Double        -- pageSize      - The size of the visible area
              -> IO Adjustment

The creation function takes every value that is contained in the object: value is the initial value and should be between the upper and lower bounds of the slider. Clicking on the arrows increases this value by stepIncrement. Clicking in the slider advances by pageIncrement. The pageSize is needed to determine if the end of the slider is still in the range. You can get and set all the parameters of an adjustment by methods or using the general set and get functions on the adjustment attributes.

onValueChanged :: Adjustment -> IO () -> IO (ConnectId Adjustment)

is the signal emitted when the value of the adjustment changes, and:

onAdjChanged :: Adjustment -> IO () -> IO (ConnectId Adjustment)

is the signal emitted when one or more of the other than the value fields have changed.

Scale and Range Widgets

Scale widgets are used to allow the user to visually select and manipulate a value within a specific range using a slider. You might want to use a scale widget, for example, to adjust the magnification level on a zoomed preview of a picture, or to control the brightness of a color, or to specify the number of minutes of inactivity before a screensaver takes over the screen.

The following functions create vertical and horizontal scale widgets, respectively:

vScaleNew :: Adjustment -> IO VScale

hScaleNew :: Adjustment -> IO Hscale

There are also two constructors which do not take an adjustment:

vScaleNewWithRange :: Double ->. Double -> Double -> IO VScale

hScaleNewWithRange :: Double ->. Double -> Double -> IO Hscale

The Double parameters refer to the minimum and maximum values and the step. The step increment (preferably a power of 10) is the value the scale moves when the arrow keys are used.

Horizontal and vertical scales are instances of ScaleClass and their common behaviors are defined in the module Graphics.UI.Gtk.Abstract.Scale.

Scale widgets can display their current value as a number beside the trough. The default behaviour is to show the value, but you can change this with this function:

scaleSetDrawValue :: ScaleClass self => self -> Bool -> IO ()

The value displayed by a scale widget is rounded to one decimal point by default, as is the value field in its Adjustment. You can change this with:

scaleSetDigits :: ScaleClass self => self -> Int -> IO ()

Finally, the value can be drawn in different positions relative to the trough:

scaleSetValuePos :: ScaleClass self => self -> PositionType -> IO ()

The PositionType is defined as:

data PositionType = PosLeft | PosRight | PosTop | PosBottom

Scale itself inherits many methods form its base class, which is Range.

Setting the Update Policy

The update policy of a range widget defines at what points during user interaction it will change the value field of its Adjustment and emit the onRangeValueChanged signal on this Adjustment. The update policies are defined by the UpdateType, which has three constructors:

UpdateContinuous
This is the default. The onRangeValueChanged signal is emitted continuously, i.e., whenever the slider is moved by even the tiniest amount.
UpdateDiscontinuous
The onRangeValueChanged signal is only emitted once the slider has stopped moving and the user has released the mouse button.
UpdateDelayed
The onRangeValueChanged signal is emitted when the user releases the mouse button, or if the slider stops moving for a short period of time.

The update policy of a range widget can be set by:

rangeSetUpdatePolicy :: RangeClass self => self -> UpdateType -> IO ()

Getting and Setting Adjustments

Getting and setting the adjustment for a range widget on the fly is done, predictably, with:

rangeGetAdjustment :: RangeClass self => self -> IO Adjustment

rangeSetAdjustment :: RangeClass self => self -> Adjustment -> IO ()

rangeSetAdjustment does absolutely nothing if you pass it the adjustment that it is already using, regardless of whether you changed any of its fields or not. If you pass it a new Adjustment, it will unreference the old one if it exists (possibly destroying it), connect the appropriate signals to the new one, and call the private function gtk_range_adjustment_changed(), which will (or at least, is supposed to...) recalculate the size and/or position of the slider and redraw if necessary. As mentioned in the section on adjustments, if you wish to reuse the same Adjustment, when you modify its values directly, you should emit the changed signal on it.

Key and Mouse Bindings

All of the Gtk2Hs range widgets react to mouse clicks in more or less the same way. Clicking mouse button 1 in the trough will cause its adjustment's stepIncrement to be added or subtracted from its value, and the slider to be moved accordingly. Clicking mouse button 2 in the trough will jump the slider to the point at which the button was clicked. Clicking mouse button 3 in the trough of a range or any button on a scrollbar's arrows will cause its adjustment's value to change by stepIncrement at a time.

Note: This did not work on Linux Fedora 6 with the standard mouse bindings.

Scrollbars are not focusable, thus have no key bindings. The key bindings for the other range widgets (which are, of course, only active when the widget has focus) do not differentiate between horizontal and vertical range widgets.

All range widgets can be operated with the left, right, up and down arrow keys, as well as with the Page Up and Page Down keys. The arrows move the slider up and down by stepIncrement, while Page Up and Page Down move it by pageIncrement. Home and End move to the beginning and end of the slider.

The user can also move the slider all the way to one end or the other of the trough using the keyboard. This is done with the Home and End keys.

Example

This example puts up a window with three range widgets all connected to the same adjustment, and a couple of controls for adjusting some of the parameters mentioned above so you can see how they affect the way these widgets work for the user.

Range widgets example

The three scales are placed so the vertical is next to the two horizontal ones, one above the other. So we need a horizontal box for the vertical scale and a vertical box next to it for the horizontal scales. The scales and the boxes must be packed with PackGrow so the scales will resize with the main box, which is a vertical box in the window.

All three scales ar constructed with the same adjustment, setting the initial value at 0.0, the minimum value at 0.0, the maximum value at 101.0, the step increment at 0.1, the page increment at 1.0 and the page size at 1.0.

  adj1 <- adjustmentNew 0.0 0.0 101.0 0.1 1.0 1.0

The user can control whether the scale values are displayed with a checkButton. This is packed into the main box and set to be active initially. A check button is a toggle button and when the user checks or uncehecks it the onToggled signal is sent. this causes the toggleDisplay function to be evaluated, which is defined as:

toggleDisplay :: ScaleClass self => CheckButton -> [self] -> IO ()
toggleDisplay b scls = sequence_ (map change scls) where
                         change sc = do st <- toggleButtonGetActive b
                                        scaleSetDrawValue sc st

The function has a checkButton type as its parameter, and a list of instances of ScaleClass. However, a list can only contain values of the same type, and vScale and hScale are different types. So, we can use the function on lists of vertical scales or horizontal scales, but lists containing both types result in a typing error.

The user can select the positionType using a widget not mentioned before, a ComboBox. This allows a selection of choices as shown below. The one to be set active is determined by an index, which is 0 here, the first one.

makeOpt1 :: IO ComboBox
makeOpt1 = do
  cb <- comboBoxNewText
  comboBoxAppendText cb "TOP"
  comboBoxAppendText cb "BOTTOM"
  comboBoxAppendText cb "LEFT"
  comboBoxAppendText cb "RIGHT"
  comboBoxSetActive cb 0
  return cb

A second comboBox lets the user select the update policy, one of the three UpdateType constructors.

makeOpt2 :: IO ComboBox
makeOpt2 = do
  cb <- comboBoxNewText
  comboBoxAppendText cb "Continuous"
  comboBoxAppendText cb "Discontinuous"
  comboBoxAppendText cb "Delayed"
  comboBoxSetActive cb 0
  return cb

The combo boxes themselves just display text, of course. To select the position, respectively the update policy, we define:

setScalePos :: ScaleClass self => ComboBox -> self -> IO ()
setScalePos cb sc = do
    ntxt <- comboBoxGetActiveText cb
    let pos = case ntxt of
                (Just "TOP")    -> PosTop
                (Just "BOTTOM") -> PosBottom
                (Just "LEFT")   -> PosLeft
                (Just "RIGHT")  -> PosRight
                Nothing         -> error "setScalePos: no position set"
    scaleSetValuePos sc pos

setUpdatePol :: RangeClass self => ComboBox -> self -> IO ()
setUpdatePol cb sc = do
    ntxt <- comboBoxGetActiveText cb
    let pol = case ntxt of
                (Just "Continuous")    -> UpdateContinuous
                (Just "Discontinuous") -> UpdateDiscontinuous
                (Just "Delayed")       -> UpdateDelayed
                Nothing                -> error "setUpdatePol: no policy set"
    rangeSetUpdatePolicy sc pol

Here we have not used lists to manage the vertical and horizontal scales, so each horizontal scale is addressed separately.

The number of precision shown on the three scales will be managed with another scale, for which we use a new adjustment. The maximum precision is 10 and each increment is 1. The precision of this control scale itself is set to 1.

  adj2 <- adjustmentNew 1.0 0.0 5.0 1.0 1.0 0.0

When the control adjustment changes, the signal onValueChanged will be emitted and then the defined function setDigits is evaluated.

setDigits :: ScaleClass self => self -> Adjustment -> IO ()
setDigits sc adj = do val <- get adj adjustmentValue
                      set sc [scaleDigits := (round val)]

Here we use the general functions set and get on the attributes; we might have used the appropriate methods as well. Note that the Double of the adjustment value must be rounded to an Integral type.

We use another horizontal scale to manage the page size of the three example scales. When set at 0.0 the scales can reach their initial maximum of 100.0 and when set at 100.0 the scales are fixed at the lowest value. This involves the adjusting of the adjustment by a onValueChanged signal from a third adjustment by this code snippet:

  onValueChanged adj3 $ do val <- adjustmentGetValue adj3
                           adjustmentSetPageSize adj1 val

The main function is:

import Graphics.UI.Gtk

main :: IO ()
main = do
  initGUI
  window  <- windowNew
  set window [windowTitle := "range controls",
              windowDefaultWidth := 250]
  mainbox <- vBoxNew False 10
  containerAdd window mainbox
  containerSetBorderWidth mainbox 10

  box1 <- hBoxNew False 0
  boxPackStart mainbox box1 PackGrow 0
  adj1 <- adjustmentNew 0.0 0.0 101.0 0.1 1.0 1.0
  vsc  <- vScaleNew adj1
  boxPackStart box1 vsc PackGrow 0

  box2 <- vBoxNew False 0
  boxPackStart box1 box2 PackGrow 0

  hsc1 <- hScaleNew adj1
  boxPackStart box2 hsc1 PackGrow 0
  hsc2 <- hScaleNew adj1
  boxPackStart box2 hsc2 PackGrow 0

  chb <- checkButtonNewWithLabel "Display Value on Scale Widgets"
  boxPackStart mainbox chb PackNatural 10
  toggleButtonSetActive chb True

  box3   <- hBoxNew False 10
  boxPackStart mainbox box3 PackNatural 0
  label1 <- labelNew (Just "Scale Value Position:")
  boxPackStart box3 label1 PackNatural 0
  opt1   <- makeOpt1
  boxPackStart box3 opt1 PackNatural 0

  box4   <- hBoxNew False 10
  boxPackStart mainbox box4 PackNatural 0
  label2 <- labelNew (Just "Scale Update Policy:")
  boxPackStart box4 label2 PackNatural 0
  opt2   <- makeOpt2
  boxPackStart box4 opt2 PackNatural 0

  adj2 <- adjustmentNew 1.0 0.0 5.0 1.0 1.0 0.0

  box5   <- hBoxNew False 0
  containerSetBorderWidth box5 10
  boxPackStart mainbox box5 PackGrow 0
  label3 <- labelNew (Just "Scale Digits:")
  boxPackStart box5 label3 PackNatural 10
  dsc    <- hScaleNew adj2
  boxPackStart box5 dsc PackGrow 0
  scaleSetDigits dsc 0

  adj3 <- adjustmentNew 1.0 1.0 101.0 1.0 1.0 0.0

  box6   <- hBoxNew False 0
  containerSetBorderWidth box6 10
  boxPackStart mainbox box6 PackGrow 0
  label4 <- labelNew (Just "Scrollbar Page Size:")
  boxPackStart box6 label4 PackNatural 10
  psc    <- hScaleNew adj3
  boxPackStart box6 psc PackGrow 0
  scaleSetDigits psc 0

  onToggled chb $ do toggleDisplay chb [hsc1,hsc2]
                     toggleDisplay chb [vsc]

  onChanged opt1 $ do setScalePos opt1 hsc1
                      setScalePos opt1 hsc2
                      setScalePos opt1 vsc

  onChanged opt2 $ do setUpdatePol opt2 hsc1
                      setUpdatePol opt2 hsc2
                      setUpdatePol opt2 vsc

  onValueChanged adj2 $ do setDigits hsc1 adj2
                           setDigits hsc2 adj2
                           setDigits vsc  adj2

  onValueChanged adj3 $ do val <- adjustmentGetValue adj3
                           adjustmentSetPageSize adj1 val

  widgetShowAll window
  onDestroy window mainQuit
  mainGUI

The non standard functions used in the listing have already been listed above.