Oberon/ETH Oberon/Tutorial/GadgetsProg

These tutorial pages were written by André Fischer (afi) with editorial assistance of Hannes Marais, were hosted at the ETHZ and remain under the ETH license. Related content is found in the system via Book.Tool. Extended content is also available on paper. Some tutorial pages are in the WayBack archive.

Oberon - Programming new gadgets

Objective edit

Supply enough information on gadgets programming to enable you to extend Oberon System 3 with new exciting applications.

Estimated time: 90 minutes.

Introduction edit

The Gadgets system builds on the basic Oberon System 3 release by adding special modules and conventions for programming user interfaces. In essence, it introduces a new type of object called a gadget. Gadgets are user interface elements that the user may combine at run-time to build user interfaces. They obey strict protocols allowing them to be embedded in many different applications. The central module is called Gadgets that provides the base classes for the system. The Gadgets module depends on further modules that provide clipping operations (Display3 and Printer3), a module to manage attributes (Attributes), and a module for special effects (Effects). On top of these modules a hierarchy of modules, each of which implements a new gadget type, exists. Many of these modules are provided standard with Oberon System 3.

Programming a new gadget type edit

Learning how to program gadgets is best done by reading the source code of simple, but fully working examples.

- Skeleton.Mod is an example of how to program a visual gadget. It implements a small colored block that can be moved, resized, copied, printed and colored.

- Complex.Mod is an example of how to program a model gadget. It implements a model gadget for complex numbers.

- DocumentSkeleton.Mod is an example of how to program a document gadget. It implements a document that consists of a panel, only the color of which is stored.

Each of these three examples can be used as a basis for creating a new, custom and application oriented gadget type: a visual, a model and a document gadget.

When programming a new gadget, you will need the following:

1 - A new type for the new gadget, usually created by extending a existing "base" type. Here is a skeleton for such an extended type declaration:

TYPE
  MyGadget* = POINTER TO MyGadgetDesc;
  MyGadgetDesc* = RECORD (BaseType)
    (* additional (private) fields *)
END;

The base type might be for example

Gadgets.FrameDesc for a visual gadget
Gadgets.ObjDesc for a model gadget
Documents.DocumentDesc for a document gadget.

When extending an existing gadget the record type of that gadget is taken as base type. To ensure that the gadget is extensible, both the record and pointer types should be exported.

2 - A message handler.

3 - A New procedure.

The New procedure edit

Creating a new instance of a gadget is like everything else in the Oberon system, done via a command. A module M contains a procedure P whose task is to dynamically allocate a new instance of a certain object type. This is called the object's New procedure. Executing the New procedure M.P (this is often referred to as generator string) causes a new instance of that object type to be created. The new object instance is initialized to a default state and is ready to accept messages (i.e. it is totally functional).

The following is a typical New procedure:

PROCEDURE New*;
  VAR F: MyGadget;
BEGIN
  NEW(F);
  (* assign message handler *)
  F.handle := MyHandler;
  (* initialize private and inherited fields of F,
    e.g. F.W, F.H for a visual gadget*)
  ...
  (* "export" the newly created gadget *)
  Objects.NewObj := F
END New;

The message handler edit

Handler is a standard Oberon message handler type for class Object and message base type ObjMsg (see Objects):

Handler = PROCEDURE (obj: Objects.Object; VAR M: Objects.ObjMsg);

In a realistic object-oriented environment, messages are rarely handled completely by the first recipient. Usually, they are passed through a complex network of objects. Thus a handler for a given gadget only handles messages which should be handled differently than in the base type. It passes all other messages on to the handler of the base type (e.g. Gadgets.framehandle for a visual gadget).

There are two important message classes in Gadgets:

- Messages derived from Display.FrameMsg: The frame messages in the Display module play a central role in interframe communication. These build a communications protocol allowing frames to communicate with each other without knowing about each other's internal working. The latter is crucial if foreign or yet unknown objects are to be integrated into the system and applications need to exchange objects with each other. The FrameMsg is defined as follows:

FrameMsg = RECORD (Objects.ObjMsg)
  F: Frame; (* target frame *)
  x, y, res: INTEGER
END;

F plays a central role in the FrameMsg. It determines the destination, or target frame of a message. Often the destination frame of a message is unknown. This happens for example when model update messages are broadcast, in which case the F field is set to NIL.

- Messages not derived from Display.FrameMsg: These messages typically can be sent directly to the receiver object, by calling its handler (obj.handle(obj, msg)). E.g. Objects.AttrMsg

A typical message handler looks like the following:

PROCEDURE MyHandler*(F: Objects.Object; VAR M: Objects.ObjMsg);
  BEGIN
     WITH F: MyGadget DO
        IF M IS Display.FrameMsg THEN
        (* only for visual gadgets - not for model gadgets *)
           WITH M: Display.FrameMsg DO
            IF (M.F = NIL) OR (M.F = F) THEN
               (* handle messages derived from Display.FrameMsg here:
                  Display.DisplayMsg, Display.ModifyMsg, Display.PrintMsg,
                  Display.SelectMsg, Display.ConsumMsg,
                  Oberon.InputMsg, Oberon.ControlMsg, ... *)
            END
         END
      ELSIF Objects.AttrMsg THEN
         (* get, set and enumerate attributes *)
      ELSIF Objects.FileMsg THEN
         (* load and store of the gadget *)
      ELSIF Objects.CopyMsg THEN
         (* making a copy of the gadget *)
      ELSE (* unknown msg, framehandler might know it *)
         Gadgets.framehandle(F, M)
      END
   END
END MyHandler;

Remarks:

- When a message is handled only partially or is not handled at all, then the handler of the base type should be called.
- To ensure that the gadget can later be extended the FrameHandler should be exported.
- Model gadgets should ignore messages of the Display.FrameMsg family.

Messages derived from Display.FrameMsg edit

Display.DisplayMsg edit

The DisplayMsg broadcasts a redraw request to a single or all frames. It is defined as follows:

DisplayMsg = RECORD (Display.FrameMsg)
   id: INTEGER; (* frame, area *)
   u, v, w, h: INTEGER
END;

When the destination (F) is NIL, all frames are implied. When id is set to Display.area, the area u, v, w, h inside the destination frame should be redrawn. These coordinates are relative to the upper-left corner of the destination gadget (thus v is normally negative).

A special display mask data structure (Display3.Mask) is used to indicate which areas of a gadget are visible. It is specified as a set of non-overlapping rectangles. Drawing primitives are issued through this mask, which has the effect of clipping them to only the visible areas of the gadget.

Handling the Display.DisplayMsg therefore might look as follows:

IF (M.F = NIL) OR (M.F = F) THEN (* message addressed to this frame *)
   (* calculate display coordinates *)
   x := M.x + F.X; y := M.y + F.Y; w := F.W; h := F.H;
   IF M IS Display.DisplayMsg THEN
      WITH M: Display.DisplayMsg DO
         IF (M.id = Display.frame) OR (M.F = NIL) THEN
            Gadgets.MakeMask(F, x, y, M.dlink, R);
            RestoreFrame(F, R, x, y, w, h)
         ELSIF M.id = Display.area THEN
            Gadgets.MakeMask(F, x, y, M.dlink, R);
            Display3.AdjustMask(R, x + M.u, y + h - 1 + M.v, M.w, M.h);
            RestoreFrame(F, R, x, y, w, h)
         END
      END
   ELSIF ...

Remarks:

- Gadgets are usually rectangular, their size being described by F.W and F.H. x, y are the coordinates of the lower-left corner of the rectangle.
- Normally the drawing routines of the Display3 module are used to draw a gadget.

Display.PrintMsg edit

This is a request to a frame to print itself. It is defined as follows:

PrintMsg = RECORD (Display.FrameMsg)
   id: INTEGER; (* contents, view *)
   pageno: INTEGER
END;

A whole tree of gadgets is implied when the destination is NIL. When the id is set to view, the frame has to print itself in the form it looks on the display. When the id is set to contents it should print its complete contents (for example a text that it may be displaying). By convention, the x, y coordinates indicate the absolute printer coordinates of the lower-left corner of the frame. The frame may assume that the printer driver has been initialized already.

Printing can also be done with clipping masks. All the primitives available for display masks (Display3), are also available for printing (Printer3). One major difference is that printing masks are stored using printer coordinates. Just like for display masks, a special routine is provided to calculate the print mask of a gadget (Gadgets.MakePrinterMask).

Oberon.InputMsg edit

This message sends mouse and keyboard input to frames. It is defined as follows:

InputMsg = RECORD (Display.FrameMsg)
   id: INTEGER; (* track, consume *)
   keys: SET;
   X, Y: INTEGER;
   ch: CHAR;
   fnt: Fonts.Font;
   col, voff: SHORTINT
END;

Tracking the mouse edit

When the Oberon event loop senses a mouse movement or that a mouse button has been pressed, it sends a track message (id = Oberon.track) to the affected viewer. The gadget can do whatever it pleases, when it receives a track message. However if possible it should abide by the Oberon conventions.

Normally, gadgets have a control border in which the gadgets respond to mouse combinations for resize, move, delete and copy. These mouse combinations are handled by Gadgets.framehandle, so the mouse has to be tracked only inside the working area of the gadgets. Gadgets.InActiveArea checks whether or not the mouse is inside the working area.

Mouse clicks are normally recorded in a tracking loop. In this loop, the mouse driver is read directly and interclicks are recorded. The loop terminates when all three buttons are up again.

Thus mouse tracking may be programmed as follows:

PROCEDURE MyHandler*(F: Objects.Object; VAR M: Objects.ObjMsg);
   ...
   ELSIF M IS Oberon.InputMsg THEN
      WITH M: Oberon.InputMsg DO
         IF (M.id = Oberon.Track) & Gadgets.InActiveArea(F, M) THEN
            TrackMouse(F, M.X, M.Y, M.keys)
   ...
END MyHandler;

PROCEDURE TrackMouse(F: MyGadget; VAR X, Y: INTEGER; VAR keysum: SET);
   VAR keys: SET;
BEGIN
   keys := keysum;
   WHILE keys # {} DO
      Effects.TrackMouse(keys, X, Y, Effects.Arrow);
      keysum := keysum+keys
   END;
   IF keysum = Effects.middle THEN
      (* execute F *)
   ELSIF ...
END TrackMouse;

Programming a caret edit

When a keyboard key is pressed, a consume message (id = Oberon.consume) is broadcast. However since the Oberon event loop does not know in which frame the caret is currently set, the recipient of the message is unknown (F = NIL). Only the frame containing the caret should consume the character.

A gadget implementing a caret typically has a BOOLEAN field indicating whether or not the caret is set. Thus the definition for MyGadgetDesc may look as follows:

MyGadgetDesc* = RECORD (Gadgets.Frame)
   caret: BOOLEAN;
   (* other data *)
END

The caret field is initialized to FALSE in the New procedure. The handling of the caret could then be implemented as follows:

PROCEDURE MyHandler*(F: Objects.Object; VAR M: Objects.ObjMsg);
   VAR x, y, w, h: INTEGER;
BEGIN
   WITH F: MyGadget DO
      IF M IS Display.FrameMsg THEN
      (* Display.FrameMsg messages *)
         WITH M: Display.FrameMsg DO
            IF (M.F = NIL) OR (M.F = F) THEN
               (* calculate display coordinates *)
               x := M.x + F.X; y := M.y + F.Y; w := F.W; h := F.H;
               IF M IS Display.DisplayMsg THEN
               ...
               ELSIF M IS Oberon.InputMsg THEN
                  WITH M: Oberon.InputMsg DO
                     IF M.id = Oberon.track THEN
                        IF (M.keys = {Effects.left}) & Gadgets.InActiveArea(F, M) THEN
                           IF ~F.caret THEN
                              Oberon.Defocus();
                              F.caret := TRUE
                           END;
                           SetCaret(F, x, y)
                        ...
                        END
                     ELSIF (M.id = Oberon.consume) & F.caret THEN
                        ConsumeChar(F, M.ch);
                        M.res := 0
                     ...
                     END
                  END
               ELSIF M IS Oberon.ControlMsg THEN
                  WITH M: Oberon.ControlMsg DO
                     IF M.id IN {Oberon.defocus, Oberon.neutralize} THEN
                        IF F.caret THEN
                           F.caret := FALSE;
                           RemoveCaret(F)
                        END
                     ...
                     END
                  END
               ...
               END
            END (* IF (M.F = NIL) OR (M.F = F) *)
         END (* WITH M: Display.FrameMsg *)
      (* other messages *)
      END
   END
END MyHandler;

Oberon.ControlMsg edit

This message changes the state of a gadget. It is defined as follows:

ControlMsg = RECORD (Display.FrameMsg)
   id: INTEGER; (* defocus, neutralize, mark *)
   X, Y: INTEGER
END;

When the destination (F) is NIL, all frames are implied. When id is set to Oberon.defocus, then the gadget should remove its caret. If id is set to Oberon.neutralize, then the gadget should remove all marks it contains (caret and selection). See Programming a caret for an example of how this message is used.

Objects messages edit

The messages of the Objects module are common to all gadgets.

Objects.AttrMsg edit

In Oberon System 3, object attribute management is done strictly by sending Objects.AttrMsg messages to objects.

Typically, for our case study example, you would handle these messages as follows:

PROCEDURE MyHandler*(F: Objects.Object; VAR M: Objects.ObjMsg);
   ...
   ELSIF M IS Objects.AttrMsg THEN THEN
      WITH M: Objects.AttrMsg DO
         IF M.id = Objects.get THEN
           
            IF M.name = "Gen" THEN
               M.class := Objects.String;
               M.s := "MyGadget.New");
               M.res := 0
            ELSIF M.name = "Color" THEN
               M.class := Objects.Int;
               M.i := F.mycol;
               M.res := 0
            ELSE Gadgets.framehandle(F, M)
            END
         ELSIF M.id = Objects.set THEN
            IF M.name = "Color" THEN
             
               IF M.class = Objects.Int THEN
                  F.mycol := SHORT(M.i);
                  M.res := 0
               ELSIF M.class = Objects.String THEN   (2a)
                  Attributes.StrToInt(M.s, M.i);
                  F.mycol := SHORT(M.i);
                  M.res := 0
               (*   ELSE   ignore *)                     (2b)
               END
            ELSE Gadgets.framehandle(F, M)
            END
         ELSIF M.id = Objects.enum THEN      (3)
            M.Enum("Color");
            Gadgets.framehandle(F, M)
         END
      END
   ...
END MyHandler;

Comments:

The object must only handle the attributes that have been added to the base type. The other attributes are processed by the base type handler.

(1) id=Objects.get, return the value of a named attribute. Each object should as a minimum handle the "Gen" attribute, i.e. return the New procedure string.

(2) id=Objects.set, change the value of a named attribute.

(3) id=Objects.enum, enumerate each attribute by calling M.Enum(extended attribute) repeatedly.

Objects.FileMsg edit

The purpose of FileMsg messages is to load and store objects from and to a sequential file.

FileMsg = RECORD (ObjMsg)
   id: INTEGER; (* id = load, store *)
   len: LONGINT;
   R: Files.Rider
END;

Typically, for our case study example, you would handle these messages as follows:

PROCEDURE MyHandler*(F: Objects.Object; VAR M: Objects.ObjMsg);
   ...
   ELSIF M IS Objects.FileMsg THEN
      WITH M: Objects.FileMsg DO
         IF M.id = Objects.store THEN         (1)
            Files.WriteInt(M.R, F.mycol)
         ELSIF M.id = Objects.load THEN      (2)
            Files.ReadInt(M.R, F.mycol)
         END;
         Gadgets.framehandle(F, M)
      END
   ...
END MyHandler;

Comments:

The object must only handle the attributes that have been added to the base type. The other attributes are processed by the base type handler.

(1) id=Objects.load, the object is requested to store its data to the file specified by the rider M.R.
(2) id=Objects.store, then the object is requested to load its data from the file specified by the rider M.R.

To keep loading and storing of objects portable among the different Oberon platforms, use the procedures of the Files module which read and write the different Oberon basic types (e.g. WriteInt, WriteString, ...).

Objects.CopyMsg edit

Messages of type CopyMsg are used to create an exact copy of a given object.

CopyMsg = RECORD (ObjMsg)
   id: INTEGER; (* id = shallow | deep *)
   obj: Object
END;

We distinguish between shallow and deep copies. When a shallow copy has to be created, as many references to original components as possible are left unresolved, whereas in the case of a deep copy, all references are resolved by recursively creating copies of the components. Note that, in both cases, the copy message is at least passed through a part the entire data structure representing the original object.

Objects.CopyMsg:

  PROCEDURE MyHandler*(F: Objects.Object; VAR M: Objects.ObjMsg);
     VAR F1: Frame;
     ...
     ELSIF M IS Objects.CopyMsg THEN
        WITH M: Objects.CopyMsg DO
           IF M.stamp = F.stamp THEN M.obj := F.dlink
              (* Copy message arrives again *)
           ELSE
              (* First time copy message arrives *)
              NEW(F1);
              F.stamp := M.stamp;   (1)
              F.dlink := F1;
              (* Copy private data *)
              F1.mycol := F.mycol;
              ...
              (* Copy data of base type *)
              Gadgets.CopyFrame(M, F, F1);
              M.obj := F1
           END
        END
     ...
  END MyHandler;

Comments:

(1) The same copy message may arrive more then once. The time stamp field is thus used to detect if a copy of the object has already been made.

Programming a new document type edit

Load and store a document edit

Documents need not to handle messages of the type Objects.FileMsg. Loading and storing of documents is done by the two procedure variable fields Load and Store of its base type (Documents.Document). Thus the New procedure for a document looks as follows:

  PROCEDURE NewDoc*;
     VAR D: Documents.Document;
  BEGIN
     NEW(D);
     (* assign procedures *)
     D.Load := Load;
     D.Store := Store;
     D.handle := DocHandler;
     D.W := 250; D.H := 200;
     Objects.NewObj := D
  END NewDoc;

Where Load is defined as follows:

  PROCEDURE Load(D: Documents.Document);
     VAR
        obj: Objects.Object;
        tag, x, y, w, h: INTEGER;
        name: ARRAY 64 OF CHAR;
        F: Files.File; R: Files.Rider;
  BEGIN
     (* create a child gadget for the document *)
     obj := Gadgets.CreateObject("Panels.NewPanel");
     WITH obj: Gadgets.Frame DO
        x := 0; y := 0; w := 250; h := 200;
        F := Files.Old(D.name);
        IF F # NIL THEN
           Files.Set(R, F, 0);
           Files.ReadInt(R, tag);
           IF tag = Documents.Id THEN
              Files.ReadString(R, name);
              Files.ReadInt(R, x); Files.ReadInt(R, y);
              Files.ReadInt(R, w); Files.ReadInt(R, h);
              (* read data specific to this document type *)
              ...
           ELSE
              (* not a document header,
              create an empty child (obj), D.name := <new doc> *)
           END
        ELSE
           (* create an empty child (obj), D.name := <new doc> *) 
        END;
        D.X := x; D.Y := y; D.W := w; D.H := h;
        Documents.Init(D, obj)
     END
  END Load;

Remarks:

- All document files have a header consisting of tag, name, x, y, w and h.
- The child gadget needs not to be a panel, any gadget can be used.

Where Store is defined as follows:

  PROCEDURE Store(D: Documents.Document);
     VAR
        obj: Gadgets.Frame;
        F: Files.File;
        R: Files.Rider;
  BEGIN
     (* get the child gadget *)
     obj := D.dsc(Gadgets.Frame);
     F := Files.New(D.name);
     Files.Set(R, F, 0);
     (* write the document header *)
     Files.WriteInt(R, Documents.Id);
     Files.WriteString(R, <gen string of this document type>);
     Files.WriteInt(R, D.X); Files.WriteInt(R, D.Y);
     Files.WriteInt(R, D.W); Files.WriteInt(R, D.H);
     (* write data specific to this document type *)
     ...
     Files.Register(F)
  END Store;

Special attributes of a document edit

Compared to all other gadgets, documents have three additional read-only attributes (see Objects.AttrMsg):

- Menu: String attribute which specifies the contents of the menu bar. The syntax for this string is:

  menu = { command [ "[" caption "]" ] " " }.
  command = moduleName "." commandName.
  caption = string.

- Icon: String attribute which specifies the icon to be used, when the document is iconized with Desktops.MakeIcons *. The string gives the full name of a picture in the Icons.Lib.

- Adaptive: Boolean attribute which specifies whether a document should dynamically change its size, when opened as Oberon viewer.

  PROCEDURE DocHandler(D: Objects.Object; VAR M: Objects.ObjMsg);
  BEGIN
     WITH D: Documents.Document DO
        IF M IS Objects.AttrMsg THEN
           WITH M: Objects.AttrMsg DO
              IF M.id = Objects.get THEN
                 IF M.name = "Gen" THEN
                    M.class := Objects.String;
                    M.s := <gen string of this document type>; M.res := 0
                 ELSIF M.name = "Adaptive" THEN
                    M.class := Objects.Bool; M.b := TRUE; M.res := 0
                 ELSIF M.name = "Icon" THEN
                    M.class := Objects.String; M.s := "Icons.Tool"; M.res := 0
                 ELSIF M.name = "Menu" THEN
                    M.class := Objects.String;
                    M.s := "Desktops.StoreDoc[Store]"; M.res := 0
                 ELSE Documents.Handler(D, M)
                 END
              ELSE Documents.Handler(D, M)
              END
           END
        ...
        ELSE Documents.Handler(D, M)
        END
     END
  END DocHandler;

Displaying a document edit

Normally there is no need to explicitly handle the Display.DisplayMsg and Display.ModifyMsg messages. Documents.Handler is responsible for delegating these messages to the menu bar and the child gadgets. However, if e.g. the size of the document is limited to a minimal or maximal size, the Display.ModifyMsg message may be changed before calling Documents.Handler.

Index edit

[ A | C | D | G | H | I | K | L | M | N | O | S | T ]

A

Adaptive

C

caret
Complex.Mod

D

destination frame
Display.DisplayMsg
Display.FrameMsg
Display.PrintMsg
Display3.Mask
Documents.DocumentDesc
DocumentSkeleton.Mod

G

Gadgets.FrameDesc
Gadgets.ObjDesc
Gen attribute

H

handler

I

Icon

K

keyboard input

L

LoadDoc

M

Menu
message
mouse tracking

N

New procedure
NewDoc

O

Oberon.ControlMsg
Oberon.InputMsg
Objects.AttrMsg
Objects.CopyMsg
Objects.FileMsg

S

Skeleton.Mod
StoreDoc

T

target frame


Revised on July 23, 1996
Installed on 30 05 1997