10.4 Using object references: ``Who's Who?''

A user interface generally consists of a large amount of UI components. Some of these are used as input devices and some as output devices. Input devices generally activate functionality in the host system. Output devices are addressed by the host system to present results. Both input- and output devices may be related to entities within the application. For example, a particular icon may be the visualisation of a file in the computer's file-system.

The application must be able to find the references to these UI components. Various techniques are available to keep track of objects in the user interface. Below we will discuss the following case:

We want to create a frame, consisting of a dialog window and a picture window. The dialog contains a menu holding fill-patterns. The picture contains a box with a popup-menu that fills the interior of the box with the currently selected fill-pattern.

To reduce the code of the individual examples, the following predicate creating the fill-pattern menu is assumed to be available:

fill_pattern(@white_image).
fill_pattern(@grey12_image).
fill_pattern(@grey25_image).
fill_pattern(@grey50_image).
fill_pattern(@grey75_image).
fill_pattern(@black_image).

make_fill_pattern_menu(M) :-
        new(M, menu(fill_pattern, marked)),
        send(M, layout, horizontal),
        forall(fill_pattern(P),
               send(M, append, menu_item(P, @default, P))).

10.4.1 Global named references

Using this approach, we will call the menu @fill_pattern_menu. It leads to the following (minimal) program:

fill_1(P) :-
        new(D, dialog('Fill 1')),
        make_fill_pattern_menu(@fill_pattern_menu),
        send(D, append, @fill_pattern_menu),
        send(new(P, picture), below, D),
        send(D, open).

add_box_1(P) :-
        send(P, display, new(B, box(100,100)), point(20,20)),
        send(B, popup, new(Pop, popup)),
        send(Pop, append,
             menu_item(fill,
                       message(B, fill_pattern,
                               @fill_pattern_menu?selection))).
1 ?-    fill_1(P),
        add_box_1(P).

This approach is straightforward. Unfortunately it has various serious disadvantages:

Global references are part of PCE to keep track of objects that are created once and will remain in existence during the entire PCE session. Examples are the predefined global objects @pce, @prolog, @display, etc. Other examples are reusable objects such as relations, messages, recognisers, methods, images to be used as fill-patterns, etc. See below:

:- pce_global(@center,
              new(spatial(xref=x+w/2, yref=y+h/2,
                          xref=x+w/2, yref=y+h/2)).
:- pce_global(@move_outline,
              new(move_outline_gesture)).

10.4.2 Using the prolog database

Dynamic predicates form another technique often used by novice PCE users. Using dynamic predicates the ``label'' would result in:

:- dynamic
        fill_pattern_menu/1.

fill_2(P) :-
        new(D, dialog('Fill 2')),
        make_fill_pattern_menu(M),
        send(D, append, M),
        asserta(fill_pattern_menu(M)),
        send(new(P, picture), below, D),
        send(D, open).

add_box_2(P) :-
        send(P, display, new(B, box(100,100)), point(20,20)),
        send(B, popup, new(Pop, popup)),
        fill_pattern_menu(M),
        send(Pop, append,
             menu_item(fill,
                       message(B, fill_pattern,
                               M?selection))).
1 ?-    fill_2(P),
        add_box_2(P).

This is not a proper way to deal with references either. First of all, it does not really solve the danger of name conflicts unless one is using Prolog modules to establish storage of the dynamic predicates local to the module that uses them. More seriously, using implicit object references, PCE assumes it is allowed to destroy the object whenever no other PCE object has a reference to it. The fill_pattern_menu/1 predicate then holds an invalid reference.

10.4.3 Using object-level attributes

PCE object-level attributes provide another approach:

fill_3(P) :-
        new(D, dialog('Fill 3')),
        make_fill_pattern_menu(M),
        send(D, append, M),
        send(new(P, picture), below, D),
        send(P, attribute, fill_pattern_menu, M),
        send(D, open).

add_box_3(P) :-
        send(P, display, new(B, box(100,100)), point(20,20)),
        send(B, popup, new(Pop, popup)),
        get(P, fill_pattern_menu, M),
        send(Pop, append,
             menu_item(fill,
                       message(B, fill_pattern,
                               M?selection))).
1 ?-    fill_3(P),
        add_box_3(P).

This approach is much better. There no longer is a potential name-conflict and PCE has access to all information it needs for proper memory management. Two disadvantages remain. First of all, the message object has a direct reference to `P' and therefore the entire recogniser object cannot be shared by multiple graphical objects (reused). Second, the code for the box assumes the picture has an attribute fill_pattern_menu and this attribute refers to a menu holding fill-patterns.

10.4.4 Using window and graphical behaviour

All graphicals in PCE have a name, and graphical devices define the method `device<-member: name' to find the (first) graphical with this name. The default name for a graphical is its class name. For dialog-items it is the label of the item. Using <-member results in:

fill_4(P) :-
        new(D, dialog('Fill 4')),
        make_fill_pattern_menu(M),
        send(D, append, M),
        send(new(P, picture), below, D),
        send(D, open).

:- pce_global(@fill_with_current_pattern,
              make_fill_with_current_pattern).

make_fill_with_current_pattern(G) :-
        new(G, popup),
        send(G, append,
             menu_item(fill,
                       message(Gr, fill_pattern,
                               ?(?(Gr?frame, member, dialog),
                                 member,
                                 fill_pattern)?selection))).

add_box_4(P) :-
        send(P, display, new(B, box(100,100)), point(20,20)),
        send(B, popup, @fill_with_current_pattern).
fill4 :-
        fill_4(P),
        add_box_4(P).

In this example we have made the recogniser generic. This saves both time and memory. Note however that this approach could be used in the previous example as well.

This example has largely the same (dis)advantages as the previous two. As an advantage, the attribute object may be omitted. The assumption here is that the frame the box is in contains a dialog which in turn contains a graphical object named `fill_pattern' that implements a <-selection method yielding an image.

10.4.5 Using user defined classes

Using user-defined classes we can hide the implementation details and make objects depend on each other in a much more organised manner.

:- pce_begin_class(fill5, frame).

initialise(F) :->
        send(F, send_super, initialise, 'Fill 5'),
        send(F, append, new(D, dialog)),
        make_fill_pattern_menu(M),
        send(D, append, M),
        send(new(picture), below, D).

current_fill_pattern(F, P:image) :<-
        get(F, member, dialog, D),
        get(D, member, fill_pattern, M),
        get(M, selection, P).

draw_box(F) :->
        get(F, member, picture, P),
        send(P, display, fillbox(100,100), point(20,20)).

:- pce_end_class.

:- pce_begin_class(fillbox, box).

:- pce_global(@fillbox_recogniser, make_fillbox_recogniser).
make_fillbox_recogniser(G) :-
        Gr = @arg1,
        new(G, popup_gesture(new(P, popup))),
        send(P, append,
             menu_item(fill,
                       message(Gr, fill_pattern,
                               Gr?frame?current_fill_pattern))).

event(B, Ev:event) :->
        (   send(B, send_super, event, Ev)
        ;   send(@fillbox_recogniser, event, Ev)
        ).
:- pce_end_class.
1 ?- send(new(F, fill5), open),
     send(F, draw_box).

The fillbox now only assumes it is contained in an application window that defines <-current_fill_pattern, while the application (the frame) hides its internal window organisation using the methods <-current_fill_pattern and ->draw_box.

10.4.6 Summary

Using global references or the Prolog database to keep track of instances in the UI is not the appropriate way. This approach quickly leads to name-conflicts, harms the memory management of PCE and makes it difficult to write reusable code.

Using attributes or user-defined classes to find (graphical) objects solves the name-conflict problems and allows PCE to perform proper memory management. It also allows multiple copies of these windows to run simultaneously. Using user-defined classes allows one to make the code more robust against later changes and allow low-level objects to be better reusable.

Large applications should carefully design the infra-structure to manage the structure of the UI components as well as the relation between UI objects and application entities. See Wielemaker & Anjewierden, 1989.

Hyper objects as described in section 10.11 form an alternative to relate objects that is suitable if dependent objects cannot rely on each other's existence.