Estimated time: 90 minutes.
-
-
-
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 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;
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
FrameMsg = RECORD (Objects.ObjMsg)
F: Frame; (* target frame *)
x, y, res: INTEGER
END;
- 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;
- 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.
DisplayMsg = RECORD (Display.FrameMsg)
id: INTEGER; (* frame, area *)
u, v, w, h: INTEGER
END;
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 ...
- 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
PrintMsg = RECORD (Display.FrameMsg)
id: INTEGER; (* contents, view *)
pageno: INTEGER
END;
Printing can also be done with clipping masks. All the primitives available for display masks (
InputMsg = RECORD (Display.FrameMsg)
id: INTEGER; (* track, consume *)
keys: SET;
X, Y: INTEGER;
ch: CHAR;
fnt: Fonts.Font;
col, voff: SHORTINT
END;
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;
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
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;
ControlMsg = RECORD (Display.FrameMsg)
id: INTEGER; (* defocus, neutralize, mark *)
X, Y: INTEGER
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.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;
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.
FileMsg = RECORD (ObjMsg)
id: INTEGER; (* id = load, store *)
len: LONGINT;
R: Files.Rider
END;
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;
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
CopyMsg = RECORD (ObjMsg)
id: INTEGER; (* id = shallow | deep *)
obj: Object
END;
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;
(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.
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;
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;
- 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;
- 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.
- 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;