p-Syntax12.Scn.Fnt ZParcElemsAlloc Syntax14m.Scn.Fnt/Syntax10.Scn.Fnt*P6V b Syntax12m.Scn.Fnt hSyntax12i.Scn.Fnt5 6 Z b   Y Z b A 6GraphicElemsAllocSyntax10i.Scn.Fnt+ Edit CoreSyntax10.Scn.Fnt Edit++ EditToolsB6 TextPrinter7 TextFramesRectanglesNew  H   8H 8 X @x xH @p @ @p  h  H (hElektra.Scn.Fnt > < >$ >d > <$ <d <`* ElementsSyntax10b.Scn.Fnt ...4X Command Packages ...xh`h :  :0 :p :p :t :l :d : : : :D :< :4 :p(X0 LineElemsHP (Hh`@(8P h`((PX1 ParcElemsxP  TextsP P 8P pP @ Z b  sJ ZeStyleElemsAlloc begin figure$OPTableElemsAllocSyntax10.Scn.FntParcElemsAlloc/(`(JHTableElemsAllocSSyntax10.Scn.FntParcElemsAlloc/nohead "*"/noline "*"/col "LCR"/table 1 mm = 36'000 units 1 point = 12'700 units 1 inch = 914'400 units Syntax12.Scn.FntL SSyntax10.Scn.FntParcElemsAlloc/top 8/nohead "v"/noline "hvlrb"/col "NNL"/table dots per inch units per dot examples 91 10'000 Ceres-1/2 monochrome monitors; Ceres *.Scn.Fnt files 300 3'048 Ceres laser printer; Ceres *.Pr3.Fnt and *.Lm3.Fnt files 4/top 8/nohead "*"/noline "hvlrb"/col "C"/table   . Z b C= < _ G=  \, Z b  \ N  Y|ot MV  j b pX'LineElemsAllocjSyntax10i.Scn.FntSyntax10b.Scn.Fnt      5-,2*-*E B 3)$@+'@A  h&!# ,! s  /"   7   /6 > ,! C  9 7 : :* " " O  N N <65; =/28 pX' b   # K tJ 3   E A} # ;s Z b pX'd  O)<,"&",= "  * v  n-K K pX' Z b   = o Bk _ Z b pX'p!  6 )! /   /   2,MMath12.Scn.Fntc!h2W2., pX' b . b %K _. b pX' pX' b mE j  b pX'd pX' b  R\ 8-1x* b pX'e^~ pX' Z b  Z b pX'Ld\UA pX' Z b   8"B9 Xh  b pX'Td\U@ pX' Z b    6#B9 ;h)o Z b pX'_d\UA pX' b  t  ~9 ;R |9 ;Rk Z b pX't( ai _\U{ 8A pX' Z b  79I9 ^ b pX'z ah _\k| F pX' Z b #|    }\ q Z b pX'y ah _\k|<  pX' Z b Programming Elements for the Oberon Text System 3.0 (28 Sept 93) C. Szyperski / M. Hausner Introduction This memo gives a tutorial on how to program elements for the Oberon text system. It assumes that the reader has acquired some knowledge on how to use Edit. If this is not the case it is strongly recommended to first read the Edit Guide [1]. The memo begins with an overview showing the module structure and continues with concise descriptions of the core modules. Thereafter a stepwise introduction to the programming of text elements is given. To avoid confusion it is possible to skip the module descriptions and directly start with the programming tutorial, reading the module descriptions as soon as needed. Related documents. For a description of available commands in modules Edit and EditTools, refer to the separate memo Edit.Guide.Text [1]. For a list of currently available elements, see Elem.Guide.Text [2]. The original Edit system has been described in [3]. This report is still a useful reference when looking for the design ideas behind Edit ([4] is another, more concise reference). However, many of the details are outdated by now. The Oberon system in general is documented in [5]. [1] Edit.Guide.Text - Memo, Institute for Computer Systems, ETH Zrich. [2] Elem.Guide.Text - Memo, Institute for Computer Systems, ETH Zrich. [3] Clemens A. Szyperski. Write - An Extensible Text Editor for the Oberon System. Technical Report 151, Dep. Informatik, ETH Zrich, January 1991. [4] Clemens A. Szyperski. Write-ing Applications. Designing an Extensible Text Editor as an Application Framework. Proceedings TOOLS Europe '92, Dortmund, Germany; Prentice Hall, Englewood Cliffs, NJ. March 1992. [5] Martin Reiser. The Oberon System - User Guide and Programmer's Manual. Addison-Wesley, Reading, MA. 1991. Overview This section introduces the modular structure of Edit and the distribution of abstractions to the various modules. Figure 1 illustrates the import relation of the Edit core modules (TextFrames, TextPrinter, ParcElems, and Texts), and the placement of typical extensions. The simpler type of extensions follows the generic Oberon model of adding new commands by implementing a new command package, i.e. a module exporting the new commands. The more involved type of extension adds new elements to the text system. Both kinds of extensions typically import all of the core modules plus any number of additionally needed other modules. (Module ParcElems is not normally required to implement a command or an element.)  Figure 1. Edit Core Modules and Placement of Typical Extensions. Texts Texts are generalized to be understood as sequences of attributed text_objects, where a text object is either an ordinary character or a text element. As for standard texts, the attributes are font, color, and vertical offset. Elements behave like normal characters of ASCII code 1CX when read by a Reader or a Scanner. Additionally, the Reader field elem contains a reference to the previously read element, or NIL if a normal character was read last. Hence, programs that are not aware of elements, but are well_behaved when reading a non_graphical character of the ASCII set, can process texts containing elements. For example, a program source containing elements can be compiled, as the compiler considers all non_graphical characters as white space. Hence, Texts.Read(R, ch); IF R.elem # NIL THEN ...handle element... ELSE ...handle normal character... END; can be used to operate on a text with embedded elements. An element is characterized by its bounding box (W, H) in device independent coordinates. The used unit is defined as follows:  Pixel Resolution vs. Device Independent Units. Furthermore, each element has a handler attached by means of field handle. The functionality of handlers may be compared to the frame handlers used in the Oberon viewer system. Its principles are not investigated further in this memo. By means of its handler, an element can react to messages. Texts defines the model_level messages for element filing (FileMsg), element copying (CopyMsg), and element identification (IdentifyMsg). New elements can be written to a Buffer using procedure WriteElem. Care must be taken that a particular element is not present in more than one text at more than one position at a time. (This condition is checked and may cause a trap 99 in procedure WriteElem.) The base text of an element may be retrieved using procedure ElemBase. If the element does not belong to a text, NIL is returned. When writing an element to a text that already belongs to a text, it must be copied first. Copying is done by sending a CopyMsg to the element. The copy is returned in the message field e. (Procedure CopyElem is used to implement the copy mechanism of an element. It cannot be used to copy an element directly.) Elements can be retrieved from a text by sequential reading (using the standard Read procedure), or by directly reading the next or previous element starting from a certain position in the text. The latter is done by calling ReadElem or ReadPrevElem, respectively. Finally, the standard procedures Load (or Open) and Store can be used to load and store texts containing arbitrary elements. Texts uses the identification and filing messages to implement this kind of generic loading and storing. Newly created elements during load_time are transferred to Texts using variable new. Modules that implement text elements do not register with Texts: When receiving an identify message, an element returns the module and procedure string identifying an allocation procedure; these strings are dictionary coded and written to the file. This is used upon load time (via Modules) to invoke the allocation procedure which has to allocate an element, install a handler, and assign that element to the global variable Texts.new. If this worked, a load message is sent to the newly created object; otherwise the box information is used (and the remaining element block skipped) to create an alien element. Likewise, a store message is used to ask an element to write its private data. Procedure CopyElem copies all fields defined in the base type ElemDesc. It is supposed to be called from copy procedures defined for extended types. (A record assignment is not recommened: it copies private fields, the prev and next pointers in this case.) ElemBase makes use of the fact that elements are firmly integrated by allowing an element to find out about its current host text (or NIL if there is none). TextFrames TextFrames implements a frame class capable of displaying formatted texts. TextFrames defines a large number of procedures and implements many services. This section is especially terse in describing TextFrames and the reader is referred to the programming tutorial below, where TextFrames functionality is explained in more detail when needed. TextFrames contains the screen formatter. It defines a special display prepare message (TextFrames.DisplayMsg with field prepare=TRUE) sent to elements about to be displayed. This allows for adapting to remaining space on a line and the like. Hence, an element is allowed to change its bounding box upon receipt of a display prepare message. TextFrames tries to delegate mouse clicks to elements hit by the cursor. This is done by sending a TextFrames.TrackMsg. By tracking the mouse until all mouse buttons have been released again, the element can selectively consume mouse clicks. It is strongly recommended to restrict the set of consumed mouse clicks to middle button clicks and interclicks, i.e. the command click combinations. These are undefined for elements, anyway. Consuming left or right button clicks or interclicks causes interference with the caret and selection controlling clicks interpreted by text frames. It is possible to attach elements to some external model, i.e. to use elements as views showing some shared model. Elements doing so may install a sub-frame by allocating a new frame of some appropriate class and assigning it to field TextFrames.DisplayMsg.elemFrame. To update these views, a model change should cause a notification by broadcasting a message to the viewer system. TextFrames delegates such messages to all installed sub-frames. A message derived from TextFrames.NotifyMsg has the additional property that the delegating frame adds its identity to the field TextFrames.NotifyMsg.frame. Opening a TextFrame takes besides the handler to be used and the text to be displayed a bunch of additional parameters. For each of these a default variable is exported. The parameters define the border width defined around a displayed text in a text frame (left, right, top, bot), and the scroll bar width (barW, if barW > left the scroll bar is not displayed and not available).  DEFINITION TextFrames; (* CAS/MH/HM 21.9.1993 (Rel 3.0) *) IMPORT Display, Fonts, Texts; CONST (* update message IDs *) replace = 0; insert = 1; delete = 2; (* units *) mm = 36000; Unit = 10000; (*parc options*) gridAdj = 0; leftAdj = 1; rightAdj = 2; pageBreak = 3; twoColumns = 4; (* adjust line height to multiples of the selected line height (optional); adjust paragraphs to the left (default), the right, the center (neither left nor right), or block adjust (left and right); enforce a page break before the parc when printing (optional); format using two columns (optional) *) MaxTabs = 32; TYPE Parc = POINTER TO ParcDesc; ParcDesc = RECORD (Texts.ElemDesc) left: LONGINT; (* distance from (F.X + F.left); in units *) first: LONGINT; (* first line indentation from P.left; in units *) width: LONGINT; (* parc width; in units *) lead: LONGINT; (* distance to previous line; in units *) lsp: LONGINT; (* line spacing of text after P; in units *) dsr: LONGINT; (* descender of text after P; in units *) opts: SET; nofTabs: INTEGER; tab: ARRAY MaxTabs OF LONGINT; (* in units *) END; Location = RECORD org, pos: LONGINT; x, y, dx, dy: INTEGER END; Frame = POINTER TO FrameDesc; FrameDesc = RECORD (Display.FrameDesc) text: Texts.Text; (*he displayed text*) org: LONGINT; (*position of first character displayed*) col: INTEGER; (*frame background color*) left, right, top, bot: INTEGER; (*frame border, in pixels*) markH: INTEGER; (* position of tick mark in scroll bar (< 0 => no tick mark) *) barW: INTEGER; (*scroll bar width*) time: LONGINT; (* selection time *) hasCar, hasSel, showsParcs: BOOLEAN; (*caret/selection present; parcs visible*) carloc, selbeg, selend: Location; focus: Display.Frame; (* frame of nested element if this element contains the focus *) END; DisplayMsg = RECORD (Texts.ElemMsg) prepare: BOOLEAN; fnt: Fonts.Font; col: SHORTINT; pos: LONGINT; (*position in host text*) frame: Display.Frame; (*~prepare => host frame*) X0, Y0: INTEGER; (*~prepare => receiver origin in screen space*) indent: LONGINT; (*prepare => width already consumed in line, in units*) elemFrame: Display.Frame (*optional return parameter*) END; TrackMsg = RECORD (Texts.ElemMsg) X, Y: INTEGER; keys: SET; fnt: Fonts.Font; col: SHORTINT; pos: LONGINT; (*position in host text*) frame: Display.Frame; (*host frame*) X0, Y0: INTEGER (*receiver origin in screen space*) END; FocusMsg = RECORD (Texts.ElemMsg) focus: BOOLEAN; (*whether to focus or to defocus*) elemFrame: Display.Frame; (*focus/defocus target*) frame: Display.Frame (*host frame*) END; NotifyMsg = RECORD (Display.FrameMsg) frame: Display.Frame (*host frame*) END; UpdateMsg = RECORD (Display.FrameMsg) id: INTEGER; (* replace, insert, delete *) text: Texts.Text; beg, end: LONGINT END; InsertElemMsg = RECORD (Display.FrameMsg) e: Texts.Elem; END; VAR left, right, top, bot, barW, menuH: INTEGER; (*default values used when opening a new frame*) defParc: Parc; PROCEDURE Mark (F: Frame; mark: INTEGER); (* Parcs *) PROCEDURE ParcBefore (T: Texts.Text; pos: LONGINT; VAR P: Parc; VAR beg: LONGINT); (*retrieve parc P and its position beg responsible for position pos*) (* Locators *) PROCEDURE LocatePos (F: Frame; pos: LONGINT; VAR loc: Location); PROCEDURE LocateLine (F: Frame; y: INTEGER; VAR loc: Location); PROCEDURE LocateChar (F: Frame; x, y: INTEGER; VAR loc: Location); PROCEDURE LocateWord (F: Frame; x, y: INTEGER; VAR loc: Location); PROCEDURE Pos (F: Frame; x, y: INTEGER): LONGINT; (* Caret & Selection *) PROCEDURE RemoveSelection (F: Frame); PROCEDURE SetSelection (F: Frame; beg, end: LONGINT); (*forces range to visible bounds*) PROCEDURE RemoveCaret (F: Frame); PROCEDURE SetCaret (F: Frame; pos: LONGINT); (*only done if within visible bounds*) (* Display Range *) PROCEDURE Show (F: Frame; pos: LONGINT); (*display F.text from pos; adjusts to beginning of line; removes global marks as needed and neutralizes F*) (* User Interface *) PROCEDURE TrackLine (F: Frame; VAR x, y: INTEGER; VAR org: LONGINT; VAR keysum: SET); PROCEDURE TrackWord (F: Frame; VAR x, y: INTEGER; VAR pos: LONGINT; VAR keysum: SET); PROCEDURE TrackCaret (F: Frame; VAR x, y: INTEGER; VAR keysum: SET); PROCEDURE TrackSelection (F: Frame; VAR x, y: INTEGER; VAR keysum: SET); (* General *) PROCEDURE Open (F: Frame; T: Texts.Text; pos: LONGINT); PROCEDURE Handle (f: Display.Frame; VAR msg: Display.FrameMsg); PROCEDURE NotifyDisplay (T: Texts.Text; op: INTEGER; beg, end: LONGINT); PROCEDURE Text (name: ARRAY OF CHAR): Texts.Text; PROCEDURE NewText (T: Texts.Text; pos: LONGINT): Frame; PROCEDURE NewMenu (name, commands: ARRAY OF CHAR): Frame; END TextFrames.  TextPrinter TextPrinter provides functionality similar to TextFrames, but supports printing of text portions. The print message has the same fields as the display message, except for passing no frame, and for adding a field pno giving the page number of the page the element is going to be printed on. TextPrinter encapsulates the handling of printer metrics files (*.Lm3.Fnt files): A font can be registered using the procedure FontNo, returning a code number. Font may be used to lookup the font associated with a certain code. Procedure DX(f, c) returns the printer dx of character c using font f. Get(f, c, dx, x, y, w, h) returns the character metrics in global units of c using font f. Finally, GetChar(f, u, c, p, dx, x, y, w, h) returns the metrics for unit u plus the pattern p used for screen output. This last procedure is useful when formatting characters based on printer units in order to display wysiwyg strings on screen. For example, TableElems make use of this to display tables in a wysiwyg fashion. Procedure PlaceHeader(x, y, w, pno, f, head, alt) places a header in frame (x, y, w, h), where the hight h depends on the used font f. If alt is false, or pno is odd, the page number pno is placed on the right side of the frame, and the header string head is place on the left side. Otherwise, the page number is placed on the left, and the header string on the right. Procedure PlaceBody(x, y, w, h, T, pos, pno, place) formats a page using page number pno to fill the frame (x, y, w, h) starting at pos in text T. If place is true, the page is also sent to the printer. On return, pos indicates the first character that has not fit onto the page, i.e. the first character to appear on the next page. DEFINITION TextPrinter; (* CAS 15-Oct-91 *) IMPORT Display, Fonts, Texts, TextFrames; CONST Unit = 3048; (*unit for a 300 dpi printer*) TYPE PrintMsg = RECORD (Texts.ElemMsg) prepare: BOOLEAN; indent: LONGINT; (*prepare => width already consumed in line, in units*) fnt: Fonts.Font; col: SHORTINT; pos: LONGINT; (*position in host text*) X0, Y0, pno: INTEGER (*receiver origin in screen space; page number*) END; (* Printer Metrics *) PROCEDURE FontNo (fnt: Fonts.Font): SHORTINT; (*register font fnt and try to read appropriate Lm3 file if registered for the first time*) PROCEDURE Font (fno: SHORTINT): Fonts.Font; (*lookup registered font*) PROCEDURE DX (fno: SHORTINT; ch: CHAR): LONGINT; (*dx in printer metrics*) PROCEDURE Get (fno: SHORTINT; ch: CHAR; VAR dx, x, y, w, h: LONGINT); (*character box in printer metrics*) PROCEDURE GetChar (fno: SHORTINT; targetUnit: LONGINT; ch: CHAR; VAR pdx: LONGINT; VAR dx, x, y, w, h: INTEGER; VAR pat: Display.Pattern); (*character box in metrics based on targetUnit, pdx always in global and pat always in screen units*) PROCEDURE InitFonts; (*to be called from outer Print command, only - releases registered fonts*) (* Printer Page Placement *) PROCEDURE PlaceHeader (headerX, headerY, headerW: INTEGER; pno: INTEGER; fnt: Fonts.Font; VAR header: ARRAY OF CHAR; alt: BOOLEAN); (*place a header in box headerX, headerY, headerW using font fnt*) PROCEDURE PlaceBody (bodyX, bodyY, bodyW, bodyH: INTEGER; T: Texts.Text; VAR pos: LONGINT; pno: INTEGER; place: BOOLEAN); (*format and optionally place page with number pno in box bodyX, bodyY, bodyW, bodyH; returns with pos pointing to the first character of the next page or to the end of the text*) PROCEDURE PrintDraft (t: Texts.Text; header: ARRAY OF CHAR; copies: INTEGER); END TextPrinter.  ParcElems ParcElems implements the parcs (paragraph controls) defined in module TextFrames, adding interactive manipulation features for most parc attributes. Furthermore, the parc handler installed by ParcElems supports querying and changing parc attributes using the message ParcElems.StateMsg. This is used by the commands Edit.Set and Edit.Get. Hence, extended parcs defining new attributes can be defined that in turn extend the parameters taken by Edit.Set and Edit.Get. (The standard StyleElems take advantage of this by propagating changes to all parcs in a text that have the same name.) DEFINITION ParcElems; (* CAS 15-Oct-91 *) IMPORT Display, Files, Texts, TextFrames, TextPrinter; CONST (*StateMsg.id*) set = 0; get = 1; TYPE StateMsg = RECORD (Texts.ElemMsg) id: INTEGER; pos: LONGINT; frame: TextFrames.Frame; par: Texts.Scanner; (*used to consume set/get arguments*) log: Texts.Text (*used to output set/get protocols*) END; PROCEDURE ParcExtent (T: Texts.Text; beg: LONGINT; VAR end: LONGINT); (*returns influence interval [beg, end) of parc located in T at beg*) PROCEDURE ChangedParc (P: TextFrames.Parc; beg: LONGINT); (*notify viewers of change in parc P located at beg*) PROCEDURE LoadParc (P: TextFrames.Parc; VAR r: Files.Rider); (*load parc P using rider r*) PROCEDURE StoreParc (P: TextFrames.Parc; VAR r: Files.Rider); (*store parc P using rider r*) PROCEDURE CopyParc (SP, DP: TextFrames.Parc); (*copy source parc SP into destination parc DP*) PROCEDURE Prepare (P: TextFrames.Parc; indent, unit: LONGINT); (*standard reaction to TextFrames.DisplayMsg(prepare)*) PROCEDURE Draw (P: TextFrames.Parc; F: Display.Frame; col: SHORTINT; x0, y0: INTEGER); (*standard reaction to TextFrames.DisplayMsg(Xprepare)*) PROCEDURE Edit (P: TextFrames.Parc; F: TextFrames.Frame; pos: LONGINT; x0, y0, x, y : INTEGER; keysum: SET); (*standard reaction to TextFrames.TrackMsg*) PROCEDURE SetAttr (P: TextFrames.Parc; F: TextFrames.Frame; pos: LONGINT; VAR S: Texts.Scanner; log: Texts.Text); (*use S to scan arguments and set selected attributes of P*) PROCEDURE GetAttr (P: TextFrames.Parc; F: TextFrames.Frame; VAR S: Texts.Scanner; log: Texts.Text); (*use S to scan arguments and get selected attributes of P*) PROCEDURE Handle (E: Texts.Elem; VAR msg: Texts.ElemMsg); (*handler of standard parcs and its components*) PROCEDURE Alloc; (*allocation command called by Texts when loading a parc*) END ParcElems.  How to Program Text Elements This section covers the material required to implement new text elements. Starting with almost trivial functionality, new features are added stepwise. Each step is accompanied by a small but functional example in full source form. It is recommended to understand each of the steps before proceeding. Minimal Element A minimal element needs to handle the copy message, only. It will be displayed or printed as a white area in the text, visible only indirectly by the place it takes. When storing a text containing a minimal element, the element will not be stored (as it does not handle identification and filing messages). Hence, after reloading the text, the element is gone. The implementation consists of only two procedures: A minimal handler to implement copying, and a command to allow elements of the new class to be inserted into texts. The handler uses Texts.CopyElem to implement the copying. If the defined elements would contain any further record fields, these should also be copied when handling a CopyMsg. The insertion procedure creates a new element and initializes the fields W, H, and handle. Then, it sends a TextFrames.InsertElemMsg to the current focus viewer. Note that the implementation of minimal elements does not even require the definition of a new type: As long as no new fields are needed, the type Texts.ElemDesc will do. 7 Texts.CopyMsg - the receiver creates a fully initialized copy of itself and returns it in the message field e. CopyMsg = RECORD (ElemMsg) e: Elem END;  MODULE MinimalElems; IMPORT Texts, TextFrames, Oberon; PROCEDURE Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); VAR copy: Texts.Elem; BEGIN IF msg IS Texts.CopyMsg THEN NEW(copy); Texts.CopyElem(e, copy); msg(Texts.CopyMsg).e := copy END END Handle; PROCEDURE Insert*; VAR e: Texts.Elem; M: TextFrames.InsertElemMsg; BEGIN NEW(e); e.W := 5*TextFrames.mm; e.H := e.W; e.handle := Handle; M.e := e; Oberon.FocusViewer.handle(Oberon.FocusViewer, M); END Insert; END MinimalElems.  Loading and Storing To load and store elements it is necessary to handle identification messages and to implement an allocator for the defined elements. The identification message informs module Texts about the names of allocator procedure; these names are in turn stored in a text file. When loading a text, Texts uses the names of allocator procedures to call the allocators using Oberon.Call. An allocator in turn creates a new instance of the appropriate element class and assigns it to variable Texts.new. 7 Texts.IdentifyMsg - the receiver returns the name of its allocator (module, procedure) IdentifyMsg = RECORD (ElemMsg) mod, proc: ARRAY 32 OF CHAR END;  MODULE FileableElems; IMPORT Texts, TextFrames, Oberon; PROCEDURE Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); VAR copy: Texts.Elem; BEGIN IF msg IS Texts.CopyMsg THEN NEW(copy); Texts.CopyElem(e, copy); msg(Texts.CopyMsg).e := copy ELSIF msg IS Texts.IdentifyMsg THEN WITH msg: Texts.IdentifyMsg DO msg.mod := "FileableElems"; msg.proc := "Alloc" END END END Handle; PROCEDURE Alloc*; VAR e: Texts.Elem; BEGIN NEW(e); e.handle := Handle; Texts.new := e END Alloc; PROCEDURE Insert*; VAR e: Texts.Elem; M: TextFrames.InsertElemMsg; BEGIN NEW(e); e.W := 5*TextFrames.mm; e.H := e.W; e.handle := Handle; M.e := e; Oberon.FocusViewer.handle(Oberon.FocusViewer, M); END Insert; END FileableElems.  Adding a Specific State The elements implemented so far had no specific state. To add state information to an element, a new type needs to be derived from Texts.ElemDesc. To enable loading and storing of that derived type, handling of another message used for filing elements has to be added. In the example of StatefulElems, a character array has been added to allow the defined elements to hold strings. Insert has been extended to take an argument and use it to initialize the newly created element. 7 Texts.FileMsg - depending on the message id the receiver loads or stores its state using the rider r. The state of the base type Texts.ElemDesc (W, H) is automatically loaded and stored. (The field pos is the position of the element in the hosting text.) FileMsg = RECORD (ElemMsg) id: INTEGER; (*load = 0, store = 1, other values reserved*) pos: LONGINT; r: Files.Rider END;  MODULE StatefulElems; IMPORT Files, Texts, Oberon, TextFrames; TYPE Elem = POINTER TO RECORD (Texts.ElemDesc) s: ARRAY 32 OF CHAR END; PROCEDURE Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); VAR copy: Elem; i: INTEGER; ch: CHAR; BEGIN WITH e: Elem DO IF msg IS Texts.CopyMsg THEN NEW(copy); Texts.CopyElem(e, copy); copy.s := e.s; msg(Texts.CopyMsg).e := copy ELSIF msg IS Texts.IdentifyMsg THEN WITH msg: Texts.IdentifyMsg DO msg.mod := "StatefulElems"; msg.proc := "Alloc" END ELSIF msg IS Texts.FileMsg THEN WITH msg: Texts.FileMsg DO IF msg.id = Texts.load THEN i := 0; REPEAT Files.Read(msg.r, ch); e.s[i] := ch; INC(i) UNTIL ch = 0X ELSIF msg.id = Texts.store THEN i := 0; REPEAT ch := e.s[i]; Files.Write(msg.r, ch); INC(i) UNTIL ch = 0X END END END END END Handle; PROCEDURE Alloc*; VAR e: Elem; BEGIN NEW(e); e.handle := Handle; Texts.new := e END Alloc; PROCEDURE Insert*; VAR e: Elem; s: Texts.Scanner; M: TextFrames.InsertElemMsg; BEGIN Texts.OpenScanner(s, Oberon.Par.text, Oberon.Par.pos); Texts.Scan(s); IF (s.class = Texts.String) & (s.line = 0) THEN NEW(e); e.W := 5*TextFrames.mm; e.H := e.W; e.handle := Handle; COPY(s.s, e.s); M.e := e; Oberon.FocusViewer.handle(Oberon.FocusViewer, M); END END Insert; END StatefulElems.  Structuring the Implementation Before proceeding by adding new features some remarks on the recommended structure of element implementing modules may be in order. First of all, it is a good idea to use the handler as a message dispatcher only, i.e. the handling of specific messages should be factored into a set of procedures. This makes the handler far easier to read and the implementation extensible. The latter holds if the handler and the individual handling procedures are exported: An extension implemented in a new module will define a new handler and call the old handler whenever it does not want to intercept the handling of a particular method. If it does intercept a certain message type, the extension often needs to resort to the original implementation, which it does by calling the corresponding handling procedure. Secondly, the initialization of an element should be packaged into an (possibly exported) procedure. Typically, such procedures are named Open... in the Oberon system. The module below has exactly the same functionality as the one given in the previous section, but is structured following the conventions just explained. Also, all parts that are required to make the implementation itself extensible have been exported. (Note that for making the element type itself extensible it is necessary to define and export a named record type; this may be compared to the anonymous record type used above.)  MODULE StatefulElems; IMPORT Files, Texts, Oberon, TextFrames; TYPE Elem* = POINTER TO ElemDesc; ElemDesc* = RECORD (Texts.ElemDesc) s*: ARRAY 32 OF CHAR END; PROCEDURE^ Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); PROCEDURE Alloc*; VAR e: Elem; BEGIN NEW(e); e.handle := Handle; Texts.new := e END Alloc; PROCEDURE Open* (e: Elem; s: ARRAY OF CHAR); BEGIN e.W := 5*TextFrames.mm; e.H := e.W; e.handle := Handle; COPY(s, e.s) END Open; PROCEDURE Copy* (se, de: Elem); BEGIN Texts.CopyElem(se, de); de.s := se.s END Copy; PROCEDURE Load* (e: Elem; VAR r: Files.Rider); VAR i: INTEGER; ch: CHAR; BEGIN i := 0; REPEAT Files.Read(r, ch); e.s[i] := ch; INC(i) UNTIL ch = 0X END Load; PROCEDURE Store* (e: Elem; VAR r: Files.Rider); VAR i: INTEGER; ch: CHAR; BEGIN i := 0; REPEAT ch := e.s[i]; Files.Write(r, ch); INC(i) UNTIL ch = 0X END Store; PROCEDURE Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); VAR copy: Elem; BEGIN WITH e: Elem DO IF msg IS Texts.CopyMsg THEN NEW(copy); Copy(e, copy); msg(Texts.CopyMsg).e := copy ELSIF msg IS Texts.IdentifyMsg THEN WITH msg: Texts.IdentifyMsg DO msg.mod := "StatefulElems"; msg.proc := "Alloc" END ELSIF msg IS Texts.FileMsg THEN WITH msg: Texts.FileMsg DO IF msg.id = Texts.load THEN Load(e, msg.r) ELSIF msg.id = Texts.store THEN Store(e, msg.r) END END END END END Handle; PROCEDURE Insert*; VAR e: Elem; s: Texts.Scanner; M: TextFrames.InsertElemMsg; BEGIN Texts.OpenScanner(s, Oberon.Par.text, Oberon.Par.pos); Texts.Scan(s); IF (s.class = Texts.String) & (s.line = 0) THEN NEW(e); Open(e, s.s); M.e := e; Oberon.FocusViewer.handle(Oberon.FocusViewer, M); END END Insert; END StatefulElems.  Displayable Elements The elements developed so far are quite unusual as they have no visual representation. Adding visual representation is done by handling another message. This time the message is device specific (corresponds to the display) and hence is defined in module TextFrames instead of Texts. In a first step the visual representation of the element is chosen to be particularly simple: the area taken by the element is simply filled with a grey pattern. 7 TextFrames.DisplayMsg - If Xprepare, the receiver is asked to display itself (using module Display) at absolute screen coordinates (X0, Y0). The text attributes set for the receiving element are given by fnt and col; if set, a vertical offset is cumulated into coordinate Y0. The field pos is the position of the element in the hosting text. The hosting frame is available via frame. Field indent is not valid when Xprepare holds. Finally, an element may install a subframe by creating a new frame and returning it using field elemFrame. (This is explained in an example further below.) DisplayMsg = RECORD (Texts.ElemMsg) prepare: BOOLEAN; fnt: Fonts.Font; col: SHORTINT; pos: LONGINT; frame: Display.Frame; X0, Y0: INTEGER; indent: LONGINT; elemFrame: Display.Frame END;  MODULE VisibleElems; IMPORT Files, Display, Texts, Oberon, TextFrames; TYPE Elem* = POINTER TO ElemDesc; ElemDesc* = RECORD (Texts.ElemDesc) s*: ARRAY 32 OF CHAR END; PROCEDURE^ Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); PROCEDURE Alloc*; VAR e: Elem; BEGIN NEW(e); e.handle := Handle; Texts.new := e END Alloc; PROCEDURE Open* (e: Elem; s: ARRAY OF CHAR); BEGIN e.W := 5*TextFrames.mm; e.H := e.W; e.handle := Handle; COPY(s, e.s) END Open; PROCEDURE Copy* (se, de: Elem); BEGIN Texts.CopyElem(se, de); de.s := se.s END Copy; PROCEDURE Load* (e: Elem; VAR r: Files.Rider); VAR i: INTEGER; ch: CHAR; BEGIN i := 0; REPEAT Files.Read(r, ch); e.s[i] := ch; INC(i) UNTIL ch = 0X END Load; PROCEDURE Store* (e: Elem; VAR r: Files.Rider); VAR i: INTEGER; ch: CHAR; BEGIN i := 0; REPEAT ch := e.s[i]; Files.Write(r, ch); INC(i) UNTIL ch = 0X END Store; PROCEDURE Draw* (e: Elem; col, x0, y0: INTEGER); VAR w, h: INTEGER; BEGIN w := SHORT(e.W DIV TextFrames.Unit); h := SHORT(e.H DIV TextFrames.Unit); Display.ReplPattern(col, Display.grey1, x0, y0, w, h, Display.replace) END Draw; PROCEDURE Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); VAR copy: Elem; BEGIN WITH e: Elem DO IF msg IS Texts.CopyMsg THEN NEW(copy); Copy(e, copy); msg(Texts.CopyMsg).e := copy ELSIF msg IS Texts.IdentifyMsg THEN WITH msg: Texts.IdentifyMsg DO msg.mod := "VisibleElems"; msg.proc := "Alloc" END ELSIF msg IS Texts.FileMsg THEN WITH msg: Texts.FileMsg DO IF msg.id = Texts.load THEN Load(e, msg.r) ELSIF msg.id = Texts.store THEN Store(e, msg.r) END END ELSIF msg IS TextFrames.DisplayMsg THEN WITH msg: TextFrames.DisplayMsg DO IF ~msg.prepare THEN Draw(e, msg.col, msg.X0, msg.Y0) END END END END END Handle; PROCEDURE Insert*; VAR e: Elem; s: Texts.Scanner; M: TextFrames.InsertElemMsg; BEGIN Texts.OpenScanner(s, Oberon.Par.text, Oberon.Par.pos); Texts.Scan(s); IF (s.class = Texts.String) & (s.line = 0) THEN NEW(e); Open(e, s.s); M.e := e; Oberon.FocusViewer.handle(Oberon.FocusViewer, M); END END Insert; END VisibleElems.  Printable Elements Besides displaying an element it is useful to have a printed representation. To implement priniting it is necessary to handle the print message defined by module TextPrinter. It is very similar to the display message introduced above, but contains fewer fields, as certain features designed for interaction (like subframes) make no sense when printing. Also, the print message assumes that the receiving element uses modules Printer (and perhaps TextPrinter) instead of Display to output its representation. The example below uses a grey pattern to print itself that it similar to the one used for displaying. 7 TextPrinter.PrintMsg - If Xprepare, the receiver is asked to print itself (using module Printer) at absolute printer coordinates (X0, Y0). The text attributes set for the receiving element are given by fnt and col; if set, a vertical offset is cumulated into coordinate Y0. The field pos is the position of the element in the hosting text. Field indent is not valid when Xprepare holds. The number of the page that is used for the page the element will be printed on is indicated by pno. DisplayMsg = RECORD (Texts.ElemMsg) prepare: BOOLEAN; indent: LONGINT; fnt: Fonts.Font; col: SHORTINT; pos: LONGINT; X0, Y0, pno: INTEGER END;  MODULE PrintableElems; IMPORT Files, Display, Texts, Oberon, Printer, TextFrames; TYPE Elem* = POINTER TO ElemDesc; ElemDesc* = RECORD (Texts.ElemDesc) s*: ARRAY 32 OF CHAR END; PROCEDURE^ Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); PROCEDURE Alloc*; VAR e: Elem; BEGIN NEW(e); e.handle := Handle; Texts.new := e END Alloc; PROCEDURE Open* (e: Elem; s: ARRAY OF CHAR); BEGIN e.W := 5*TextFrames.mm; e.H := e.W; e.handle := Handle; COPY(s, e.s) END Open; PROCEDURE Copy* (se, de: Elem); BEGIN Texts.CopyElem(se, de); de.s := se.s END Copy; PROCEDURE Load* (e: Elem; VAR r: Files.Rider); VAR i: INTEGER; ch: CHAR; BEGIN i := 0; REPEAT Files.Read(r, ch); e.s[i] := ch; INC(i) UNTIL ch = 0X END Load; PROCEDURE Store* (e: Elem; VAR r: Files.Rider); VAR i: INTEGER; ch: CHAR; BEGIN i := 0; REPEAT ch := e.s[i]; Files.Write(r, ch); INC(i) UNTIL ch = 0X END Store; PROCEDURE Draw* (e: Elem; col, x0, y0: INTEGER); VAR w, h: INTEGER; BEGIN w := SHORT(e.W DIV TextFrames.Unit); h := SHORT(e.H DIV TextFrames.Unit); Display.ReplPattern(col, Display.grey1, x0, y0, w, h, Display.replace) END Draw; PROCEDURE Print* (e: Elem; col, x0, y0: INTEGER); VAR w, h: INTEGER; BEGIN w := SHORT(e.W DIV TextPrinter.Unit); h := SHORT(e.H DIV TextPrinter.Unit); Printer.ReplPattern(x0, y0, w, h, 2) END Print; PROCEDURE Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); VAR copy: Elem; BEGIN WITH e: Elem DO IF msg IS Texts.CopyMsg THEN NEW(copy); Copy(e, copy); msg(Texts.CopyMsg).e := copy ELSIF msg IS Texts.IdentifyMsg THEN WITH msg: Texts.IdentifyMsg DO msg.mod := "PrintableElems"; msg.proc := "Alloc" END ELSIF msg IS Texts.FileMsg THEN WITH msg: Texts.FileMsg DO IF msg.id = Texts.load THEN Load(e, msg.r) ELSIF msg.id = Texts.store THEN Store(e, msg.r) END END ELSIF msg IS TextFrames.DisplayMsg THEN WITH msg: TextFrames.DisplayMsg DO IF ~msg.prepare THEN Draw(e, msg.col, msg.X0, msg.Y0) END END ELSIF msg IS TextPrinter.PrintMsg THEN WITH msg: TextPrinter.PrintMsg DO IF ~msg.prepare THEN Print(e, msg.col, msg.X0, msg.Y0) END END END END END Handle; PROCEDURE Insert*; VAR e: Elem; s: Texts.Scanner; M: TextFrames.InsertElemMsg; BEGIN Texts.OpenScanner(s, Oberon.Par.text, Oberon.Par.pos); Texts.Scan(s); IF (s.class = Texts.String) & (s.line = 0) THEN NEW(e); Open(e, s.s); M.e := e; Oberon.FocusViewer.handle(Oberon.FocusViewer, M); END END Insert; END PrintableElems.  Adapting to the Environment An element may compute its bounding box instead of having it fixed. This can be used to have a printed form that has different size than the displayed form. For example, an element may be visible on screen, but invisible on paper by having a printed form of zero width and height. Also, an element may decide to fill the remaining space in a line, or all space to the next tabulator stop. All these cases are handled by implementing special reactions to the TextFrames.DisplayMsg and TextPrinter.PrintMsg when the prepare flag is set. The example below handles the case where the width of the element is different when printing it. The Draw and Print procedures have been changed to display the string hold by the element in an underlined fashion. To do so, the procedures StrDispWidth and StrPrntWidth have been added which use information provided by modules Display and TextPrinter to compute the width of a string when displayed or printed, respectively. (Note that the width of an element is reset after printing it. This avoids problems with tools directly working on a displayed text, as such tools may inspect and use the width stored in field W of an element.) 7 TextFrames.DisplayMsg - If prepare, the receiver is asked to prepare itself for being displayed. The text attributes set for the receiving element are given by fnt and col; if set, a vertical offset is cumulated into coordinate Y0. The field pos is the position of the element in the hosting text. Field indent indicates the space (in units) already taken in the currently casted line. Field Y0 is set to the base line offset that will be applied to the element. (This value may be changed by the element to affect the resulting base line offset and line heights.) Fields frame, X0 and elemFrame are undefined if prepare holds. Note that the prepare message may be received more than once before the element receives the correspoding display message. This is due to line breaks caused when trying to place the element. DisplayMsg = RECORD (Texts.ElemMsg) prepare: BOOLEAN; fnt: Fonts.Font; col: SHORTINT; pos: LONGINT; frame: TextFrames.Frame; X0, Y0: INTEGER; indent: LONGINT; elemFrame: Display.Frame END; 7 TextPrinter.PrintMsg - If prepare, the receiver is asked to prepare itself for being printed. The text attributes set for the receiving element are given by fnt and col; if set, a vertical offset is cumulated into coordinate Y0. The field pos is the position of the element in the hosting text. Field indent indicates the space (in units) already taken in the currently casted line. Field Y0 is set to the base line offset that will be applied to the element. (This value may be changed by the element to affect the resulting base line offset and line heights.) The number of the page that the element is expected to be printed on is indicated by pno. Field X0 is undefined if prepare holds. Note that the prepare message may be received more than once before the element receives the correspoding print message. This is due to line and page breaks caused when trying to place the element. DisplayMsg = RECORD (Texts.ElemMsg) prepare: BOOLEAN; indent: LONGINT; fnt: Fonts.Font; col: SHORTINT; pos: LONGINT; X0, Y0, pno: INTEGER END;  MODULE UnderlineElems0; IMPORT Files, Display, Fonts, Texts, Oberon, Printer, TextFrames, TextPrinter; TYPE Elem* = POINTER TO ElemDesc; ElemDesc* = RECORD (Texts.ElemDesc) s*: ARRAY 32 OF CHAR END; PROCEDURE StrDispWidth* (fnt: Fonts.Font; s: ARRAY OF CHAR): LONGINT; VAR pat: Display.Pattern; width, i, dx, x, y, w, h: INTEGER; ch: CHAR; BEGIN width := 0; i := 0; ch := s[i]; WHILE ch # 0X DO Display.GetChar(fnt.raster, ch, dx, x, y, w, h, pat); INC(width, dx); INC(i); ch := s[i] END; RETURN LONG(width) * TextFrames.Unit END StrDispWidth; PROCEDURE DispStr* (fnt: Fonts.Font; s: ARRAY OF CHAR; col, x0, y0: INTEGER); VAR pat: Display.Pattern; i, dx, x, y, w, h: INTEGER; ch: CHAR; BEGIN i := 0; ch := s[i]; WHILE ch # 0X DO Display.GetChar(fnt.raster, ch, dx, x, y, w, h, pat); Display.CopyPattern(col, pat, x0+x, y0+y, Display.replace); INC(i); ch := s[i]; INC(x0, dx) END END DispStr; PROCEDURE StrPrntWidth* (fnt: Fonts.Font; s: ARRAY OF CHAR): LONGINT; VAR width, dx, x, y, w, h: LONGINT; i: INTEGER; fno: SHORTINT; ch: CHAR; BEGIN width := 0; fno := TextPrinter.FontNo(fnt); i := 0; ch := s[i]; WHILE ch # 0X DO TextPrinter.Get(fno, ch, dx, x, y, w, h); INC(width, dx); INC(i); ch := s[i] END; RETURN width END StrPrntWidth; PROCEDURE PrntStr* (fnt: Fonts.Font; s: ARRAY OF CHAR; x0, y0: INTEGER); BEGIN Printer.String(x0, y0, s, fnt.name) END PrntStr; PROCEDURE^ Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); PROCEDURE Alloc*; VAR e: Elem; BEGIN NEW(e); e.handle := Handle; Texts.new := e END Alloc; PROCEDURE Open* (e: Elem; s: ARRAY OF CHAR); BEGIN e.W := 5*TextFrames.mm; e.H := e.W; e.handle := Handle; COPY(s, e.s) END Open; PROCEDURE Copy* (se, de: Elem); BEGIN Texts.CopyElem(se, de); de.s := se.s END Copy; PROCEDURE Load* (e: Elem; VAR r: Files.Rider); VAR i: INTEGER; ch: CHAR; BEGIN i := 0; REPEAT Files.Read(r, ch); e.s[i] := ch; INC(i) UNTIL ch = 0X END Load; PROCEDURE Store* (e: Elem; VAR r: Files.Rider); VAR i: INTEGER; ch: CHAR; BEGIN i := 0; REPEAT ch := e.s[i]; Files.Write(r, ch); INC(i) UNTIL ch = 0X END Store; PROCEDURE PrepDraw* (e: Elem; fnt: Fonts.Font; VAR dy: INTEGER); BEGIN e.W := StrDispWidth(fnt, e.s); e.H := LONG(fnt.height) * TextFrames.Unit; dy := fnt.minY; IF dy > -2 THEN dy := -2 END END PrepDraw; PROCEDURE Draw* (e: Elem; pos: LONGINT; fnt: Fonts.Font; col, x0, y0: INTEGER); VAR p: TextFrames.Parc; beg: LONGINT; w: INTEGER; BEGIN w := SHORT(e.W DIV TextFrames.Unit); TextFrames.ParcBefore(Texts.ElemBase(e), pos, p, beg); INC(y0, SHORT(p.dsr DIV TextFrames.Unit)); DispStr(fnt, e.s, col, x0, y0); Display.ReplConst(col, x0, y0 - 2, w, 1, Display.replace) END Draw; PROCEDURE PrepPrint* (e: Elem; fnt: Fonts.Font; VAR dy: INTEGER); BEGIN e.W := StrPrntWidth(fnt, e.s); e.H := LONG(fnt.height) * TextPrinter.Unit; dy := SHORT(LONG(fnt.minY) * TextFrames.Unit DIV TextPrinter.Unit); IF dy > -2 THEN dy := -2 END END PrepPrint; PROCEDURE Print* (e: Elem; pos: LONGINT; fnt: Fonts.Font; x0, y0: INTEGER); VAR p: TextFrames.Parc; beg: LONGINT; w: INTEGER; BEGIN w := SHORT(e.W DIV TextPrinter.Unit); TextFrames.ParcBefore(Texts.ElemBase(e), pos, p, beg); INC(y0, SHORT(p.dsr DIV TextPrinter.Unit)); PrntStr(fnt, e.s, x0, y0); Printer.ReplConst(x0, y0 - 2, w, 1); e.W := StrDispWidth(fnt, e.s) END Print; PROCEDURE Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); VAR copy: Elem; BEGIN WITH e: Elem DO IF msg IS Texts.CopyMsg THEN NEW(copy); Copy(e, copy); msg(Texts.CopyMsg).e := copy ELSIF msg IS Texts.IdentifyMsg THEN WITH msg: Texts.IdentifyMsg DO msg.mod := "UnderlineElems0"; msg.proc := "Alloc" END ELSIF msg IS Texts.FileMsg THEN WITH msg: Texts.FileMsg DO IF msg.id = Texts.load THEN Load(e, msg.r) ELSIF msg.id = Texts.store THEN Store(e, msg.r) END END ELSIF msg IS TextFrames.DisplayMsg THEN WITH msg: TextFrames.DisplayMsg DO IF msg.prepare THEN PrepDraw(e, msg.fnt, msg.Y0) ELSE Draw(e, msg.pos, msg.fnt, msg.col, msg.X0, msg.Y0) END END ELSIF msg IS TextPrinter.PrintMsg THEN WITH msg: TextPrinter.PrintMsg DO IF msg.prepare THEN PrepPrint(e, msg.fnt, msg.Y0) ELSE Print(e, msg.pos, msg.fnt, msg.X0, msg.Y0) END END END END END Handle; PROCEDURE Insert*; VAR e: Elem; s: Texts.Scanner; M: TextFrames.InsertElemMsg; BEGIN Texts.OpenScanner(s, Oberon.Par.text, Oberon.Par.pos); Texts.Scan(s); IF (s.class = Texts.String) & (s.line = 0) THEN NEW(e); Open(e, s.s); M.e := e; Oberon.FocusViewer.handle(Oberon.FocusViewer, M); END END Insert; END UnderlineElems0.  Dealing with Mouse Clicks The UnderlineElems developed above are already quite useful and well behaved elements. In a next step the elements are refined to react to mouse clicks. To do so, it is sufficient to handle the tracking message defined in TextFrames. The example below interprets a middle mouse click to toggle the state of the element between underlined and normal display of the encapsulated string. 7 TextFrames.TrackMsg - The receiving element, based at screen coordinates (X0, Y0) is asked to handle a mouse click at screen coordinate (X, Y) with keys pressed. The text attributes set for the receiving element are given by fnt and col; if set, a vertical offset is cumulated into coordinate Y0. The field pos is the position of the element in the hosting text. The hosting frame can be referred to via frame. TrackMsg = RECORD (Texts.ElemMsg) X, Y: INTEGER; keys: SET; fnt: Fonts.Font; col: SHORTINT; pos: LONGINT; frame: Display.Frame; X0, Y0: INTEGER; END;  MODULE UnderlineElems; IMPORT Files, Input, Display, Fonts, Texts, Oberon, Printer, TextFrames, TextPrinter; TYPE Elem* = POINTER TO ElemDesc; ElemDesc* = RECORD (Texts.ElemDesc) s*: ARRAY 32 OF CHAR; uline*: BOOLEAN END; PROCEDURE StrDispWidth* (fnt: Fonts.Font; s: ARRAY OF CHAR): LONGINT; VAR pat: Display.Pattern; width, i, dx, x, y, w, h: INTEGER; ch: CHAR; BEGIN width := 0; i := 0; ch := s[i]; WHILE ch # 0X DO Display.GetChar(fnt.raster, ch, dx, x, y, w, h, pat); INC(width, dx); INC(i); ch := s[i] END; RETURN LONG(width) * TextFrames.Unit END StrDispWidth; PROCEDURE DispStr* (fnt: Fonts.Font; s: ARRAY OF CHAR; col, x0, y0: INTEGER); VAR pat: Display.Pattern; i, dx, x, y, w, h: INTEGER; ch: CHAR; BEGIN i := 0; ch := s[i]; WHILE ch # 0X DO Display.GetChar(fnt.raster, ch, dx, x, y, w, h, pat); Display.CopyPattern(col, pat, x0+x, y0+y, Display.replace); INC(i); ch := s[i]; INC(x0, dx) END END DispStr; PROCEDURE StrPrntWidth* (fnt: Fonts.Font; s: ARRAY OF CHAR): LONGINT; VAR width, dx, x, y, w, h: LONGINT; i: INTEGER; fno: SHORTINT; ch: CHAR; BEGIN width := 0; fno := TextPrinter.FontNo(fnt); i := 0; ch := s[i]; WHILE ch # 0X DO TextPrinter.Get(fno, ch, dx, x, y, w, h); INC(width, dx); INC(i); ch := s[i] END; RETURN width END StrPrntWidth; PROCEDURE PrntStr* (fnt: Fonts.Font; s: ARRAY OF CHAR; x0, y0: INTEGER); BEGIN Printer.String(x0, y0, s, fnt.name) END PrntStr; PROCEDURE^ Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); PROCEDURE Alloc*; VAR e: Elem; BEGIN NEW(e); e.handle := Handle; Texts.new := e END Alloc; PROCEDURE Open* (e: Elem; s: ARRAY OF CHAR; uline: BOOLEAN); BEGIN e.W := 5*TextFrames.mm; e.H := e.W; e.handle := Handle; COPY(s, e.s); e.uline := uline END Open; PROCEDURE Copy* (se, de: Elem); BEGIN Texts.CopyElem(se, de); de.s := se.s; de.uline := se.uline END Copy; PROCEDURE Load* (e: Elem; VAR r: Files.Rider); VAR i: INTEGER; ch: CHAR; BEGIN i := 0; REPEAT Files.Read(r, ch); e.s[i] := ch; INC(i) UNTIL ch = 0X; Files.Read(r, ch); IF ch = 0X THEN e.uline := FALSE ELSE e.uline := TRUE END END Load; PROCEDURE Store* (e: Elem; VAR r: Files.Rider); VAR i: INTEGER; ch: CHAR; BEGIN i := 0; REPEAT ch := e.s[i]; Files.Write(r, ch); INC(i) UNTIL ch = 0X; IF e.uline THEN Files.Write(r, 1X) ELSE Files.Write(r, 0X) END END Store; PROCEDURE Changed* (e: Elem; pos: LONGINT); BEGIN Texts.ChangeLooks(Texts.ElemBase(e), pos, pos+1, {}, NIL, 0, 0) END Changed; PROCEDURE PrepDraw* (e: Elem; fnt: Fonts.Font; VAR dy: INTEGER); BEGIN e.W := StrDispWidth(fnt, e.s); e.H := LONG(fnt.height) * TextFrames.Unit; dy := fnt.minY; IF dy > -2 THEN dy := -2 END END PrepDraw; PROCEDURE Draw* (e: Elem; pos: LONGINT; fnt: Fonts.Font; col, x0, y0: INTEGER); VAR p: TextFrames.Parc; beg: LONGINT; w: INTEGER; BEGIN w := SHORT(e.W DIV TextFrames.Unit); TextFrames.ParcBefore(Texts.ElemBase(e), pos, p, beg); INC(y0, SHORT(p.dsr DIV TextFrames.Unit)); DispStr(fnt, e.s, col, x0, y0); IF e.uline THEN Display.ReplConst(col, x0, y0 - 2, w, 1, Display.replace) END END Draw; PROCEDURE PrepPrint* (e: Elem; fnt: Fonts.Font; VAR dy: INTEGER); BEGIN e.W := StrPrntWidth(fnt, e.s); e.H := LONG(fnt.height) * TextPrinter.Unit; dy := SHORT(LONG(fnt.minY) * TextFrames.Unit DIV TextPrinter.Unit); IF dy > -2 THEN dy := -2 END END PrepPrint; PROCEDURE Print* (e: Elem; pos: LONGINT; fnt: Fonts.Font; x0, y0: INTEGER); VAR p: TextFrames.Parc; beg: LONGINT; w: INTEGER; BEGIN w := SHORT(e.W DIV TextPrinter.Unit); TextFrames.ParcBefore(Texts.ElemBase(e), pos, p, beg); INC(y0, SHORT(p.dsr DIV TextPrinter.Unit)); PrntStr(fnt, e.s, x0, y0); IF e.uline THEN Printer.ReplConst(x0, y0 - 2, w, 1) END; e.W := StrDispWidth(fnt, e.s) END Print; PROCEDURE Track* (e: Elem; pos: LONGINT; x, y: INTEGER; keys: SET); VAR keysum: SET; BEGIN IF keys = {1} THEN keysum := keys; REPEAT Oberon.DrawCursor(Oberon.Mouse, Oberon.Arrow, x, y); Input.Mouse(keys, x, y); keysum := keysum + keys UNTIL keys = {}; IF keysum = {1} THEN e.uline := ~e.uline; Changed(e, pos) END END END Track; PROCEDURE Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); VAR copy: Elem; BEGIN WITH e: Elem DO IF msg IS Texts.CopyMsg THEN NEW(copy); Copy(e, copy); msg(Texts.CopyMsg).e := copy ELSIF msg IS Texts.IdentifyMsg THEN WITH msg: Texts.IdentifyMsg DO msg.mod := "UnderlineElems"; msg.proc := "Alloc" END ELSIF msg IS Texts.FileMsg THEN WITH msg: Texts.FileMsg DO IF msg.id = Texts.load THEN Load(e, msg.r) ELSIF msg.id = Texts.store THEN Store(e, msg.r) END END ELSIF msg IS TextFrames.DisplayMsg THEN WITH msg: TextFrames.DisplayMsg DO IF msg.prepare THEN PrepDraw(e, msg.fnt, msg.Y0) ELSE Draw(e, msg.pos, msg.fnt, msg.col, msg.X0, msg.Y0) END END ELSIF msg IS TextPrinter.PrintMsg THEN WITH msg: TextPrinter.PrintMsg DO IF msg.prepare THEN PrepPrint(e, msg.fnt, msg.Y0) ELSE Print(e, msg.pos, msg.fnt, msg.X0, msg.Y0) END END ELSIF msg IS TextFrames.TrackMsg THEN WITH msg: TextFrames.TrackMsg DO Track(e, msg.pos, msg.X, msg.Y, msg.keys) END END END END Handle; PROCEDURE Insert*; VAR e: Elem; s: Texts.Scanner; M: TextFrames.InsertElemMsg; BEGIN Texts.OpenScanner(s, Oberon.Par.text, Oberon.Par.pos); Texts.Scan(s); IF (s.class = Texts.String) & (s.line = 0) THEN NEW(e); Open(e, s.s, TRUE); M.e := e; Oberon.FocusViewer.handle(Oberon.FocusViewer, M); END END Insert; END UnderlineElems.  Inplace Editing and Active Elements The final refinement of UnderlineElems introduces inplace editing, i.e. the capability to edit the contents of an element in situ. To do so in a way that follows the Oberon model, an element can install a subframe into its hosting text frame. This is done by extending the element's reaction to a DisplayMsg. Each time a display message is received the element creates a new frame, sets it up properly, and returns it in message field elemFrame. This field is initialized to NIL. If the sending text frame finds it to be non_NIL on return, it installs the passed frame into its descender list of frames. Then the user may choose to use a left mouse click to focus one of the installed subframes. A focussed subframe is framed using a grey pattern to make the focus state clearly visible. To control the process of focussing and de_focussing, an element can interpret the TextFrames.FocusMsg. A focussed subframe receives all messages broadcasted to the viewer system or sent to the hosting text frame, unless intercepted by the text frame. Hence, any existing frame implementation can be used. (Note: TextFrames currently does not support nesting - hence a text frame currently cannot be installed as a subframe.) Also, a minimalistic frame holding only a very simple handler can be used to implement elements that need to receive broadcast messages when visible. This can be used to implement active elements by having a task broadcast a special message, say once a second. All visible elements that have installed a subframe will receive this message, and - if they know the message type - can react to it. For example, the ClockElems and IconElems modules work this way. The example below uses a minimal frame to receive broadcast messages for simple animation purposes. 7 TextFrames.FocusMsg - The receiver's subframe is about to be focussed or defocussed, depending on the value of focus. The subframe is indicated by elemFrame, the host frame by frame. FocusMsg = RECORD (Texts.ElemMsg) focus: BOOLEAN; elemFrame: Display.Frame; frame: Display.Frame; END;  MODULE FancyElems; IMPORT Files, Input, Display, Viewers, Fonts, Texts, Oberon, Printer, TextFrames, TextPrinter; TYPE Elem* = POINTER TO ElemDesc; ElemDesc* = RECORD (Texts.ElemDesc) s*: ARRAY 32 OF CHAR; uline*: BOOLEAN END; TickMsg = RECORD (Display.FrameMsg) END; VAR next: LONGINT; task: Oberon.Task; PROCEDURE Ticker; VAR msg: TickMsg; BEGIN IF Oberon.Time() > next THEN next := Oberon.Time() + 100; Viewers.Broadcast(msg) END END Ticker; PROCEDURE HandleFrame (f: Display.Frame; VAR msg: Display.FrameMsg); BEGIN IF msg IS TickMsg THEN Display.ReplConst(Display.white, f.X, f.Y, f.W, f.H, Display.invert) ELSIF msg IS Oberon.InputMsg THEN WITH msg: Oberon.InputMsg DO IF msg.id = Oberon.track THEN Oberon.DrawCursor(Oberon.Mouse, Oberon.Arrow, msg.X, msg.Y) END END END END HandleFrame; PROCEDURE StrDispWidth* (fnt: Fonts.Font; s: ARRAY OF CHAR): LONGINT; VAR pat: Display.Pattern; width, i, dx, x, y, w, h: INTEGER; ch: CHAR; BEGIN width := 0; i := 0; ch := s[i]; WHILE ch # 0X DO Display.GetChar(fnt.raster, ch, dx, x, y, w, h, pat); INC(width, dx); INC(i); ch := s[i] END; RETURN LONG(width) * TextFrames.Unit END StrDispWidth; PROCEDURE DispStr* (fnt: Fonts.Font; s: ARRAY OF CHAR; col, x0, y0: INTEGER); VAR pat: Display.Pattern; i, dx, x, y, w, h: INTEGER; ch: CHAR; BEGIN i := 0; ch := s[i]; WHILE ch # 0X DO Display.GetChar(fnt.raster, ch, dx, x, y, w, h, pat); Display.CopyPattern(col, pat, x0+x, y0+y, Display.replace); INC(i); ch := s[i]; INC(x0, dx) END END DispStr; PROCEDURE StrPrntWidth* (fnt: Fonts.Font; s: ARRAY OF CHAR): LONGINT; VAR width, dx, x, y, w, h: LONGINT; i: INTEGER; fno: SHORTINT; ch: CHAR; BEGIN width := 0; fno := TextPrinter.FontNo(fnt); i := 0; ch := s[i]; WHILE ch # 0X DO TextPrinter.Get(fno, ch, dx, x, y, w, h); INC(width, dx); INC(i); ch := s[i] END; RETURN width END StrPrntWidth; PROCEDURE PrntStr* (fnt: Fonts.Font; s: ARRAY OF CHAR; x0, y0: INTEGER); BEGIN Printer.String(x0, y0, s, fnt.name) END PrntStr; PROCEDURE^ Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); PROCEDURE Alloc*; VAR e: Elem; BEGIN NEW(e); e.handle := Handle; Texts.new := e END Alloc; PROCEDURE Open* (e: Elem; s: ARRAY OF CHAR; uline: BOOLEAN); BEGIN e.W := 5*TextFrames.mm; e.H := e.W; e.handle := Handle; COPY(s, e.s); e.uline := uline END Open; PROCEDURE Copy* (se, de: Elem); BEGIN Texts.CopyElem(se, de); de.s := se.s; de.uline := se.uline END Copy; PROCEDURE Load* (e: Elem; VAR r: Files.Rider); VAR i: INTEGER; ch: CHAR; BEGIN i := 0; REPEAT Files.Read(r, ch); e.s[i] := ch; INC(i) UNTIL ch = 0X; Files.Read(r, ch); IF ch = 0X THEN e.uline := FALSE ELSE e.uline := TRUE END END Load; PROCEDURE Store* (e: Elem; VAR r: Files.Rider); VAR i: INTEGER; ch: CHAR; BEGIN i := 0; REPEAT ch := e.s[i]; Files.Write(r, ch); INC(i) UNTIL ch = 0X; IF e.uline THEN Files.Write(r, 1X) ELSE Files.Write(r, 0X) END END Store; PROCEDURE Changed* (e: Elem; pos: LONGINT); BEGIN Texts.ChangeLooks(Texts.ElemBase(e), pos, pos+1, {}, NIL, 0, 0) END Changed; PROCEDURE PrepDraw* (e: Elem; fnt: Fonts.Font; VAR dy: INTEGER); BEGIN e.W := StrDispWidth(fnt, e.s); e.H := LONG(fnt.height) * TextFrames.Unit; dy := fnt.minY; IF dy > -2 THEN dy := -2 END END PrepDraw; PROCEDURE Draw* (e: Elem; pos: LONGINT; fnt: Fonts.Font; col, x0, y0: INTEGER; VAR ef: Display.Frame); VAR f: Display.Frame; p: TextFrames.Parc; beg: LONGINT; y, w, h: INTEGER; BEGIN w := SHORT(e.W DIV TextFrames.Unit); h := SHORT(e.H DIV TextFrames.Unit); TextFrames.ParcBefore(Texts.ElemBase(e), pos, p, beg); y := y0 + SHORT(p.dsr DIV TextFrames.Unit); DispStr(fnt, e.s, col, x0, y); IF e.uline THEN Display.ReplConst(col, x0, y - 2, w, 1, Display.replace) END; NEW(f); f.X := x0; f.Y := y0; f.W := w; f.H := h; f.handle := HandleFrame; ef := f END Draw; PROCEDURE PrepPrint* (e: Elem; fnt: Fonts.Font; VAR dy: INTEGER); BEGIN e.W := StrPrntWidth(fnt, e.s); e.H := LONG(fnt.height) * TextPrinter.Unit; dy := SHORT(LONG(fnt.minY) * TextFrames.Unit DIV TextPrinter.Unit); IF dy > -2 THEN dy := -2 END END PrepPrint; PROCEDURE Print* (e: Elem; pos: LONGINT; fnt: Fonts.Font; x0, y0: INTEGER); VAR p: TextFrames.Parc; beg: LONGINT; w: INTEGER; BEGIN w := SHORT(e.W DIV TextPrinter.Unit); TextFrames.ParcBefore(Texts.ElemBase(e), pos, p, beg); INC(y0, SHORT(p.dsr DIV TextPrinter.Unit)); PrntStr(fnt, e.s, x0, y0); IF e.uline THEN Printer.ReplConst(x0, y0 - 2, w, 1) END; e.W := StrDispWidth(fnt, e.s) END Print; PROCEDURE Track* (e: Elem; pos: LONGINT; x, y: INTEGER; keys: SET); VAR keysum: SET; BEGIN IF keys = {1} THEN keysum := keys; REPEAT Oberon.DrawCursor(Oberon.Mouse, Oberon.Arrow, x, y); Input.Mouse(keys, x, y); keysum := keysum + keys UNTIL keys = {}; IF keysum = {1} THEN e.uline := ~e.uline; Changed(e, pos) END END END Track; PROCEDURE Handle (e: Texts.Elem; VAR msg: Texts.ElemMsg); VAR copy: Elem; BEGIN WITH e: Elem DO IF msg IS Texts.CopyMsg THEN NEW(copy); Copy(e, copy); msg(Texts.CopyMsg).e := copy ELSIF msg IS Texts.IdentifyMsg THEN WITH msg: Texts.IdentifyMsg DO msg.mod := "FancyElems"; msg.proc := "Alloc" END ELSIF msg IS Texts.FileMsg THEN WITH msg: Texts.FileMsg DO IF msg.id = Texts.load THEN Load(e, msg.r) ELSIF msg.id = Texts.store THEN Store(e, msg.r) END END ELSIF msg IS TextFrames.DisplayMsg THEN WITH msg: TextFrames.DisplayMsg DO IF msg.prepare THEN PrepDraw(e, msg.fnt, msg.Y0) ELSE Draw(e, msg.pos, msg.fnt, msg.col, msg.X0, msg.Y0, msg.elemFrame) END END ELSIF msg IS TextPrinter.PrintMsg THEN WITH msg: TextPrinter.PrintMsg DO IF msg.prepare THEN PrepPrint(e, msg.fnt, msg.Y0) ELSE Print(e, msg.pos, msg.fnt, msg.X0, msg.Y0) END END ELSIF msg IS TextFrames.TrackMsg THEN WITH msg: TextFrames.TrackMsg DO Track(e, msg.pos, msg.X, msg.Y, msg.keys) END END END END Handle; PROCEDURE Insert*; VAR e: Elem; s: Texts.Scanner; M: TextFrames.InsertElemMsg; BEGIN Texts.OpenScanner(s, Oberon.Par.text, Oberon.Par.pos); Texts.Scan(s); IF (s.class = Texts.String) & (s.line = 0) THEN NEW(e); Open(e, s.s, TRUE); M.e := e; Oberon.FocusViewer.handle(Oberon.FocusViewer, M); END END Insert; BEGIN NEW(task); task.safe := FALSE; task.handle := Ticker; next := Oberon.Time() + 100; Oberon.Install(task) END FancyElems.