3. The Client Interface to ViewRec
3.0. Introduction
ViewRec is simple to use: create, in any way you wish, an aggregate of the procedures and variables you want in your user interface, invoke a ViewRec create proc (ViewRef, ViewInterface, or ViewTV) on it, and you have a ready-made user interface. A number of whistles and bells have evolved, but they are just secondary attachements.
For a small, if frivolous, example, see ViewRecExampleClient.Mesa, included in the DF file.
3.1. Typed Variables vs. REFs
ViewRec really uses the "Abstract Machine" (TypedVariables, Types, and so on) inside. It is (or, at least, it is intended to be) not necessary to understand AMTypes (very much) in order to use ViewRec. An effort has been made to provide a convenient set of "sugar" routines that convert from the more familiar world into the world of AMTypes; if you find a piece of sugar coating missing, complain, and it should get fixed.
TypedVariables are like POINTERs and REFs (they "point to" Cedar/Mesa values), and Types are like TYPEs. The difference is that there are ways, at runtime, to explore the structure through TypedVariables and Types, and this is why ViewRec uses them.
3.2. Creating RecordViewers
ViewTV is the procedure that really does it. ViewRef and ViewInterface are sugared versions for dealing with common sources of aggregate TypedVariables: refs to records, and names of definitions modules.
ViewRef passes to ViewTV a TypedVariable for the referent of the first argument (agg: REF ANY). The rest of the arguments are passed on directly.
ViewInterface passes to ViewTV an Interface Instance Record, obtained from the current memory state, for the definitions module whose name is the first argument (name: ROPE). An Interface Instance Record looks like any other record to ViewRec, and it contains as fields the variables and procedures declared in the interface. Again, the rest of the arguments are passed on directly.
ViewTV Creates a RecordViewer for the given TypedVariable. Most of the arguments can be ignored for vanilla usage; their defaults produce ordinary behavior.
agg: TypedVariable
This is what the RecordViewer displays. It must be a TypedVariable for a Cedar/Mesa value of an aggregate type. The aggregate types are: all record types (including structures, which are unpainted records; if you don't know what that means, don't worry), all sequence types, and all array types.
specs: BindingList ←
NIL
This is for special instructions to ViewRec about each component of the aggregate. It will be explained later.
label:
ROPE ←
NIL
The Viewer created may optionally have, as the first thing in its main body, a Labels.Label. This option is taken if and only if the label argument is non-empty. Furthermore, if label ends with a carriage-return, that is removed from the name of the Labels.Label, and the Label is forced to be the only thing on the first row.
otherStuff: OtherStuffProc ←
NIL
This is how the Client may put abritrary Viewers inside the main body of the RecordViewer. It is usually not needed, and will be explained later.
toButt: Closure ← []
This allows the Client to specify an action to be taken when control-return is typed in any textual value viewer in the RecordViewer created. It is usually not needed, and will be explained later.
parent: RecordViewer ←
NIL
This is the "upward" link used for finding a FeedBack area. It need not be related to the Viewers hierarchy, or anything else, for all that ViewRec cares. Vanilla flavored usage just leaves it nil, since vanilla flavored usage also provides a feedback area in the RecordViewer created, thus precluding the need to look further.
asElement: EltHandle ←
NIL
When the aggregate being viewed is an element of another aggregate also being viewed by ViewRec, this argument should give the EltHandle (gotten by GetEltHandle) of that element. This completes some internal data structure linkages that make things like GetEltHandle, and moving to the next and previous value viewers, work.
sample:
BOOLEAN ←
TRUE
This controls whether the sampling process considers the RecordViewer being created to be a root in its forest of things to update. Explained later.
createOptions: CreateOptions ← []
This gives an assortment of parameters that are only meaningful at create time. Most are layout parameters: fonts, spacings, sizes, and so on. All will be discussed later.
viewerInit: ViewerClasses.ViewerRec ← []
This is used to tell Viewers the things it needs to know to create a Viewer. If you are familiar with Viewers coding, it is what is usually called "info" in create procs (ViewerOps.CreateViewer, Containers.Create, etc.). It is passed almost unchanged into Containers.Create to make the outermost Viewer. The fields of most interest are:
name:
ROPE ←
NIL
This tells Viewers what to name the resulting viewer. If you default this, ViewRec will try to fill it in by looking for a field named "name" in agg.
parent: Viewer ←
NIL
This tells Viewers whether to make a new top level Viewer (if NIL), or embed it in another. You must fill this in correctly.
wx, wy:
INTEGER ← 0
If the Viewer created is to be embedded, this is how you specify where it goes in the parent.
ww, wh:
INTEGER ← 0
In most Viewer Create procs, these determine the size of the created Viewer. ViewRec treats them differently. ViewRec ignores wh, and sets the viewer's height after creating it and laying it out. The ww field has its usual significance, and may additionally parameterize layout. See the section on layout.
icon: Icons.IconFlavor ← unInit
This is the only other field ViewRec alters before passing to Containers.Create. If left unInit, ViewRec will choose an IconFlavor of its own.
iconic:
BOOLEAN ←
TRUE
This controls whether the Viewer starts out life iconic or full-blown; it is only meaningful for top level Viewers.
wDir:
ROPE ←
NIL
If non-NIL, this specifies the working directory for the RecordViewer; if NIL, the working directory current at the time ViewTV (or ViewRef, or ViewInterface) is called will be used.
paint:
BOOLEAN ←
TRUE
This determines whether ViewRec will paint the new viewer.
3.3. A Record Viewer by Any Other Name ...
ViewRec thinks of RecordViewers as a sub-class of Viewers. There are procedures to:
treat a RecordViewer like any other Viewer (RVQuaViewer),
determine whether a Viewer is a RecordViewer (ViewerIsRV), and
let the Cedar/Mesa type system know it is (ViewerQuaRV).
ViewRec also thinks of RecordViewers as a sub-class of REF ANY. Analogs of the above three functions are also provided along this relation. There are Cedar/Mesa language features intended to do this, but they don't all work in all cases (because RecordViewerRep is an opaque type). NarrowToRV is provided to fix that deficiency.
3.4. What Kinds of Things Can be Displayed with ViewRec
Given an instance of an aggregate type, ViewRec will decide, for each component of that aggregate, whether or not to display it. That decision can roughly be characterized as: "if the Client has told me what to do, I will obey; otherwise, if the component is simple enough, I will display it". ViewRec knows how to display numbers and enumerations (and subranges thereof), strings, procedures with "simple" arguments, and aggregates of those things.
There are two ways the Client can tell ViewRec what to do. First, it can say: "don't display the component". This is done with the BindingList argument (specs) to ViewTV. If there is a Value Binding for the component, with visible=false, the component will not be displayed. Don't be confused by the other things that Value Bindings can do as well, such as providing initial values. The switches in Value Bindings are set up to default to the commonnest case, which is making data structure linkages that you don't want the user to see.
The other way the Client can tell ViewRec what to do is to say "this is how to display the component". A Handler describes how to display the component and handle its User interactions. There is a later section that explains how to make Handlers and associate them with components.
Here is a precise statement of how ViewRec decides whether or not to display a component:
If there is a Value Binding that says the component is to be invisible, it will be invisible; otherwise:
Handlers for the component are sought. If one is found, the component is displayed according to the Handler. ViewRec starts out with standard Handlers for numbers, enumerations, strings, atoms, arrays, sequences, and "suitable" records; Clients may add more. A suitable record is "simple enough", and would display a non-zero number of components. The test for simplicity can be bypassed to always yield TRUE by setting doAllRecords in the CreateOptions. If no Handler is found:
If the component is a procedure, its argument and return records are examined to see if they're simple enough. The procedure is displayed if and only if they are. Again, the simplicity test can be short-circuited by doAllRecords in CreateOptions.
Otherwise, the component is not displayed.
The criteria for "simple enough" are similar to those for being displayed, except that there are ways something can be simple, but not displayed. An aggregate is simple enough if and only if, for every component:
There is a Value Binding for that component that says to be invisible. Otherwise:
A Handler is found for that component. Otherwise:
The component is a record (or structure) which is simple enough.
3.5. Initial Values
Since ViewTV can only be called on already existing aggregates, this is almost not a problem.
The problem comes in with procedure arguments. ViewRec must cons up an argument record for any procedure it is going to invoke. From the TypedVariable for the procedure it can do this. This is also where it gets initial values for that record.
In addition to that, ViewRec offers an additional complication: Value Bindings. These are effectively assignments performed in ViewTV.
3.6. Non-Procedures at the Top Level
ViewRec may be thought of as providing a generalization of Menus, where the procedures can take arguments. What, then, is the meaning of components that are not procedures?
One thing such components may be used for is to give some information to the user. You could think of them as showing a state variable of the user's model of your application. As such, you may not want the user to be able to directly edit that variable. This can be done with a Value Binding. Setting "editable" to FALSE makes ViewRec disallow user edits of that component.
Another use for such components could be to parameterize your application in some way. This could be accomplished with a state variable and procedure-to-set-it pair. Alternatively, ViewRec allows the Client to specify a procedure to be called whenever the user edits a component. This is done with a Notify Binding.
3.7. Keeping Up-To-Date
ViewRec will periodically review some components and update their displays if they have changed. This is done by a special process whose sole purpose in life this is. Exactly what gets reviewed, and how often the whole loop is performed, are adjustable.
3.7.1. What gets Reviewed
A list of
root RecordViewers is kept. On each iteration of the update loop, each root RecordViewer is reviewed. Reviewing a RecordViewer consists of reviewing all of its visible components. Reviewing a visible component:
Is up to the Handler, if that is how it was made (see "What Kinds of Things can be Displayed with ViewRec").
For the standard Handlers for the scalar types, this consists of comparing the current value with the one which is displayed, and updating the display if they are different.
For the standard Handlers for aggregate types, this is a no-op. This looks wrong, but is not: these components were displayed with a recursive call on ViewTV with sample=TRUE, so they are roots on their own.
If the Component is a record (or structure), it has been displayed with a recursive call on ViewTV. So, it recursively reviews the RecordViewer thus produced.
For procedures, is a no-op.
3.7.2. Tweaking
There are procedures you can call to explicitly review a RecordViewer or component. The review is done in the process you call from (there are interlocks to prevent ViewRec from confusing itself).
SampleRV will review a RecordViewer.
RedisplayElt will review a component. The component must be accessed through the procedure GetEltHandle.
GetEltHandle, given a RecordViewer and a path, will traverse the tree of containment, rooted at the aggregate of the RecordViewer, along the given path, and return the component found.
3.7.3 Frequency of Review
The review loop is controlled by a delay in each iteration. The elapsed time to do one review of each root RecordViewer is measured, and the delay is some coefficient times that time (within limits, also settable). The coefficient is ViewRec.behaviorOptions.delayParms.dMicroseconds * 1000. Feel free to adjust this number to your liking.
3.8. Abortion!
There is a protocol by which a user can request, with varying degrees of urgency, that the executing procedure be aborted (see section 2.2.1.2). The procedure is responsible for occasionally checking the urgency of the user's abortion request, and taking abortive action. There is a procedure, TestAndMaybeResetUserAbort, that, given a threshold, will check whether the urgency has reached or passed that threshold. If it has, that urgency is what's returned, and the urgency is reset to 0. If it hasn't, 0 is returned, and the urgency is unchanged.
3.9. Introducing New Ways of Displaying Things
It is possible for the Client to enrich ViewRec's repertoire. A Handler determines how a given component will be displayed, and how it will interact. A Recognizer is a procedure, which, given a Type, can decide what Handler to use (it can also decide to pass, and let someone else decide). All Handlers are "guarded" by Recognizers. ViewRec has an elaborate mechanism for locating the Recognizers to apply in the right order to get a useful variety of effects. ViewRec starts out with a standard set of Recognizers, and clients may add more.
The search for the Handler for a given component is conducted by applying Recognizers in a particular order, until one of them succeeds. The order is as follows:
First, all of the Recognizers specified in TryRecognizer Bindings are tried. There are no guarantees about the relative ordering among them.
Second, a special list of Recognizers, whose only distinction is that they come before the third group, are tried, in order.
Third, the list of Recognizers associated with the equivalence class of the type of the component is tried, in order. The equivalence class of a type is that set of types that may freely be assigned to one another. For instance, if you code "rope: TYPE = Rope.ROPE", rope and Rope.ROPE are in the same equivalence class. For another instance, INTEGER and [-3 .. 47] are not (although the compiler will insert implicit NARROWs, so your code looks like you were freely assigning).
Fourth is another special list, whose only distinction is that it comes between the third group and the fifth.
The fifth list is like the third. If the type of the component was a subrange, the first step to get the fifth list is to find what it is a subrange of. And if that is also a subrange, then find what that is a subrange of. And so on, untill you've got something that isn't a subrange. With the equivalence class of the non-subrange type is associated (a different association from the one in the third step) the list of Recognizers that gets used here. This is the standard way ViewRec handles subranges of numbers.
Next comes another special list, whose only distinction is to come here.
To get the seventh list, the first step is to strip off subranges, as in the fifth step. Then, the AMTypes.Class of the non-subrange type is taken. A Class is a broad category of types. Some Classes are: record, array, enumerated, and procedure. The seventh list is found associated with the Class.
Finally, there is one more special list, whose only distinction is that it is tried last.
The last seven lists are each added to by one of three procedures:
RegisterRecognizerByType will add a Recognizer to one of: lists 3, 5, or 7. The Reductions argument selects which one.
RegisterRecognizerBeforeReductions will add a Recognizer to one of: lists 2, 4, or 6. Again, the Reductions argument selects which one.
RegisterRecognizerToApplyAfterAll adds a Recognizer to the eighth list.
Each of the last seven lists is maintained in a particular order, and each of the above three procedures offers (via the AddPlace argument) the option of adding to the beginning or the end of the designated list.
It can be easiest to just write one recognizer per Handler. Note that by carefully choosing which list to put it in, the task of actually deciding whether or not the Handler is appropriate can be obviated.
Handlers come in two flavors: Simple, and Complex. Simple Handlers are restricted to do the Name-Value pair style of display and interaction. Complex Handlers are less restricted.
To implement a Simple Handler, you need (almost) only provide procedures to parse and print. Here, exactly, is what you need to provide:
Parse: ParseProc
Parse is given a rope to parse, and a TypedVariable to put the parsed value into. It has to be a TypedVariable, rather than a ref, becuase you can't make a ref to something embedded in an aggregate. A general way to relate a TypedVariable to something more familiar is to: make a thing of TYPE = REF Familiar, then get a TypedVariable for the Familiar via AMBridge.TVForReferent. Now parse into the Familiar, then copy from the TypedVariable for the Familiar into the TypedVariable given to Parse. Simple, no?
UnParse: UnParseProc
This guy's job is the inverse of Parse's: given the TypedVariable, produce the rope.
Max: MaxProc
This procedure is called when doing layout to decide how much area to give to the value text viewer. The height of the Tioga viewer created is the font height times the "lines" answer, plus the vertical padding from createOptions; the width is the "maxWidthNeeded" answer plus the horizontal padding. Someting like VFonts.StringWidth[the biggest string likely] will give a good maxWidthNeeded. The padding is to give Tioga some extra room; it seems to need it. But, since Tioga will make do with just about anything you give it, there's no need to sweat too much over producing these numbers.
Butt: ButtProc
This is for implementing Increment and Decrement. The TypedVariable returned is copied into the component, and the message is given (if it isn't empty, that is).
Complex Handlers are even simpler to describe. You need to provide only two procedures:
producer: ComplexProducer
Given the TypedVariable, and some contextual data, this procedure is free to produce any kind of viewer. However, the viewer produced should follow the conventions in the rest of ViewRec. There are two to pay attention to:
Messages accumulate per user action
Messages to the user generally accumulate. The message place should be cleared out once at the beginning of any sequence of events triggered by a user action (mouse click or keystroke interpreted by ViewRec or cooperating client). The procedure for displaying messages, and clearing the message place, is DisplayMessage.
One thing at a time
Upon reciept of a mouse click or keystroke, before doing any work (but after clearing the message place), check to see if something else was pending. This is done by calling FinishPendingBusiness. Some protocols may decide not to terminate now, and the boolean returned by FinishPendingBusiness will indicate this.
Conversely, you may leave an interaction suspended for later attention with a call to SetPendingBusiness. For example, consider how Simple Handlers use this. When the User wants to edit a field with a Simple Handler (textual representation), the Tioga text viewer for that value is made user-editable, and SetPendingBusiness stashes a finalization procedure. Text editing keystrokes and mouse clicks are not seen by ViewRec (except for the few that are explicity trapped, like RETURN and NEXT). When the user goes to do something else in ViewRec, she is presumed to be done editing, and that something else first calls FinishPendingBusiness. FinishPendingBusiness then calls that stashed procedure, which parses the edited text and makes the update.
updater: Updater
This is the procedure that ViewRec calls when it has detected that the current value is different from the displayed one. Of course, since ViewRec has no control over (or even knowledge of) what is being shown, it can only assume that the ComplexProducer initially draws, and the Updater updates, representations of the values seen at the time these procedures are called. If your Handler has its own way of keeping its display up to date, then it can ignore this mechanism (provide a null Updater).
elementGiver: ElementGiver
If the element being handled by this ComplexHandler is itself an aggregate, this procedure should be supplied. This procedure is for accessing the elements of the aggregate.
3.10. Layout
A RecordViewer lays itself out as follows. First, it identifies a target width. Then, for each viewer it will contain, it creates that viewer, measures its size, and places it so as not to exceed the target width (if possible). Consecutive contained viewers are placed left to right, top to bottom (as words are in English). The height of the main body is constrained by the maxEltsHeight field in the CreateOptions; if the contained viewers stretch over more vertical distance, the main body is made scrollable. Next the containers for procedure arguments and returns are dealt with. For each prepared procedure, the vertical space allotted to its arguments and returns is again limited, by createOptions.maxArgsHeight; the container will be scrollable if necessary. Next, the FeedBack viewer is added on (unless the CreateOptions say its height is to be 0, in which case it is elided). Finally, the height of the whole Viewer is set accordingly (unless it is a top-level Viewer and the height is excessive: greater than about 80% of the screen).
The target width is identified as follows. If the RecordViewer is a top level Viewer, and the viewerInit.ww given is not zero, that is taken as the target width. Otherwise, if either (1: the RecordViewer is a top level Viewer that is not iconic) or (2: the viewerInit.ww given is not zero, and the Record Viewer is embedded), the inner width of the main body of the RecordViewer (cw in its ViewerRec) just after its creation according to viewerInit is used. Otherwise, the createOptions.defaultTargetWidth is used.
Some RecordViewers (according to CreateOptions) will change their layout when the width of their containing viewer changes.
3.11. Some Random Customizations
When creating a RecordViewer, the client may consider the RecordViewer to be for "forms fillout" preperatory to doing some task. For example, such is the case when ViewRec uses itself to prepare the arguments to a procedure. Here are two "reserved for future expansion" slots that can be used to signal completion of the form and readiness to do the task.
3.11.1. otherStuff
The client can put Viewers of its own design, not associated with any components, in the body of a RecordViewer. The OtherStuffProc passed to the create procs does this. It is given the RecordViewer seen as a Viewer, and creates and returns a list of Viewers to embed in it. They are placed after the label, if any, and before the component areas. Again, they should obey the conventions from the rest of ViewRec (see section 3.9).
3.11.2. toButt
Also an argument passed to the create procs. This is something to be done when CONTROL-RETURN is typed in a Value Text Viewer.
3.12. Examples
Some small examples can be found in ViewRecExampleClient.Mesa, and in the implementation of ViewRec.ViewSelf in ViewRecOther.Mesa. Also, try invoking ViewSelf and using the resulting viewer to view the Rope interface. For more information, see the implementor.
3.13. The Working Directory
A RecordViewer has a working directory. It will be current whenever ViewRec calls client procedures (notify procedures or procedures in aggregates).
The working directory may sampled and modified via GetWorkingDirectory and SetWorkingDirectory. It is suggested that the client have some way the user can at least see, and maybe modify, the working directory of a RecordViewer. One possibility is to include the working directory in the name of the viewer.