Contents

Introduction

The gui package provides an object-oriented library of user interface components, which make use of the underlying graphics facilities.

Ivib

Ivib is a program which lets the user design a gui interface, allowing the user to interactively place and configure components in a window area.

To create a dialog window using Ivib, start the program with the name of a new source file. For example:

ivib myprog.icn

The Ivib window will appear with a blank "canvas" area, which represents the dialog window to be created.

Most of the toolbar buttons create components to add to the dialog under construction. This doesn't represent the full set of available components; the full list can be found under the "Objects" menu.

Once a component is added to the canvas, it can be dragged into position and resized (by dragging its corners). Right-clicking brings up a context menu for the component. For example, here is the view after a button has been added, and its context menu shown :-

Selecting "Dialog" opens up the component's configuration dialog; this can also be done by simply pressing the return key.

The name of the component (ie its variable name in the generated code) can be altered in the code tab :-

As well as adding components, the toolbar contains buttons for other useful operations. There is a save button, undo and redo buttons, a preview button which lets the user interact with the dialog under design, and a button which shows the tree structure of the components in the canvas.

Saving a file

Ivib's saved files are program source code that implement the interface. A subclass of the Dialog class is written (the actual super-class is in fact configurable), and this includes a method called setup(), which configures all the components of the interface. Do not edit this method, since it will be re-written next time the dialog is saved.

Separate layout file

Ivib normally saves its layout data along with the icon source file, as a lengthy comment. This can be rather overbearing, and so it is possible to save this data in a separate file. To do this select the "Code" tab of the dialog preferences (menu item "Canvas/Dialog prefs"). Then edit "Save layout in" field. In the entered value "$" represents the name of the icon source file (without its extension).

So in this case, if the icon file were mydialog.icn we would save the layout data in layouts/mydialog.layout. Obviously the layouts directory would have to exist.

The Component class

This class is the base class for all the components in the gui library, such as buttons, text fields, tables and so on.

Lifecycle

Each Component is initialized before its enclosing dialog is shown, and "finalized" just before the dialog is disposed. This is done by the methods initially(), and finally(), respectively. These methods may be overridden to include custom initialization and cleanup code, but when doing so it is important to remember to call the superclass's overridden method; for example :-

class MyComponent(Component)
   ...
   public initially()
      Component.initially()
      # custom initialization code goes here
   end
   
   public finally()
      Component.finally()
      # custom cleanup code goes here
   end
   ...
end

Attributes

Each component has its own set of window attributes, and its own cloned buffer window to draw into. There are methods to set attributes which correspond to the methods in the Window class. For example :-

b := TextButton().
        set_fg("green").
        set_font("serif")

To set attributes in ivib, use the WAttribs tab of the component's dialog, and add the constraints, as follows :-

The buffer window

The cloned buffer window is a hidden window, held in the member variable cbwin. It is opened in the initially() method. Thereafter, it can be used to calculate text dimensions, such as font heights and text widths.

Display and invalidate

The display() method is used by the component to render itself. It draws into the buffer window, cbwin. Before calling display(), cbwin is clipped and erased to the rectangle which the system desires to be redrawn. This rectangle can be obtained via the get_cbwin_clip(), which returns a Rect instance, and this can be used to avoid drawing outside the clipped rectangle unnecessarily.

The display_children() method can be called from within display() to draw the component's children.

When a component (or part of it) must be redrawn, it is "invalidated". This is done by invoking the component's invalidate() method, with an optional Rect argument indicating the part of the component which must be redrawn. The system then, at a later point, schedules a call to display() to redraw the component. Calling invalidate() is quite cheap.

Child components

A component can contain child components. Some components have children built-in, for example a scrollbar has up and down buttons which are both child components. But child components can be added arbitrarily too. In ivib, child components can be added to a Panel component by dragging them inside the Panel. They then act as a unit, and can be moved together.

In ivib, the full tree of components may be viewed using the tree view window, which is opened via the toolbar button at the right with the tree icon.

Attributes

A child component inherits the window attributes from its parent. Thus setting the above Panel's foreground to green would give both the child components a green foreground too.

Z order

The children of a component have a particular Z order, which determines which child component lies on top of another, when they overlap. Most of the time, children don't overlap each other, so this doesn't matter.

To change the Z order in ivib, click on the components in the desired order, whilst holding down the control key. The selections should then be numbered, as shown in the following example :-

Now select the menu item Selections/Order/Set Z order. The z order is changed to the selected order, resulting in the following :-

Tab order

The children also have a particular tab focus order, which determines how the focus changes when the tab (and cursor) keys are pressed. This can easily be changed in ivib by selecting as described for Z ordering, and using the menu option Selections/Order/Set tab order.

Clipping

Child components are clipped to their parent's rectangle. So for example, in the following the button is clipped by its parent panel, and is only partly visible.

The Paint class

This class is an abstraction for drawing. A Paint instance has a size, and can draw itself at an arbitrary location. The idea is that a Paint is used where one would intuitively use a string - for example in a button label, or a menu item, or a tab heading. But a Paint is abstract, so an implementation can draw anything, not just text.

The following sections describe some of the common Paint implementations. Of course, you can also define your own.

TextPaint

This is the most basic Paint instance; it just paints a simple string.

b := TextButton().
       set_paint(TextPaint("Hello"))

ImagePaint

This Paint instance paints an arbitrary image. It has options for choosing the source of the image, and for scaling it to a particular size. For example, to simply display an image from the cache :-

b := TextButton().
       set_paint(ImagePaint().set_cache("cache-key"))

In ivib, an ImagePaint may be configured by first selecting "Image" from the relevant drop-down list (for example in a button's configuration dialog). Then click the "Edit" button. The following dialog is shown.

The text field can contain either a file name, or a key into the image cache's table of named image data. The two buttons let you browse for these values if you wish to. The box to the left gives a preview of the image. The cache checkbox indicates whether the image, when loaded, should be retained by the ImageCache. This will make future references to the same image faster, but will take up memory.

GridPaint

This is a Paint class which allows text and images to be combined using the same layout system used by GridLayout. Text attributes can be set to give different fonts and colours. A single string is used to specify everything. Ivib contains an interactive editor so that you can see the results as you make edits to the string.

To access the editor, first select "Grid" from the dropdown list associated with the relevant Paint. It will normally be set to "Text". Then press the "Edit" button.

The grid string consists of text interspersed with commands, which begin on a newline with a ".". These commands control the grid and its contents. The grid string should begin with a cell command.

The content of each cell in the grid is split into lines. Each line is "output" to the window with a break command, .br. Here is a very simple grid string :-

.cell
Hello
.br
World
.br

The following screenshot shows this string being edited in the Ivib GridPaint editor.

The following is a complete list of grid string commands. Note that if an argument contains spaces, it should be surrounded in single quotes.

Borders

Instances of thte ABorder class are used to draw borders around things. The odd name is to avoid a name-clash with a component called Border. Unlike Paint, a border doesn't have a size; rather it has four inset values, giving the width of each of its four borders. A border also has two alignment values, used to specify the alignment of the thing being drawn inside the border. For example :-

TextButton().
   set_size(110, 80).
   set_paint(TextPaint("Button")).
   set_border(RaisedBorder().set_align(Align.R, Align.B))

results in

Note how the button has been given a size greater than its natural preferred size based on the label. So the alignment of the border causes the label to gravitate towards the bottom right corner within the additional space.

Normally of course a button will be sized to its preferred size, so there will be no extra space; and if there were the default alignment of a border is to centre the label.

The following image shows some of the available border classes drawing a Button's border. NullBorder is a zero size border, whilst EmptyBorder provides space, but nothing visible. The default space is the constant Gui.X_INSET horizontally and Gui.Y_INSET vertically.

The border of a component can be changed in ivib by choosing the "Other" tab in the component's configuration dialog, then choosing the border style from the drop-down list. Pressing the "Edit..." button allows the chosen border to be configured.

CompoundBorder

It is often useful to combine two borders, one within another, usually to provide some space padding, by making one of the borders an EmptyBorder.

ItemPaint

The Paint classes operate on single items of data; for example a TextPaint instance draws a particular string, and only that string. The ItemPaint classes on the other hand are designed so that a single instance can operate on multiple data items. They can thus be used to provide a flexible way of rendering data in components which draw many items of data, such as lists, trees and tables. The several data items are passed to the methods of the single ItemPaint instance to give sizing information and to draw the data.

An ItemPaint is generally used by a component together with a border in order to provide a border around each item.

An example of a sophisticated ItemPaint, is that used in the file dialog to draw file names and icons :-

The input data elements to the ItemPaint are io.ListEntry items, each of which gives information about a single file, returned by the Files.list() method. The right-hand list of files is created as follows :-

flist := ItemPaintList().
   set_draggable_cursor(&yes).
   set_item_paint(AnnotatedIconFilesItemPaint()).
   set_item_border(EmptyBorder().
                       set_insets(Gui.TEXT_INSET, Gui.TEXT_INSET, 0, 0).
                       set_x_align(Align.L))

This particular ItemPaint is also used in the example programs flowterm, explorer and ttexplorer.

Dispatcher

This class is responsible for dispatching events and re-drawing requests to open dialogs. It also includes a scheduler that can be used to run background tasks. This is used quite widely in the gui library; for example to make the cursor in a text field go on and off. It is very easy to create a background task, as follows :-

t := Dispatcher.new_task{ ... }

where the dots contain whatever code we want to run in the task. Typically this will be a procedure or method involving a loop, which does something, then sleeps for a while, and repeats. The returned value, t, is an instance of the class io.Task. The task can now be started with

t.start()

and stopped again with

t.stop()

The Task class has many other methods for controlling its behaviour, and can also be used to do background I/O - (see the documentation for the io.Task, io.TaskStream and io.Scheduler classes for more details).

The example program sieve contains a simple example of a task which calculates prime numbers. A more complex example is browser which is a simple web browser that does its background I/O using Tasks.

ImageCache

This class is used to store image data and rendered images in a convenient and efficient way. It consists of static methods, and maintains two tables. The first table maps string keys to image data. The image data is binary string image data, in any recognised image format. The second table maps string keys to Windows, each containing a single image. These can then be drawn into a dialog.

The data table is particularly useful for associating image data loaded with the $load preprocessor directive with a string key, which can then be referenced anywhere in a program. For example :-

$load MY_IMG_DATA "/home/rparlett/images/my_img.png"

...
procedure main()
   ...
   ImageCache.name_image("my_img", MY_IMG_DATA)
   ...
end
...

Now we can use the key "my_img" to refer to the png image data. However, this is only data, as opposed to an actual rendered image. These are contained in hidden Windows, one for each image, and are stored in the cache's image table. To lookup a Window, use the ImageCache.get method, passing either a filename, or a key into the named image table. For example :-

i1 := ImageCache.get("/home/rparlett/images/my_img.png")
i2 := ImageCache.get("my_img")

The first example loads the image in the named file into a Window, puts it into the image map, and returns it. The second example looks up the key "my_img" in the data map, and assuming we have placed such an entry in the data map, as in the previous example, creates a Window from that data, puts it into the image map, and returns it. In both cases, subsequent calls with the same filename or data key will immediately return the Window cached in the image map.

The most significant difference between the two examples is that the first requires the png file to be present at run-time; whilst the second will use the data stored in the data map with name_image(); that data will have come from the compile-time inclusion of the image data, so the png file needn't be present at run-time.

If a data key and filename clash (in the above example, perhaps there may be a file named "my_img" in the current directory), then the data key has priority. In other words, only if the data key is absent is a file loaded. If the file is absent too (or the image data is invalid), then the get() method fails.

A Window returned by get() should never be closed by the caller, since it may be re-used later on.

ImageCache also has a load() method; this is like get(), but it doesn't cache the image in the image table. So

i3 := ImageCache.load("my_img")

will always create a new Window from the "my_img" data. Unlike a Window returned by get(), the caller must ensure that a Window returned by load() is closed; otherwise a memory leak will result.

In ivib, selecting cache keys presents a problem, because the cache data may be setup elsewhere in the program of which the dialog being designed will form a part; so ivib has no way of knowing what the image represented by a key may be. In order to give some flexibility, ivib allows the user to provide a list of directories containing images; these are scanned at startup and the image cache is populated with image data from any image files found. The list of directories can be set up in the dialog File/Preferences, and in the "Named image paths" tab :-

For instance, the examples directory, included in the above list, contains several png and gif files; for example rpp.FONT_24.gif. At startup, this file is loaded into the image cache names table, with the key rpp.FONT_24 (the extension is dropped). Then this image is available (along with several others) from the list shown when a cache key is requested :-

When the dialog is incorporated into the program, it is the programmer's responsibility to set up the cache so that any keys used are available. So for example, if the rpp.FONT_24 key were used, then the file rpp.FONT_24.gif could be included with $load and the data named with name_image(), as described above. The image used in the dialog created with ivib would then render correctly.

Positioning and sizing

In order to draw a component, the system needs to know its size and position on the screen. The Component class is a subclass of Rect, which gives x, y, w and h fields to provide these values; however these values need somehow to be computed. There are several possibilities.

Grid Layouts

As its name implies, a grid layout lays its child components out in a grid. Each component may take up several rows or columns in a grid, and may float within its grid cell(s), or be expanded to fill them entirely. A grid is configured by way of its components' constraints, which comprise several configurable settings stored in each component.

Constraints are not actually specific to grid layouts, since they form part of a more general layout system. They are thus stored in a table, rather than as member fields of Component. To set a constraint, use :-

c.set_constraint(key, val)

where key is a string and val is some arbitrary value which will be understood by the layout to be used.

Constraints

The full list of constraints understood by GridLayout is as follows :-

Components are initialized with a set of suitable constraints. In ivib these can be edited either in the "Constraints" tab of the component's configuration dialog :-

or via the right-click context sub-menu "Constraints"; for example :-

Gridifying a component in ivib

Ivib supports interactive design of a GridLayout. To get started, it is easiest to first layout the children by hand, but without worrying about being too precise. For example, here we have four components in a Panel :-

Now right-click on the Panel, and select the option "Gridify". The layout is changed to a GridLayout, and now appears as follows :-

(If your grid appears with more columns, and blank gaps, then press "Undo" and try making your original hand drawn layout more precise).

Note how the Panel is much smaller. This is because its size is calculated by the GridLayout, giving it the most compact form (in the panel's "Pos & Size" configuration tab, you will find that width and height are greyed out, and the "default" checkboxes ticked).

This is not obligatory however, and you can resize the panel to a larger size if you wish, simply by dragging on its corners. For example :-

Note how the two text fields have taken up the additional horizontal space. This is because the default constraints of the text field indicate that it should expand horizontally.

Adding a component

To add a component to a grid, drag it into the containing component. It will be added to the end of the grid. For example, here we have added a checkbox.

Now place it in the correct order position using the "Order Up" and "Order Down" toolbar buttons (third and fourth from the left on the second row). In the example, if we press "Up" twice, we will have.

Finally, adjust the constraints of the component as desired. In this case, we set the new component's "eol" constraint, and set its grid width to 2. Both of these can be set from the right-click sub-menu, "Constraints/Position". The final result is then :-

Expanding a cell or component

One common edit is to make a cell expand into available space. For example, we may wish the top-left button in our example expand vertically. This is done in two steps. Firstly, the cell is given a y_weight constraint of 1.0 (right-click menu option "Constraints/Y layout/Set y weight to 1.0"). This expands the cell (in fact the whole row) into the available empty space above :-

Now we wish to make the button fill its cell vertically, rather than float within its cell's space. To do this we set the constraint y_fill to &yes (menu option "Constraints/Y layout/Set y fill to 1").

Changing the alignment

Having expanded the button in our example, we now see that the adjacent text field is floating vertically in the centre of its cell. We can change this so that it aligns with the top or bottom of its cell. For example, to align at the top we set constraint y_align to t (menu "Constraints/Y layout/Set y align to t"). The result is :-

Changing the insets

As another example, we may wish to edit the insets around a component. For example, we may wish the textfield to be flush against the left edge of its cell. To do this, we set the constraint l_inset to 0 (menu "Constraints/Insets/Set l_inset to 0"). The result is :-

Setting an inset to a particular value necessitates using the component's configuration dialog and editing the value in the "Constraints" tab.

Changing the default insets

If we don't specify insets on a component in a grid, then the default insets are applied, as described in the Constraints section above. These can be edited by selecting the "Other" tab of the containing component's configuration dialog, then clicking "Edit..." next to "Layout". The following dialog appears :-

We can then alter the default inner and outer insets, or let the default values apply.

This dialog also allows configuration of the use of extra space. The default is to allocate to the grid's cells, according to weight.

Reverting to a null layout

Having "gridified" a layout, you may at some point perhaps wish to return to the default layout, where components are positioned by hand. To do this, simply select the "Other" tab of the containing component's configuration dialog :-

Then change the "Layout" setting to "&null".

Events

Components "fire" events when particular things happen, and these events can be "listened" to, so that action can be taken. Listeners are kept in a simple list, essentially of call-back procedures.

To add a listener, use the connect method, as follows :-

b := TextButton()
b.connect(callback, Event.ACTION)

Here callback should be something that may be invoked; either a procedure or a method (static or instance). Event.ACTION is the type of event to listen for. Components will fire several types of event, so this acts as a filter. If this parameter is omitted, then the listener is invoked for all events, regardless of the type.

The listener callback is invoked with three parameters, event, src and type. The first is a WinEvent structure which encapsulates the underlying graphics system event. This parameter may be null if there is no underlying event. The second is the source component firing the event, and the third is the type of event. Most of the time we know these last two parameters anyway.

If we don't care about any of the parameters, then we can use a co-expression as the callback; for example :-

b := TextButton()
b.connect(create write("pressed"), Event.ACTION)

Now a refreshed copy of the given co-expression is invoked whenever the event is fired.

Event types

The event types fired by the gui library are found as constants in the Event class. This section provides a brief summary of the various types of event and when they are fired.

The following event types are generated for all Components, based on mouse activity within it.

      MOUSE_LEFT_PRESS
      MOUSE_MIDDLE_PRESS
      MOUSE_RIGHT_PRESS
      MOUSE_LEFT_RELEASE
      MOUSE_MIDDLE_RELEASE
      MOUSE_RIGHT_RELEASE
      MOUSE_LEFT_DRAG
      MOUSE_MIDDLE_DRAG
      MOUSE_RIGHT_DRAG
      MOUSE_MOVEMENT
      MOUSE_WHEEL_UP
      MOUSE_WHEEL_DOWN
      MOUSE_LEFT_DRAG_OVER
      MOUSE_MIDDLE_DRAG_OVER
      MOUSE_RIGHT_DRAG_OVER
      MOUSE_LEFT_RELEASE_OVER
      MOUSE_MIDDLE_RELEASE_OVER
      MOUSE_RIGHT_RELEASE_OVER

The other event types are :-

Events in ivib

To add one or more listeners to a component in ivib, select the "Event" tab of the component's configuration dialog.

To add a listener, click "Add". This will add a listener for the ACTION type. If this isn't what you want, edit the details and click "Apply". When the dialog is saved, a listener method is inserted. In the example shown above, the following code is generated :-

private on_my_button(ev)
end

This method can be filled in with whatever code is required. Note that the default name for the method is generated from the component's name, so it is best to give the component a meaningful name (by editing it in the "Code" tab), before adding a listener; then the default method name will probably make sense.

Dialog

The Dialog class is a Component, which represents the top of the hierarchy of components in a dialog. It sets up and manages the underlying graphics Window instance.

In ivib, to access the dialog preferences, either right-click on a blank part of the canvas and select "Dialog", or use the menu option "Canvas/Dialog prefs".

Useful methods

To display a dialog, invoke its show() method. This displays the dialog and returns immediately. The method show_modal() on the other hand, waits until the dialog has been closed before returning. Both these methods take a parameter, d, which specifies a parent dialog; the shown dialog then is "transient" for this dialog (ie it stays above it and is minimized and restored with it). Also, when the parent is closed, the child is also closed. A program in the examples directory, called multi, demonstrates these features.

To close a dialog, invoke its dispose() method.

Three useful empty methods may be overridden in a Dialog subclass. The first is component_setup(). This is called just before the dialog is displayed. It is a good place to tweak components set up by ivib. The second is init_dialog(). This is invoked just after the dialog is displayed. At this point everything has been initialized. It is a good place to start any background tasks. Finally, the converse of init_dialog() is end_dialog(). It is invoked just before the dialog is closed, and is a good place to free any resources or to stop any background tasks.

Scaling

In order to make an interface look acceptable on monitors with varying pixel densities, a simple scaling facility is provided by the gui library. An environment variable, OI_GUI_SCALE can be set to indicate the amount of scaling to apply; the default value is calculated from the monitor resolution and size, with 96 dots per inch corresponding to a scale value of 1.0, meaning don't scale at all. A value of 2.0 would produce an interface twice as large; for example :-

OI_GUI_SCALE=2.0 ivib

In order for an interface to support with scaling, it just needs to apply the scale() procedure to any constant which is measured in pixels. For example, a text field's width should be set with something like :-

tf := TextField().
        set_width(scale(160))

ivib supports scaling. Simply select the checkbox "Scale output dimensions" in the "Size" tab of the dialog's preferences dialog. Then all pixel dimensions will have the scale() procedure applied in the output file.

Contents