3. A sample tool using Viewers
This program illustrates how to build tools in the CedarViewers' world. The user interface it creates uses both existing classes of viewers as well as a new class of viewer for special effects. It contains a number of sections, each of which demonstrates a particular set of techniques. The best use of this code would be to copy interesting sections as models for creating your own tool.
It would be a good idea to try the SampleTool before looking at this example. When you type "Run SampleTool" to the UserExec, the SampleTool will appear in iconic form on the lower left side of the Cedar display. Open it by selecting it with the YELLOW (middle) mouse button and then try it out to see how it behaves.
The first part of the SampleTool viewer consists of the standard line of menu buttons at the top of any viewer with a few of the operations deleted and a new one, "MyMenuEntry", added.
The second part holds a "factorial computer", which allows you to compute N! by changing the value of N in the input part of the viewer and then selecting the button labelled "Compute Factorial" with the RED (left) mouse button.
The third part of the viewer presents a horizontal, logarithmic bar graph that dynamically updates itself and shows "Words Allocated Per Second" in the Cedar system.
3.1. SampleTool.mesa Scott McGregor
-- SampleTool.mesa; Written by Scott McGregor on June 9, 1982 9:51 am
-- Last edited by Mitchell on August 16, 1982 4:16 pm
-- Last edited by McGregor on October 4, 1982 11:01 am
DIRECTORY
Buttons USING [Button, ButtonProc, Create],
Containers USING [ChildXBound, Container, Create],
Convert USING [IntFromRope, ValueToRope],
Graphics USING [Context, DrawBox, SetStipple],
Labels USING [Create, Label, Set],
Menus USING [AppendMenuEntry, CreateEntry, CreateMenu, Menu, MenuProc],
MessageWindow USING [Append, Blink],
Process USING [Detach, Pause, SecondsToTicks, Ticks],
Rope USING [Cat, ROPE, Length],
Rules USING [Create, Rule],
SafeStorage USING [NarrowFault, NWordsAllocated],
ShowTime USING [GetMark, Microseconds],
UserExec USING [CommandProc, GetExecHandle, RegisterCommand],
VFonts USING [CharWidth, StringWidth],
ViewerClasses USING [PaintProc, Viewer, ViewerClass, ViewerClassRec],
ViewerOps USING [CreateViewer, PaintViewer, RegisterViewerClass, SetOpenHeight],
ViewerTools USING [MakeNewTextViewer, GetContents, SetSelection];
SampleTool:
CEDAR PROGRAM --
(note 3.0)
IMPORTS Buttons, Containers, Convert, Graphics, Labels, Menus, MessageWindow, Process, Rope, Rules, SafeStorage, ShowTime, UserExec, VFonts, ViewerOps, ViewerTools =
BEGIN
-- The Containers interface is used to create an outer envelope or "container" for the different sections below. For uniformity, we define some standard distances between entries in the tool.
entryHeight: CARDINAL = 15; -- how tall to make each line of items
entryVSpace: CARDINAL = 8; -- vertical leading space between lines
entryHSpace: CARDINAL = 10; -- horizontal space between items in a line
Handle: TYPE = REF SampleToolRec; -- a REF to the data for a particular instance of the sample tool; multiple instances can be created.
SampleToolRec:
TYPE =
RECORD [
-- the data for a particular tool instance
outer: Containers.Container ← NIL, -- handle for the enclosing container
height: CARDINAL ← 0, -- height measured from the top of the container
fact: FactorialViewer, -- the factorial viewer's state
graph: GraphViewer ]; -- the bar graph viewer's state
MakeSampleTool: UserExec.CommandProc =
TRUSTED BEGIN
my: Handle ← NEW[SampleToolRec];
myMenu: Menus.Menu ← Menus.CreateMenu[];
Menus.AppendMenuEntry[
-- add our command to the menu
menu: myMenu,
entry: Menus.CreateEntry[
name: "MyMenuEntry", -- name of the command
proc: MyMenuProc -- proc associated with command
]
];
my.outer ← Containers.Create[[
-- construct the outer container
(note 3.1)
name: "Sample Tool", -- name displayed in the caption
iconic: TRUE, -- so tool will be iconic (small) when first created
column: left, -- initially in the left column
menu: myMenu, -- displaying our menu command
scrollable: FALSE ]]; -- inhibit user from scrolling contents
MakeFactorial[my]; -- build each (sub)viewer in turn
MakeGraph[my];
ViewerOps.SetOpenHeight[my.outer, my.height]; -- hint our desired height
ViewerOps.PaintViewer[my.outer, all]; -- reflect above change
END;
MyMenuProc: Menus.MenuProc = TRUSTED BEGIN
this procedure is called whenever the user left-clicks the entry labelled "MyMenuEntry" in the tool menu.
MessageWindow.Append[
message: "You just invoked the sample menu item with the ",
clearFirst: TRUE];
IF control
THEN MessageWindow.Append[
message: "Control-",
clearFirst: FALSE];
IF shift
THEN MessageWindow.Append[
message: "Shift-",
clearFirst: FALSE];
MessageWindow.Append[
message:
SELECT mouseButton
FROM
red => "Red",
yellow => "Yellow",
ENDCASE => "Blue",
clearFirst: FALSE];
MessageWindow.Append[message: " mouse button.", clearFirst: FALSE];
MessageWindow.Blink[ ];
END;
FactorialViewer:
TYPE =
RECORD [
input: ViewerClasses.Viewer ← NIL, -- the Text Box for user input
result: Labels.Label ← NIL ]; -- result of the computation
MakeFactorial:
PROC [handle: Handle] =
TRUSTED BEGIN --
(note 3.2)
promptButton, computeButton: Buttons.Button;
initialData: Rope.ROPE = "5";
initialResult: Rope.ROPE = "120";
handle.height ← handle.height + entryVSpace; -- space down from the top of the viewer
promptButton ← Buttons.Create[
info: [
name: "Type a number:",
wy: handle.height,
-- default the width so that it will be computed for us --
wh: entryHeight, -- specify rather than defaulting so line is uniform
parent: handle.outer,
border: FALSE ],
proc: Prompt,
clientData: handle]; -- this will be passed to our button proc
handle.fact.input ← ViewerTools.MakeNewTextViewer[ [
parent: handle.outer,
wx: promptButton.wx + promptButton.ww + entryHSpace,
wy: handle.height+2,
ww: 5*VFonts.CharWidth['0], -- five digits worth of width
wh: entryHeight,
data: initialData, -- initial contents
scrollable: FALSE,
border: FALSE]];
computeButton ← Buttons.Create[
info: [
name: "Compute Factorial",
wx: handle.fact.input.wx + handle.fact.input.ww + entryHSpace,
wy: handle.height,
ww:, -- default the width so that it will be computed for us (note 3.3)
wh: entryHeight, -- specify rather than defaulting so line is uniform
parent: handle.outer,
border: TRUE],
clientData: handle, -- this will be passed to our button proc
proc: ComputeFactorial];
handle.fact.result ← Labels.Create[ [
name: initialResult, -- initial contents
wx: computeButton.wx + computeButton.ww + entryHSpace,
wy: handle.height,
ww: 20*VFonts.CharWidth['0], -- 20 digits worth of width
wh: entryHeight,
parent: handle.outer,
border: FALSE]];
handle.height ← handle.height + entryHeight + entryVSpace; -- interline spacing
END;
Prompt: Buttons.ButtonProc = TRUSTED BEGIN
-- force the selection into the user input field
handle: Handle ← NARROW[clientData]; -- get our data
ViewerTools.SetSelection[handle.fact.input]; -- force the selection
END;
ComputeFactorial: Buttons.ButtonProc =
TRUSTED BEGIN
handle: Handle ← NARROW[clientData]; -- get our data
contents: Rope.ROPE ← ViewerTools.GetContents[handle.fact.input];
inputNumber: INT;
resultNumber: REAL ← 1.0;
IF Rope.Length[contents]=0 THEN inputNumber ← 0
ELSE inputNumber ← Convert.IntFromRope[contents
! SafeStorage.NarrowFault => {inputNumber←-1; CONTINUE}]; --(note 3.4)
IF inputNumber
NOT IN [0..34]
THEN
{
MessageWindow.Append[
message: "I can't compute factorial for that input",
clearFirst: TRUE ];
MessageWindow.Blink[ ] }
ELSE
FOR n:
INT
IN [2..inputNumber]
DO
resultNumber ← resultNumber*n;
ENDLOOP;
Labels.Set[handle.fact.result, Convert.ValueToRope[[real[resultNumber]]]];
END;
-- the bar graph reflecting collector activity
GraphViewer: TYPE = RECORD [viewer: ViewerClasses.Viewer ← NIL];
MakeGraph:
PROC [handle: Handle] =
TRUSTED BEGIN --
(note 3.6)
xIncr: INTEGER; -- temporarily used for labelling graph below
xTab: INTEGER = 10;
label: Rope.ROPE ← "1"; -- used to place labels on the graph
rule: Rules.Rule ← Rules.Create[ [
-- create a bar to separate sections 1 and 2
parent: handle.outer,
wy: handle.height,
ww: handle.outer.cw,
wh: 2]];
Containers.ChildXBound[handle.outer, rule]; -- constrain rule to be width of parent
handle.height ← handle.height + entryVSpace; -- spacing after rule
[ ] ← Labels.Create[[
name: "Words Allocated Per Second", parent: handle.outer,
wx: xTab, wy: handle.height, border: FALSE ]];
handle.height ← handle.height + entryHeight + 2; -- interline spacing
handle.graph.viewer ← CreateBarGraph[
parent: handle.outer,
x: xTab, y: handle.height, w: 550, h: entryHeight,
fullScale: 5.0 ]; -- orders of magnitude
handle.height ← handle.height + entryHeight + 2; -- interline spacing
xIncr ← handle.graph.viewer.ww/5; -- so we can space labels at equal fifths
FOR i:
INTEGER IN [0..5)
DO
-- place the labels, 1, 10, 100, 1000, 10000 along the graph
[ ] ← Labels.Create[[name: label, parent: handle.outer,
wx: xTab+i*xIncr - VFonts.StringWidth[label]/2,
wy: handle.height, border: FALSE ]];
label ← label.Cat["0"]; -- concatenate another zero each time
ENDLOOP;
handle.height ← handle.height + entryHeight + entryVSpace; -- extra space at end
TRUSTED {Process.Detach[FORK MeasureProcess[handle]]}; -- start the update process
END;
MeasureProcess: PROC [handle: Handle] = TRUSTED BEGIN --(note 3.7)
-- Forked as a separate process. Updates the bar graph at periodic intervals.
updateInterval: Process.Ticks = Process.SecondsToTicks[1];
mark, nextMark: ShowTime.Microseconds;
words, nextWords, deltaWords, deltaTime: REAL;
mark ← ShowTime.GetMark[ ];
words ← SafeStorage.NWordsAllocated[ ];
UNTIL handle.graph.viewer.destroyed
DO
nextMark ← ShowTime.GetMark[ ];
deltaTime ← (nextMark - mark) * 1.0E-6;
nextWords ← SafeStorage.NWordsAllocated[ ];
deltaWords ← nextWords - words;
SetBarGraphValue[handle.graph.viewer, deltaWords/deltaTime];
words ← nextWords;
mark ← nextMark;
Process.Pause[updateInterval];
ENDLOOP;
END;
-- this section creates the viewer class for a logarithmic bar graph.
-- private data structure for instances of BarChart viewers
GraphData: TYPE = REF GraphDataRec;
GraphDataRec:
TYPE =
RECORD [
value: REAL ← 0, -- current value being displayed (normalized)
scale: REAL ]; -- "full scale"
PaintGraph: ViewerClasses.PaintProc =
TRUSTED BEGIN --
(note 3.8)
myGray: CARDINAL = 122645B; -- every other bit
data: GraphData ← NARROW[self.data];
Graphics.SetStipple[context, myGray];
Graphics.DrawBox[context, [0, 0, data.value, self.ch]];
END;
CreateBarGraph: PROC [x, y, w, h: INTEGER, parent: ViewerClasses.Viewer,
fullScale: REAL]
RETURNS [barGraph: ViewerClasses.Viewer] =
TRUSTED BEGIN --
(note 3.9)
instanceData: GraphData ← NEW[GraphDataRec];
instanceData.scale ← fullScale;
barGraph ← ViewerOps.CreateViewer[
flavor: $BarGraph, -- the class of viewer registered in the start code below
info: [
parent: parent,
wx: x, wy: y, ww: w, wh:h,
data: instanceData,
scrollable: FALSE]
];
END;
-- use this routine to set the bar graph to new values
SetBarGraphValue:
PROC [barGraph: ViewerClasses.Viewer, newValue:
REAL] =
BEGIN
my: GraphData ← NARROW[barGraph.data];
Log10: --Fast-- PROC [x: REAL] RETURNS [lx: REAL] = BEGIN
-- truncated for values of [1..inf), 3-4 good digits
-- algorithm from Abramowitz: Handbook of Math Functions, p. 68
sqrt10: REAL = 3.162278;
t: REAL;
lx ← 0;
WHILE x > 10 DO x ← x/10; lx ← lx+1 ENDLOOP; -- scale to [1..10]
IF x > sqrt10 THEN {x ← x/sqrt10; lx ← lx+0.5}; -- scale to [1..1/sqrt10]
t ← (x-1)/(x+1);
lx ← lx + 0.86304*t + 0.36415*(t*t*t) -- magic cubic approximation
END;
my.value ← Log10[1+newValue] * barGraph.cw / my.scale;
TRUSTED {ViewerOps.PaintViewer[viewer: barGraph, hint: client, clearClient: TRUE]};
END;
-- graphClass is a record containing the procedures and data common to all BarGraph viewer instances (class record).
graphClass: ViewerClasses.ViewerClass ← --
[3.1]
NEW[ViewerClasses.ViewerClassRec ← [paint: PaintGraph]];
-- Register a command with the UserExec that will create an instance of this tool.
UserExec.RegisterCommand[name: "SampleTool", proc: MakeSampleTool,
briefDoc: "Create a sample viewers tool" ];
-- Register the BarGraph class of viewer with the Window Manager
TRUSTED {ViewerOps.RegisterViewerClass[$BarGraph, graphClass]};
[ ] ← MakeSampleTool[UserExec.GetExecHandle[ ]]; -- and create an instance
3.2 Notes for SampleTool
(3.0) [hopefully temporary note] To make this module a valid CEDAR PROGRAM, many sections had to be declared TRUSTED regions because they use other, not yet SAFE, interfaces. Most of these regions are entire procedure bodies, but in some cases it has been possible to narrow the TRUSTED region to a smaller scope.
(3.1) Many of the Viewer procedures take a fair number of arguments in order to provide a number of options. Many of these can be defaulted, as in this section of the program. Using keyword notation makes these calls significantly more clear about which parameters are being specified as well as making them independent of the order specified for the parameters.
(3.2) The factorial sub-Viewer contains a "Text Box" for the user to enter a number, and a button that computes the factorial of the number in the text box when pushed and writes it in a result field. The fields for the user's input and the result are stored in handle.fact.
Note that the name of MakeFactorial's parameter, handle, and its type, Handle, differ only in the case of the "h". In general, having names that differ only in case shifts of letters is a very bad idea. However, this one exception is so compelling and so common that it has become part of the Cedar stylistic conventions: If there is only one variable of a type in a scope, its name can be the same as that of its type except that it should begin with a lowercase letter. Think of it as equivalent to the definite article in English ("the Handle").
(3.3) Buttons.Create will compute the width from the button's name, so we can default this argument to the procedure. Specifying the keyword parameter name w without a value indicates that it should get whatever default value is specified in the definition of Buttons.Create.
(3.4) Convert.IntFromRope lets the error SafeStorage.NarrowFault escape from it (stylistically, it really ought to map that error into one defined in its interface). ComputeFactorial catches this error and assigns -1 to inputNumber to trigger the subsequent test for its being in a suitable range and so give the error message "I can't compute factorial for that input" in the message window.
(3.5) Atoms provide virtual-memory-wide unique identifiers like $BlackOnGrey (all atom constants are denoted by a leading "$" followed by an identifier).
(3.6) The "bar graph" sub-Viewer contains a logarithmic bar graph depicting the number of words per second currently being allocated via the Cedar counted storage allocator. Unlike the factorial sub-Viewer, which only makes use of pre-defined viewer classes, this section defines a new viewer class, $BarGraph [line 3.1], to display information and makes a viewer which is an instance of the class.
(3.7) MeasureProcess runs as an independent process, looping as long as the viewer denoted by handle continues to exist. It computes the average number of words allocated per second in Cedar safe storage and then sleeps for a short time before repeating the cycle.
(3.8) PaintGraph is called whenever the bar graph contents are to be redisplayed on the screen. It will always be called by the window manager, which will pass it a Graphics.Context, correctly scaled, rotated and clipped for the viewer on the screen. It is passed to the Viewers machinery when the $BarGraph class is created [line 3.1].
(3.9) CreateBarGraph is called to create a new BarGraph viewer. It creates the private data for the new instance and then calls a Viewers' system routine to create the actual viewer.