EBImpl.mesa
Copyright Ó 1988, 1989, 1990, 1992 by Xerox Corporation. All rights reserved.
Goodisman, August 15, 1989 5:10:44 pm PDT
Kenneth A. Pier, October 30, 1990 5:15 pm PST
Bier, November 10, 1992 10:48 am PST
Doug Wyatt, April 21, 1992 11:25 am PDT
Contents: The implementation of EmbeddedButtons, A framework for adding standardized behaviors to any application that is willing to support a set of button objects and implement the behavior access routines.
DIRECTORY
Atom, CodeTimer, EBButtonClasses, EBConcreteTypes, EBEditors, EBEditorsExtras, EBEvent, EBLanguage, EBMesaLisp, EBTypes, EmbeddedButtons, IO, KeyMapping, KeyTypes, Process, RefTab, Rope, ScreenCoordsTypes, SimpleFeedback, SpecialKeySyms, TIPPrivate, TIPTypes, UserInput, UserInputDiscrimination, UserInputOps, UserInputOpsExtras2, ViewerClasses, ViewerOps;
EBImpl: CEDAR MONITOR -- for parseInfo --
IMPORTS Atom, CodeTimer, EBLanguage, EBMesaLisp, IO, KeyMapping, RefTab, Rope, SimpleFeedback, TIPPrivate, UserInputDiscrimination, UserInputOps, UserInputOpsExtras2, ViewerOps
EXPORTS EBButtonClasses, EBEditorsExtras, EBEditors, EBTypes, EmbeddedButtons = BEGIN
ActiveButton: TYPE = EBTypes.ActiveButton;
ActiveDoc: TYPE = EBTypes.ActiveDoc;
ButtonClass: TYPE = EBButtonClasses.ButtonClass;
ButtonInfo: TYPE = EBTypes.ButtonInfo;
ButtonInfoRec: TYPE = EBTypes.ButtonInfoRec;
ButtonInfoPrivateRec: PUBLIC TYPE = EBConcreteTypes.ButtonInfoPrivateRec;
EBLanguageProc: TYPE = EBLanguage.EBLanguageProc;
ROPE: TYPE = Rope.ROPE;
Variable: TYPE = REF VariableObj;
VariableObj: TYPE = EBLanguage.VariableObj;
VariableTable: TYPE = EBConcreteTypes.VariableTable;
Viewer: TYPE = ViewerClasses.Viewer;
TargetTableEntry: TYPE = REF TargetTableEntryRep;
TargetTableEntryRep: TYPE = EBConcreteTypes.TargetTableEntryRep;
Error Reporting
Error: PUBLIC PROC [screenMessage: ROPE, fileMessage: ROPE ¬ NIL] = {
The $EmbeddedButtons is set up by ButtonApplicationsImpl's initialization.
IF fileMessage = NIL THEN fileMessage ¬ screenMessage;
SimpleFeedback.Append[$EmbeddedButtons, oneLiner, $UserError, screenMessage];
SimpleFeedback.Append[$EmbeddedButtons, oneLiner, $InternalError, fileMessage];
};
Used By Editors
ActiveDocObj: TYPE = EBTypes.ActiveDocObj;
ActiveDocClass: TYPE = EBEditors.ActiveDocClass;
ActiveDocPrivateObj: PUBLIC TYPE = EBConcreteTypes.ActiveDocPrivateObj;
Event: TYPE ~ EBEvent.Event;
EventRep: PUBLIC TYPE ~ EBEvent.EventRep; -- for EBTypes
Handle: TYPE = UserInput.Handle;
HandleRep: PUBLIC TYPE ~ UserInputPrivate.Rep; -- used only by MouseCoords routine below
CreateActiveDoc: PUBLIC PROC [theDoc: REF, docClass: ActiveDocClass] RETURNS [theActiveDoc: ActiveDoc] = {
nameTable: RefTab.Ref ¬ RefTab.Create[];
targetTable: RefTab.Ref ¬ RefTab.Create[];
theActiveDoc ¬ NEW[ActiveDocObj ¬ [
theDoc: theDoc,
private: NEW[ActiveDocPrivateObj ¬ [
class: docClass,
nameTable: nameTable,
targetTable: targetTable]]]];
};
GetHandle: PUBLIC PROC [event: Event] RETURNS [UserInput.Handle] = {
RETURN[event.handle];
};
GetEvent: PUBLIC PROC [input: LIST OF REF] RETURNS [Event ¬ NIL] ~ {
IF input#NIL AND UserInputDiscrimination.IsHandle[input.first] AND input.rest#NIL THEN {
WITH input.rest.first SELECT FROM
action: EBEvent.Action => IF input.rest.rest=NIL THEN {
handle: Handle ← UserInputDiscrimination.NarrowHandle[input.first];
RETURN[NEW[EventRep ¬ [handle, action]]];
};
ENDCASE;
};
};
ValidEvent: PUBLIC PROC [event: Event] RETURNS [BOOL] ~ {
RETURN[event#NIL];
};
Button1: KeyTypes.KeySym ~ SpecialKeySyms.Button1;
Button2: KeyTypes.KeySym ~ SpecialKeySyms.Button2;
Button3: KeyTypes.KeySym ~ SpecialKeySyms.Button3;
MouseMotion: PUBLIC PROC [event: Event] RETURNS [BOOL ¬ FALSE] = {
SELECT event.action.kind FROM
$IntegerPosition, $Position, $FakePosition => RETURN[TRUE];
ENDCASE;
};
MouseAction: PUBLIC PROC [event: Event] RETURNS [BOOL ¬ FALSE] ~ {
SELECT event.action.kind FROM
$Key, $KeyStillDown => {
keySyms: KeyMapping.KeySyms ~
KeyMapping.GetKeySyms[UserInputOps.GetMapping[event.handle], event.action.keyCode];
FOR i: BYTE IN[0..keySyms.n) DO
SELECT keySyms[i] FROM
Button1, Button2, Button3 => RETURN[TRUE];
ENDCASE;
ENDLOOP;
};
$IntegerPosition, $Position, $FakePosition => RETURN[TRUE];
ENDCASE;
};
MouseAllUp: PUBLIC PROC [event: Event] RETURNS [BOOL] ~ {
RETURN[
UserInputOps.GetKeySymState[event.handle, Button1]=up AND
UserInputOps.GetKeySymState[event.handle, Button2]=up AND
UserInputOps.GetKeySymState[event.handle, Button3]=up
];
};
MouseCoords: PUBLIC PROC [event: Event] RETURNS [EBEditors.MousePosition] ~ {
RETURN[event.handle.mousePosition];
RETURN[UserInputOpsExtras2.GetPosition[event.handle]];
};
HandleEvent: PUBLIC PROC [event: Event, button: ActiveButton, doc: ActiveDoc] ~ {
buttonInfo: ButtonInfo ~ GetButtonInfo[button, doc];
IF buttonInfo#NIL THEN {
class: ButtonClass ~ buttonInfo.private.class;
IF class#NIL AND class.handleEvent#NIL THEN -- Pass event to button class
class.handleEvent[event, buttonInfo.private.instanceData, buttonInfo];
};
};
parseInfo: TIPPrivate.TIPParseInfo ~ TIPPrivate.CreateParseInfo[name: "EBImpl.ParseEvent"];
MatchEvent: ENTRY PROC [table: TIPTypes.TIPTable, event: Event] RETURNS [LIST OF REF] ~ {
ENABLE UNWIND => NULL;
parseInfo.inCreek ¬ event.handle;
parseInfo.tableHead ¬ table;
RETURN[TIPPrivate.WideMatchEvent[parseInfo, event.action­]];
};
stdCoords: ScreenCoordsTypes.TIPScreenCoords ~ TIPPrivate.stdCoords;
ParseEvent: PUBLIC PROC [table: TIPTypes.TIPTable, event: Event] RETURNS [LIST OF REF] ~ {
result: LIST OF REF ~ MatchEvent[table, event];
FOR list: LIST OF REF ¬ result, list.rest UNTIL list=NIL DO
IF list.first=stdCoords THEN { [] ¬ ViewerOps.MouseInViewer[stdCoords]; EXIT };
ENDLOOP;
RETURN[result];
};
ButtonDataFromRope: PUBLIC PROC [rope: ROPE, instantiateNow: BOOL ¬ FALSE, button: ActiveButton ¬ NIL, doc: ActiveDoc ¬ NIL] RETURNS [val: REF] = {
CodeTimer.StartInt[$ButtonDataFromRope, $EmbeddedButtons];
val ¬ NEW[ButtonInfoRec ¬ [
button: button,
doc: doc,
private: NEW [ButtonInfoPrivateRec ¬ [
rope: rope,
changed: FALSE,
class: NIL,
nameChecked: FALSE,
name: $Unchecked,
fieldValuesOK: FALSE,
language: NIL,
languageVersion: 0,
instanceData: NIL,
hasComplained: FALSE]]]];
CodeTimer.StopInt[$ButtonDataFromRope, $EmbeddedButtons];
};
RopeFromButtonData: PUBLIC PROC [val: REF] RETURNS [rope: Rope.ROPE ¬ ""] = {
CodeTimer.StartInt[$RopeFromButtonData, $EmbeddedButtons];
WITH val SELECT FROM
buttonInfo: ButtonInfo => {
IF buttonInfo.private.class = NIL OR ~buttonInfo.private.changed OR buttonInfo.private.class.unparseInstanceData = NIL THEN rope ¬ buttonInfo.private.rope
ELSE {
SELECT buttonInfo.private.language FROM
$Poppy => {
Have button class unparse fields into the symbol table.
[] ¬ buttonInfo.private.class.unparseInstanceData[buttonInfo.private.instanceData, buttonInfo, $Poppy, 1];
Now unparse the symbol table.
rope ¬ Rope.Concat["Poppy1 ", EBLanguage.PoppyUnparse[buttonInfo.private.symbols, buttonInfo.private.order]];
};
$ButtonLisp => rope ¬ Rope.Cat[Atom.GetPName[buttonInfo.private.class.name], " ", buttonInfo.private.class.unparseInstanceData[buttonInfo.private.instanceData, buttonInfo, $ButtonLisp, 1]];
ENDCASE;
};
};
ENDCASE => Error["EmbeddedButtons: Internal error. ButtonData is wrong type (RopeFromButtonData)"];
CodeTimer.StopInt[$RopeFromButtonData, $EmbeddedButtons];
};
ParseFeedbackField: EBLanguage.FieldParseProc = {
PROC [stream: IO.STREAM] RETURNS [val: REF]
For registration with EBLanguage.RegisterFieldParseProc.
rope: ROPE ¬ stream.GetRope[];
val ¬ ButtonFeedbackFromRope[rope];
};
UnparseFeedbackField: EBLanguage.FieldUnparseProc = {
PROC[val: REF] RETURNS [rope: ROPE];
For registration with EBLanguage.RegisterFieldUnparseProc.
WITH val SELECT FROM
ft: FeedbackTable => RETURN[ft.rope];
ENDCASE => RETURN[""];
};
ButtonFeedbackFromRope: PUBLIC PROC [rope: ROPE, instantiateNow: BOOL ¬ FALSE] RETURNS[val: REF] = {
"table" will be computed when the button is used.
val ¬ NEW[FeedbackTableRec ¬ [
rope: rope,
table: NIL]];
};
ButtonFeedbackToRope: PUBLIC PROC [val: REF] RETURNS [rope: Rope.ROPENIL] = {
WITH val SELECT FROM
ft: FeedbackTable => RETURN[ft.rope];
ENDCASE => Error["EmbeddedButtons: Internal error. ButtonFeedback is wrong type (ButtonFeedbackToRope)"];
};
Used By Button Classes
RegisterButtonClass: PUBLIC PROC [buttonClassName: ATOM, buttonClass: ButtonClass] = {
[] ¬ RefTab.Store[classTable, buttonClassName, buttonClass];
};
RegisterNameValuePair: PUBLIC PROC [name: ATOM, value: REF, buttonInfo: ButtonInfo] = {
[] ¬ RefTab.Store[buttonInfo.doc.private.nameTable, name, value];
Should change to include button for traceback.
};
CheckListOfATOM: PROC [list: LIST OF REF] RETURNS [ok: BOOL ¬ TRUE] = {
FOR l: LIST OF REF ¬ list, l.rest UNTIL l = NIL DO
IF NOT ISTYPE[l.first, ATOM] THEN {
Error["EmbeddedButtons: Message handlers (targets) must be atoms"];
RETURN[FALSE];
};
ENDLOOP;
};
UnknownLanguage: PROC [] = {
Error["PassEventToApplication: Unknown button language"];
};
GetApplications: PUBLIC PROC [buttonInfo: ButtonInfo] RETURNS [applications: LIST OF ATOM] = {
SELECT buttonInfo.private.language FROM
$Poppy => {
targetsRope: ROPE ~ EBLanguage.GetFieldRope[buttonInfo.private.symbols, $MessageHandler];
val: REF ~ EBMesaLisp.Parse[IO.RIS[targetsRope]].val;
tail: LIST OF ATOM;
IF ISTYPE[val, LIST OF REF] THEN {
FOR list: LIST OF REF ¬ NARROW[val], list.rest UNTIL list = NIL DO
IF NOT ISTYPE[list.first, ATOM] THEN RETURN[NIL]; -- one bad apple...
[applications, tail] ¬ AddAtom[NARROW[list.first], applications, tail];
ENDLOOP;
}
ELSE {
IF ISTYPE[val, ATOM] THEN applications ¬ LIST[NARROW[val, ATOM]]
ELSE applications ¬ NIL;
};
};
ENDCASE => {UnknownLanguage[]; RETURN[NIL]};
};
AddAtom: PUBLIC PROC [entity: ATOM, entityList, ptr: LIST OF ATOM] RETURNS [newList, newPtr: LIST OF ATOM] = {
IF ptr = NIL THEN {
IF NOT entityList = NIL THEN ERROR;
newPtr ¬ newList ¬ CONS[entity, NIL];
RETURN;
}
ELSE {
newList ¬ entityList;
ptr.rest ¬ CONS[entity, NIL];
newPtr ¬ ptr.rest;
};
};
DefaultBehavior: PROC [class: ButtonClass, instanceData: REF, buttonInfo: ButtonInfo] = INLINE {
Usually, when a button is not connected to any application, it will exhibit some default behavior when clicked so that the user knows it is working. Radio Buttons, for instance, change their state when clicked, as a default behavior.
IF class # NIL AND class.defaultBehavior # NIL
THEN class.defaultBehavior[instanceData, buttonInfo];
};
PassEventToApplication: PUBLIC PROC [event: LIST OF REF, buttonInfo: ButtonInfo, application: ATOM ¬ $MessageHandlers] = {
targets: LIST OF ATOM;
IF application = $MessageHandlers THEN targets ¬ GetApplications[buttonInfo]
ELSE targets ¬ LIST[application];
Process.Detach[FORK ForkedEvent[event, buttonInfo, targets]]; -- unfortunately this causes synchronization problems with Tioga (sigh), Bier February 19, 1990
ForkedEvent[event, buttonInfo, targets];
};
ForkedEvent: PROC [event: LIST OF REF, buttonInfo: ButtonInfo, targets: LIST OF ATOM] = {
anyTargetsFound: BOOL ¬ FALSE;
target: ATOM;
found: BOOL;
class: ButtonClass;
instanceData: REF;
val, results: REF;
class ¬ buttonInfo.private.class;
instanceData ¬ buttonInfo.private.instanceData;
IF event = NIL OR targets = NIL
THEN {DefaultBehavior[class, instanceData, buttonInfo]; RETURN};
FOR l: LIST OF ATOM ¬ targets, l.rest UNTIL l = NIL DO -- for each application...
target ¬ l.first;
IF target = $Default THEN {
DefaultBehavior[class, instanceData, buttonInfo];
anyTargetsFound ¬ TRUE;
LOOP;
};
[found, val] ¬ RefTab.Fetch[buttonInfo.doc.private.targetTable, target];
IF found THEN { -- this document is linked to a particular application window
anyTargetsFound ¬ TRUE;
WITH val SELECT FROM
entry: TargetTableEntry => {
IF entry.notifyProc # NIL THEN {
results ¬ entry.notifyProc­[buttonInfo, event, entry.viewer, entry.applicationData];
ResultsFeedback[results, buttonInfo];
}
ELSE IF entry.viewer # NIL THEN {
ViewerOps.NotifyViewer[entry.viewer, event];
};
};
ENDCASE => {
Error["EmbeddedButtons: Internal error. Target table entry is wrong type (PassEventToApplication)"];
RETURN;
};
}
ELSE { -- Let class do default handling and then try globally-registered applications
IF class # NIL AND class.defaultBehavior # NIL
THEN class.defaultBehavior[instanceData, buttonInfo];
[found, val] ¬ RefTab.Fetch[registeredNotifyTable, target];
IF found THEN {
anyTargetsFound ¬ TRUE;
WITH val SELECT FROM
proc: REF RegisteredNotifyProc => {
IF proc # NIL
THEN results ¬ proc­[event, buttonInfo]
ELSE results ¬ $Error;
ResultsFeedback[results, buttonInfo];
};
ENDCASE => {
Error["EmbeddedButtons: Internal error. Registered notify proc is wrong type (PassEventToApplication)"];
RETURN;
};
}
ELSE Error[Rope.Concat["This message handler (target) not found: ", Atom.GetPName[target]]];
};
ENDLOOP;
};
okEvent: LIST OF REF = LIST[$Done, $OK];
errorEvent: LIST OF REF = LIST[$Done, $Error];
warningEvent: LIST OF REF = LIST[$Done, $Warning];
ResultsFeedback: PROC [results: REF, buttonInfo: ButtonInfo] ~ {
feedbackEvent: LIST OF REF;
WITH results SELECT FROM
list: LIST OF REF => feedbackEvent ¬ list;
ENDCASE => {
IF results = NIL THEN feedbackEvent ¬ okEvent
ELSE IF results = $Error THEN feedbackEvent ¬ errorEvent
ELSE IF results = $Warning THEN feedbackEvent ¬ warningEvent
ELSE feedbackEvent ¬ LIST[results];
};
FeedbackNotify[feedbackEvent, buttonInfo];
};
FeedbackTable: TYPE = REF FeedbackTableRec;
FeedbackTableRec: TYPE = RECORD [
rope: ROPE,
table: REF FeedbackTableObj
];
FeedbackTableObj: TYPE = LIST OF FeedbackTableEntry;
FeedbackTableEntry: TYPE = REF FeedbackTableEntryRec;
FeedbackTableEntryRec: TYPE = RECORD [
event: LIST OF REF,
action: LIST OF REF
];
MatchFeedbackTableEntry: PROC [feedbackEvent: LIST OF REF, table: FeedbackTable] RETURNS [bestAction: LIST OF REF ¬ NIL] = {
bestActionMatches: INT ¬ 0;
Search the table for the most specific match to this event.
FOR l: LIST OF FeedbackTableEntry ¬ table.table­, l.rest UNTIL l = NIL DO
m: INT;
m ¬ IF Subset[l.first.event, feedbackEvent] THEN Matches[feedbackEvent, l.first.event] ELSE 0;
IF m > bestActionMatches THEN {
bestAction ¬ l.first.action;
bestActionMatches ¬ m;
};
ENDLOOP;
};
Subset: PROC [a, b: LIST OF REF] RETURNS [BOOL] = {
Included: PROC [a: REF, b: LIST OF REF] RETURNS [BOOL] = {
FOR list: LIST OF REF ¬ b, list.rest UNTIL list = NIL DO
IF Match[a, list.first] THEN RETURN[TRUE];
ENDLOOP;
RETURN[FALSE];
};
Returns TRUE iff all REFs of a (or their equivalents, see ExactMatch) are also in b.
FOR alist: LIST OF REF ¬ a, alist.rest UNTIL alist = NIL DO
IF NOT Included[alist.first, b] THEN RETURN[FALSE];
ENDLOOP;
RETURN[TRUE];
};
Match: PROC [a, b: REF] RETURNS [BOOL] = {
IF a = b THEN RETURN[TRUE]
ELSE IF EBLanguage.Equal[a, b] THEN RETURN[TRUE]
ELSE IF a = $Red AND b = $Left THEN RETURN[TRUE]
ELSE IF a = $Yellow AND b = $Middle THEN RETURN[TRUE]
ELSE IF a = $Blue AND b = $Right THEN RETURN[TRUE]
ELSE IF a = $Left AND b = $Red THEN RETURN[TRUE]
ELSE IF a = $Middle AND b = $Yellow THEN RETURN[TRUE]
ELSE IF a = $Right AND b = $Blue THEN RETURN[TRUE]
ELSE RETURN[FALSE];
};
Matches: PROC [a, b: LIST OF REF] RETURNS [result: INT ¬ 0] = {
Returns the number of elements of the first list that also occur in the second list with the following provisos: $Red matches $Left, $Yellow matches $Middle, $Blue matches $Right.
FOR c: LIST OF REF ¬ a, c.rest UNTIL c = NIL DO
FOR d: LIST OF REF ¬ b, d.rest UNTIL d = NIL DO
IF Match[c.first, d.first] THEN result ¬ result + 1;
ENDLOOP;
ENDLOOP;
};
CheckFeedbackTable: PROC [buttonInfo: ButtonInfo] RETURNS [table: FeedbackTable ¬ NIL] = {
Get the feedback table
buttonFeedback: REF;
SELECT buttonInfo.private.language FROM
$Poppy => {
buttonFeedback ¬ EBLanguage.GetFieldRef[buttonInfo.private.symbols, $Feedback];
};
ENDCASE => RETURN;
IF buttonFeedback = NIL THEN RETURN[NIL]; -- this button doesn't specify any feedback
WITH buttonFeedback SELECT FROM
t: FeedbackTable => {
table ¬ t;
IF table.table = NIL THEN {
table.table ¬ ParseFeedbackTable[table.rope];
IF table.table = NIL THEN RETURN[NIL];
};
};
ENDCASE => {
Error["EmbeddedButtons: Internal error. Button feedback table is wrong type (FeedbackNotify)"];
RETURN[NIL];
};
};
refTRUE: REF BOOL ¬ NEW[BOOL ¬ TRUE];
FeedbackNotify: PUBLIC PROC [feedbackEvent: LIST OF REF, buttonInfo: ButtonInfo] = {
feedbackEvent may come from several places including
(1) Translating raw actions through ButtonClassesCommon.tip which produces the atoms Up, Down, Red, Yellow, Blue, MouseMoved and produces mouse coordinates.
(2) The SetValueProc of a given ButtonClass. For example, Multi-state buttons send ($Value, <ref>). Radio buttons send ($State, <REF BOOL>) or ($StateStill, <REF BOOL>).
table: FeedbackTable;
bestAction: LIST OF REF;
CodeTimer.StartInt[$FeedbackNotify, $EmbeddedButtons];
table ¬ CheckFeedbackTable[buttonInfo];
IF table = NIL THEN RETURN;
bestAction ¬ MatchFeedbackTableEntry[feedbackEvent, table];
IF bestAction = NIL OR (bestAction.first = NIL AND bestAction.rest = NIL) THEN {
CodeTimer.StopInt[$FeedbackNotify, $EmbeddedButtons];
RETURN;
};
ActionToEditor[bestAction, buttonInfo, feedbackEvent];
CodeTimer.StopInt[$FeedbackNotify, $EmbeddedButtons];
};
Context: TYPE = EBTypes.Context;
feedbackContext: Context ¬ EBLanguage.CreateContext[];
InButton: PUBLIC PROC [x, y: INTEGER, buttonInfo: ButtonInfo] RETURNS[BOOL] = {
IF buttonInfo.doc.private.class.inButton # NIL THEN
RETURN[buttonInfo.doc.private.class.inButton[buttonInfo.button, buttonInfo.doc, x, y]];
RETURN[TRUE];
};
MapButtons: PUBLIC PROC [doc: ActiveDoc, proc: EachButtonProc] RETURNS [aborted: BOOL ¬ FALSE] = {
IF doc.private.class.mapRef # NIL THEN aborted ¬ doc.private.class.mapRef[doc, proc];
};
EachButtonProc: TYPE = EmbeddedButtons.EachButtonProc;
Used By Feedback Procedures
RegisterPoppyProc: PUBLIC PROC [procedureName: ATOM, proc: EBLanguageProc, interpreted: BOOL ¬ TRUE] = {
EBLanguage.RegisterProc[procedureName, proc, interpreted];
};
FeedbackToEditor: PUBLIC PROC [feedback: REF, buttonInfo: ButtonInfo] RETURNS [REF] = {
RETURN[buttonInfo.doc.private.class.feedback[buttonInfo.button, buttonInfo.doc, feedback]];
};
ActionToEditor: PUBLIC PROC [action: LIST OF REF, buttonInfo: ButtonInfo, feedbackEvent: LIST OF REF] = {
receivers: LIST OF REF ¬ LIST[buttonInfo.doc.private.class.name];
CodeTimer.StartInt[$ActionToEditor, $EmbeddedButtons];
EBLanguage.SetSystemValue[feedbackContext, $MessageReceiver, receivers];
EBLanguage.SetSystemValue[feedbackContext, $Feedback, refTRUE];
IF ISTYPE[action.first, LIST OF REF] THEN {
FOR l: LIST OF REF ¬ action, l.rest UNTIL l = NIL DO
WITH l.first SELECT FROM
lora: LIST OF REF => {
message: REF ¬ EBLanguage.Evaluate[lora, buttonInfo, feedbackEvent, feedbackContext];
IF message # NIL THEN [] ¬ FeedbackToEditor[message, buttonInfo];
};
ENDCASE => Error["EmbeddedButtons: Feedback table actions must be lists!"];
ENDLOOP;
}
ELSE {
Otherwise, we have a list like (1 ApplyLook <TextFromValue>) or like <SetCursor bullseye>. Evaluate as needed and pass to the editor once.
message: REF ¬ EBLanguage.Evaluate[action, buttonInfo, feedbackEvent, feedbackContext];
IF message # NIL THEN [] ¬ FeedbackToEditor[message, buttonInfo];
};
CodeTimer.StopInt[$ActionToEditor, $EmbeddedButtons];
};
SetButtonValue: PUBLIC PROC [buttonInfo: ButtonInfo, variable: ATOM ¬ $Value, value: REF] = {
IF variable = $Value THEN {
class: ButtonClass ¬ buttonInfo.private.class;
instanceData: REF ¬ buttonInfo.private.instanceData;
IF class # NIL AND class.setValue # NIL THEN {
changed: BOOL ¬ class.setValue[instanceData, value, buttonInfo];
IF changed THEN MarkButtonAsChanged[buttonInfo];
};
}
ELSE {
variableTable: VariableTable ¬ NARROW[GetFieldRef[$Variables, buttonInfo]];
IF variableTable = NIL THEN RETURN
ELSE {
realVar: Variable ¬ EBLanguage.GetVariable[variableTable, variable];
IF realVar = NIL THEN {
Error[IO.PutFR["Button %g has non-existent or incorrect definition for variable %g. Can't SetButtonValue", [atom[buttonInfo.private.name]], [atom[variable]] ]];
}
ELSE {
realVar.value ¬ value;
EBLanguage.SetVariable[variableTable, variable, realVar];
};
};
};
};
Used By Applications
NotifyProc: TYPE = EmbeddedButtons.NotifyProc;
RegisteredNotifyProc: TYPE = EmbeddedButtons.RegisteredNotifyProc;
LinkDocToApplication: PUBLIC PROC [doc: ActiveDoc, target: ATOM, targetViewer: Viewer ¬ NIL, applicationData: REF ¬ NIL, notifyProc: NotifyProc ¬ NIL] = {
refNotifyProc: REF NotifyProc ¬ IF notifyProc = NIL THEN NIL ELSE NEW[NotifyProc ¬ notifyProc];
tte: TargetTableEntry ¬ NEW[TargetTableEntryRep ¬ [
viewer: targetViewer,
applicationData: applicationData,
notifyProc: refNotifyProc]];
[] ¬ RefTab.Store[doc.private.targetTable, target, tte];
};
RegisterApplication: PUBLIC PROC [target: ATOM, notifyProc: RegisteredNotifyProc] = {
refRegisteredNotifyProc: REF RegisteredNotifyProc ¬ IF notifyProc = NIL THEN NIL ELSE NEW[RegisteredNotifyProc ¬ notifyProc];
[] ¬ RefTab.Store[registeredNotifyTable, target, refRegisteredNotifyProc];
};
GetValue: PUBLIC PROC [name: ATOM, doc: ActiveDoc, variable: ATOM ¬ $Value] RETURNS [val: REF ANY] = {
Tries to find the value in the table of recently queried values. Otherwise, searches the document. In the current implementation, only value $Value is cached. Other values require searching the document.
MapGetKeyValue: EmbeddedButtons.EachButtonProc = {
PROC [button: ActiveButton, doc: ActiveDoc] RETURNS [done: BOOLFALSE]
class: ButtonClass;
instanceData: REF;
thisName: ATOM;
exists: BOOL ¬ FALSE;
[thisName, exists] ¬ GetName[button, doc];
IF exists AND thisName = name THEN {
buttonInfo: ButtonInfo ¬ GetButtonInfo[button, doc]; -- does a full parse, if needed
val ¬ GetButtonValue[buttonInfo, variable]; -- stores value in nameTable, if appropriate
};
};
found: BOOL ¬ FALSE;
IF variable = $Value THEN {
[found, val] ¬ RefTab.Fetch[doc.private.nameTable, name];
};
IF val = NIL THEN [] ¬ doc.private.class.mapRef[doc, MapGetKeyValue];
};
GetButtonName: PUBLIC PROC [buttonInfo: ButtonInfo] RETURNS [name: ATOM] = {
exists: BOOL ¬ FALSE;
[name, exists] ¬ GetNameInternal[buttonInfo];
IF NOT exists THEN name ¬ NIL;
};
GetNameInternal: PROC [buttonInfo: ButtonInfo] RETURNS [name: ATOM, exists: BOOL ¬ FALSE] = {
IF buttonInfo # NIL THEN {
IF buttonInfo.private.nameChecked THEN {
name ¬ buttonInfo.private.name;
exists ¬ TRUE;
}
ELSE {
success: BOOL ¬ ConfirmFields[buttonInfo];
IF success THEN {
nameRope: ROPE ¬ GetFieldRope[$Name, buttonInfo];
nameVal: REF ¬ EBMesaLisp.Parse[IO.RIS[nameRope]].val;
IF nameVal # NIL THEN {
IF ISTYPE[nameVal, ATOM] THEN {
name ¬ NARROW[nameVal];
exists ¬ TRUE;
buttonInfo.private.nameChecked ¬ TRUE;
buttonInfo.private.name ¬ name;
}
ELSE Error["GetName: Invalid Name field!"];
};
};
};
};
};
GetButtonValue: PUBLIC PROC [buttonInfo: ButtonInfo, variable: ATOM ¬ $Value] RETURNS [value: REF ¬ NIL] = {
IF variable = $Value THEN {
class: ButtonClass ¬ buttonInfo.private.class;
instanceData: REF ¬ buttonInfo.private.instanceData;
IF class # NIL AND class.getValue # NIL THEN
value ¬ class.getValue[instanceData, buttonInfo];
}
ELSE {
variableTable: VariableTable ¬ NARROW[GetFieldRef[$Variables, buttonInfo]];
IF variableTable = NIL THEN RETURN[NIL]
ELSE {
realVar: Variable ¬ EBLanguage.GetVariable[variableTable, variable];
IF realVar = NIL THEN {
value ¬ NIL;
Error[IO.PutFR["Button %g has non-existent or incorrect definition for variable %g. Can't GetButtonValue", [atom[buttonInfo.private.name]], [atom[variable]] ]];
}
ELSE value ¬ realVar.value;
};
};
};
SetValue: PUBLIC PROC [name: ATOM, val: REF, doc: ActiveDoc, variable: ATOM ¬ $Value] = {
MapSetKeyValue: EBEditors.EachButtonProc = {
thisName: ATOM;
exists: BOOL ¬ TRUE;
[thisName, exists] ¬ GetName[button, doc];
IF exists AND thisName = name THEN {
buttonInfo: ButtonInfo ¬ GetButtonInfo[button, doc]; -- fully instantiates if necessary
IF buttonInfo = NIL THEN RETURN;
SetButtonValue[buttonInfo, variable, val];
};
};
[] ¬ doc.private.class.mapRef[doc, MapSetKeyValue];
};
Convenience Routines
GetRawButtonInfo: PROC [button: ActiveButton, doc: ActiveDoc] RETURNS [buttonInfo: ButtonInfo ¬ NIL] = {
data: REF ¬ doc.private.class.getRef[$ButtonData, button, doc];
IF data = NIL THEN {
Error[IO.PutFR1["Some button in doc %g has a NIL $ButtonData property (Instantiated?)", [rope[doc.private.class.getDocName[doc]]] ]];
buttonInfo ¬ NIL;
}
ELSE {
WITH data SELECT FROM
b: ButtonInfo => buttonInfo ¬ b;
r: ROPE => {
buttonInfo ¬ NARROW[ButtonDataFromRope[r]];
doc.private.class.setRef[$ButtonData, button, doc, buttonInfo, FALSE];
};
ENDCASE => {
Error["Don't know what this ButtonData is!", "A button was pressed whose ButtonData was an unknown type."];
buttonInfo ¬ NIL;
};
};
};
GetButtonInfo: PROC [button: ActiveButton, doc: ActiveDoc] RETURNS [buttonInfo: ButtonInfo] = {
CodeTimer.StartInt[$GetButtonInfo, $EmbeddedButtons];
buttonInfo ¬ GetRawButtonInfo[button, doc];
IF buttonInfo # NIL THEN {
success: BOOL ¬ FALSE;
buttonInfo.button ¬ button;
buttonInfo.doc ¬ doc;
success ¬ ConfirmButtonInfo[buttonInfo];
IF NOT success THEN buttonInfo ¬ NIL;
};
CodeTimer.StopInt[$GetButtonInfo, $EmbeddedButtons];
};
GetName: PROC [button: ActiveButton, doc: ActiveDoc] RETURNS [name: ATOM, exists: BOOL ¬ FALSE] = {
buttonInfo: ButtonInfo ¬ GetRawButtonInfo[button, doc]; -- no parsing, but may already be parsed
IF buttonInfo # NIL THEN {
IF buttonInfo.private.nameChecked THEN {
name ¬ buttonInfo.private.name;
exists ¬ TRUE;
}
ELSE {
success: BOOL ¬ ConfirmFields[buttonInfo];
IF success THEN {
nameRope: ROPE ¬ GetFieldRope[$Name, buttonInfo];
nameVal: REF ¬ EBMesaLisp.Parse[IO.RIS[nameRope]].val;
IF nameVal # NIL THEN {
IF ISTYPE[nameVal, ATOM] THEN {
name ¬ NARROW[nameVal];
exists ¬ TRUE;
buttonInfo.private.nameChecked ¬ TRUE;
buttonInfo.private.name ¬ name;
}
ELSE Error["GetName: Invalid Name field!"];
};
};
};
};
};
GetButtonClass: PUBLIC PROC [buttonInfo: ButtonInfo] RETURNS [buttonClass: ButtonClass ¬ NIL] = {
buttonClass ¬ buttonInfo.private.class;
};
GetDocClassName: PUBLIC PROC [doc: ActiveDoc] RETURNS[name: ATOM] = {
name ¬ doc.private.class.name;
};
GetFieldRope: PUBLIC PROC [key: ATOM, buttonInfo: ButtonInfo] RETURNS [rope: ROPE] = {
Gets the rope corresponding to the key in the specification of the button.
rope ¬ EBLanguage.GetFieldRope[buttonInfo.private.symbols, key];
};
GetFieldAtom: PUBLIC PROC [key: ATOM, buttonInfo: ButtonInfo] RETURNS [atom: ATOM] = {
Gets the atom corresponding to the key in the specification of the button.
atom ¬ EBLanguage.GetFieldAtom[buttonInfo.private.symbols, key];
};
GetFieldRef: PUBLIC PROC [key: ATOM, buttonInfo: ButtonInfo] RETURNS [ref: REF] = {
Gets the ref corresponding to the key in the specification of the button.
ref ¬EBLanguage.GetFieldRef[buttonInfo.private.symbols, key];
};
SetFieldRope: PUBLIC PROC [key: ATOM, rope: ROPE, buttonInfo: ButtonInfo] = {
Gets the rope corresponding to the key in the specification of the button.
EBLanguage.SetFieldRope[buttonInfo.private.symbols, buttonInfo.private.order, key, rope];
};
SetFieldAtom: PUBLIC PROC [key: ATOM, atom: ATOM, buttonInfo: ButtonInfo] = {
Gets the atom corresponding to the key in the specification of the button.
EBLanguage.SetFieldAtom[buttonInfo.private.symbols, buttonInfo.private.order, key, atom];
};
SetFieldRef: PUBLIC PROC [key: ATOM, ref: REF, buttonInfo: ButtonInfo] = {
Gets the ref corresponding to the key in the specification of the button.
EBLanguage.SetFieldRef[buttonInfo.private.symbols, buttonInfo.private.order, key, ref];
};
GetAtom: PUBLIC PROC [key: ATOM, buttonInfo: ButtonInfo] RETURNS [atom: ATOM ¬ NIL] = {
r: REF ¬ buttonInfo.doc.private.class.getRef[key, buttonInfo.button, buttonInfo.doc];
IF r = NIL THEN RETURN;
WITH r SELECT FROM
a: ATOM => atom ¬ a;
ENDCASE => Error["EmbeddedButtons: Value returned to GetAtom was not an ATOM."];
};
SetAtom: PUBLIC PROC [key: ATOM, atom: ATOM, buttonInfo: ButtonInfo, edited: BOOL] = {
buttonInfo.doc.private.class.setRef[key, buttonInfo.button, buttonInfo.doc, atom, edited];
};
GetRope: PUBLIC PROC [key: ATOM, buttonInfo: ButtonInfo] RETURNS [rope: ROPE] = {
r: REF ¬ buttonInfo.doc.private.class.getRef[key, buttonInfo.button, buttonInfo.doc];
IF r = NIL THEN RETURN;
WITH r SELECT FROM
a: ROPE => rope ¬ a;
ENDCASE => Error["EmbeddedButtons: Value returned to GetRope was not a ROPE."];
};
SetRope: PUBLIC PROC [key: ATOM, rope: ROPE, buttonInfo: ButtonInfo, edited: BOOL] = {
buttonInfo.doc.private.class.setRef[key, buttonInfo.button, buttonInfo.doc, rope, edited];
};
GetRef: PUBLIC PROC [key: ATOM, buttonInfo: ButtonInfo] RETURNS [ref: REF] = {
ref ¬ buttonInfo.doc.private.class.getRef[key, buttonInfo.button, buttonInfo.doc];
};
SetRef: PUBLIC PROC [key: ATOM, ref: REF, buttonInfo: ButtonInfo, edited: BOOL] = {
buttonInfo.doc.private.class.setRef[key, buttonInfo.button, buttonInfo.doc, ref, edited];
};
GetText: PUBLIC PROC [buttonInfo: ButtonInfo] RETURNS [text: ROPE] = {
text ¬ buttonInfo.doc.private.class.getText[buttonInfo.button, buttonInfo.doc];
};
GetDocName: PUBLIC PROC [buttonInfo: ButtonInfo] RETURNS [name: ROPE] = {
name ¬ buttonInfo.doc.private.class.getDocName[buttonInfo.doc];
};
Internal Routines
ParseFeedbackTable: PROC [r: ROPE] RETURNS [f: REF FeedbackTableObj ¬ NIL] = {
s: IO.STREAM;
CodeTimer.StartInt[$ParseFeedbackTable, $EmbeddedButtons];
s ¬ IO.RIS[r];
f ¬ ParseCurrentFeedbackTable[s];
CodeTimer.StopInt[$ParseFeedbackTable, $EmbeddedButtons];
};
ParseCurrentFeedbackTable: PROC [s: IO.STREAM] RETURNS [f: REF FeedbackTableObj ¬ NIL] = {
object: REF;
head: FeedbackTableObj;
tail: FeedbackTableObj;
char: CHAR;
f ¬ NEW[FeedbackTableObj ¬ NIL];
[] ¬ s.SkipWhitespace[];
char ¬ s.PeekChar[];
IF char # '( AND char # '< THEN {
Error["EmbeddedButtons: The value of Feedback: must be in parens or angle brackets"];
RETURN;
};
object ¬ EBMesaLisp.Parse[s].val;
IF NOT ISTYPE[object, LIST OF REF] THEN {
Error["EmbeddedButtons: The value of Feedback: must be a list"];
RETURN;
};
head ¬ LIST[NIL];
tail ¬ head;
FOR l: LIST OF REF ¬ NARROW[object], l.rest UNTIL l = NIL DO
first: LIST OF REF;
IF NOT ISTYPE[l.first, LIST OF REF] THEN {
Error["EmbeddedButtons: Entries in a feedback table must be event-action pairs"];
RETURN;
};
first ¬ NARROW[l.first];
IF NOT ISTYPE[first.first, ATOM] AND NOT ISTYPE[first.first, LIST OF REF] THEN {
Error["EmbeddedButtons: Invalid event in feedback table"];
RETURN;
};
IF NOT ISTYPE[first.rest.first, LIST OF REF] THEN {
Error["EmbeddedButtons: Invalid action in feedback table"];
RETURN;
};
tail.rest ¬ LIST[NEW[FeedbackTableEntryRec ¬ [
event: IF ISTYPE[first.first, ATOM] THEN LIST[first.first] ELSE NARROW[first.first],
action: NARROW[first.rest.first]]]];
tail ¬ tail.rest;
ENDLOOP;
f­ ¬ head.rest;
};
ConfirmButtonInfo: PROC [buttonInfo: ButtonInfo] RETURNS [success: BOOL ¬ TRUE] = {
IF buttonInfo.private.fieldValuesOK THEN {
CodeTimer.StartInt[$ConfirmButtonInfoQuick, $EmbeddedButtons];
success ¬ TRUE;
IF buttonInfo.private.instanceData = NIL THEN {
buttonInfo.private.instanceData ¬ buttonInfo.private.class.instantiate[
buttonInfo: buttonInfo,
language: buttonInfo.private.language,
languageVersion: buttonInfo.private.languageVersion];
};
CodeTimer.StopInt[$ConfirmButtonInfoQuick, $EmbeddedButtons];
}
ELSE {
CodeTimer.StartInt[$ConfirmButtonInfo, $EmbeddedButtons];
success ¬ ConfirmSymbols[buttonInfo];
IF NOT success THEN {CodeTimer.StopInt[$ConfirmButtonInfo, $EmbeddedButtons]; RETURN};
IF buttonInfo.private.instanceData = NIL THEN {
buttonInfo.private.instanceData ¬ buttonInfo.private.class.instantiate[
buttonInfo: buttonInfo,
language: buttonInfo.private.language,
languageVersion: buttonInfo.private.languageVersion];
};
CodeTimer.StopInt[$ConfirmButtonInfo, $EmbeddedButtons];
};
};
ConfirmFields: PROC [buttonInfo: ButtonInfo] RETURNS [success: BOOL ¬ TRUE] = {
Pick apart the ButtonData into fields, making an entry in "symbols" for each field. Don't parse the value section of each field.
We expect ButtonData to be in the form:
Poppy1 Class: <classname> <fieldname1>: <field1> <fieldname2>: <field2> etc.
buttonClassRope: ROPE;
buttonClassName: ATOM;
stream: IO.STREAM;
languageName: ROPE;
IF buttonInfo.private.class # NIL THEN RETURN; -- field names are OK
CodeTimer.StartInt[$ConfirmFields, $EmbeddedButtons];
stream ¬ IO.RIS[buttonInfo.private.rope];
languageName ¬ EBMesaLisp.ReadWWord[stream];
BEGIN
IF NOT Rope.Equal[languageName, "Poppy1"] THEN {
Error[Rope.Concat["Unknown button language: ", languageName]];
CodeTimer.StopInt[$ConfirmFields, $EmbeddedButtons];
RETURN[FALSE];
};
buttonInfo.private.language ¬ $Poppy;
buttonInfo.private.languageVersion ¬ 1;
[buttonInfo.private.symbols, buttonInfo.private.order] ¬ EBLanguage.PoppyParseFieldNames[stream];
buttonClassRope ¬ EBLanguage.GetFieldRope[buttonInfo.private.symbols, $Class];
buttonClassName ¬ Atom.MakeAtom[buttonClassRope];
buttonInfo.private.class ¬ LookupButtonClass[buttonClassName];
IF buttonInfo.private.class = NIL THEN GOTO NoSuchClass;
buttonInfo.private.fieldValuesOK ¬ FALSE;
EXITS
NoSuchClass => {
IF NOT buttonInfo.private.hasComplained THEN {
Error[Rope.Concat["Button class not registered: ", buttonClassRope], Rope.Concat["The button class for this button has not been registered: ", buttonClassRope]];
buttonInfo.private.hasComplained ¬ TRUE;
};
CodeTimer.StopInt[$ConfirmFields, $EmbeddedButtons];
RETURN[FALSE];
};
END;
CodeTimer.StopInt[$ConfirmFields, $EmbeddedButtons];
};
ConfirmSymbols: PROC [buttonInfo: ButtonInfo] RETURNS [success: BOOL ¬ TRUE] = {
Pick apart the ButtonData into fields, making an entry in "symbols" for each field. Parse the value section of each field.
We expect ButtonData to be in the form:
Poppy1 Class: <classname> <fieldname1>: <field1> <fieldname2>: <field2> etc.
IF buttonInfo.private.fieldValuesOK THEN RETURN; -- field names and values are OK
CodeTimer.StartInt[$ConfirmSymbols, $EmbeddedButtons];
IF buttonInfo.private.class = NIL THEN { -- field names and values are both needed
buttonClassRope: ROPE;
buttonClassName: ATOM;
stream: IO.STREAM ¬ IO.RIS[buttonInfo.private.rope];
languageName: ROPE ¬ EBMesaLisp.ReadWWord[stream];
BEGIN
IF NOT Rope.Equal[languageName, "Poppy1"] THEN {
Error[Rope.Concat["Unknown button language: ", languageName]];
CodeTimer.StopInt[$ConfirmSymbols, $EmbeddedButtons];
RETURN[FALSE];
};
buttonInfo.private.language ¬ $Poppy;
buttonInfo.private.languageVersion ¬ 1;
[buttonInfo.private.symbols, buttonInfo.private.order] ¬ EBLanguage.PoppyParse[stream];
buttonClassRope ¬ EBLanguage.GetFieldRope[buttonInfo.private.symbols, $Class];
buttonClassName ¬ Atom.MakeAtom[buttonClassRope];
buttonInfo.private.class ¬ LookupButtonClass[buttonClassName];
IF buttonInfo.private.class = NIL THEN GOTO NoSuchClass;
EXITS
NoSuchClass => {
IF NOT buttonInfo.private.hasComplained THEN {
Error[Rope.Concat["Button class not registered: ", buttonClassRope], Rope.Concat["The button class for this button has not been registered: ", buttonClassRope]];
buttonInfo.private.hasComplained ¬ TRUE;
};
CodeTimer.StopInt[$ConfirmSymbols, $EmbeddedButtons];
RETURN[FALSE];
};
END;
}
ELSE {
Field names are OK. Just parse the values.
EBLanguage.PoppyParseFieldValues[buttonInfo.private.symbols];
};
buttonInfo.private.fieldValuesOK ¬ TRUE;
CodeTimer.StopInt[$ConfirmSymbols, $EmbeddedButtons];
};
LookupButtonClass: PROC [buttonClassName: ATOM] RETURNS [buttonClass: ButtonClass ¬ NIL] = {
found: BOOL;
b: REF ANY;
[found, b] ¬ RefTab.Fetch[classTable, buttonClassName];
IF NOT ISTYPE[b, ButtonClass] THEN {
Error["EmbeddedButtons: Internal error. Button class is wrong type (LookupButtonClass).", ];
RETURN;
};
buttonClass ¬ NARROW[b];
};
MarkButtonAsChanged: PUBLIC PROC [buttonInfo: ButtonInfo] = {
Tell the editor that the document has changed by setting the ButtonData property on the button
buttonInfo.private.changed ¬ TRUE;
buttonInfo.doc.private.class.setRef[$ButtonData, buttonInfo.button, buttonInfo.doc, buttonInfo, TRUE];
};
Initialization
classTable: RefTab.Ref ¬ RefTab.Create[];
registeredNotifyTable: RefTab.Ref ¬ RefTab.Create[];
EBLanguage.RegisterFieldParseProc[$Feedback, ParseFeedbackField];
EBLanguage.RegisterFieldUnparseProc[$Feedback, UnparseFeedbackField];
END.
$ButtonFeedback is an obsolete property. Where still used, this property is in the following format:
buttonFeedback ← "languageVersion ( feedbackTable )"
languageVersion ← 19890713
feedbackTable ← feedbackTableEntry | feedbackTableEntry feedbackTable
feedbackTableEntry ← ( event entryActions )
event ← userEvent | applicationEvent | ( qualifiersAndEvent ) | ( applicationEvents )
userEvent ← ATOM [usually $Up or $Down]
applicationEvent ← ATOM
applicationEvents ← applicationEvent | applicationEvent ApplicationEvents
qualifiersAndEvent ← qualifier
| qualifier qualifiersAndEvent
| userEvent
| userEvent qualifiersAndEvent
qualifier ← ATOM [usually $Red, $Control, $Shift, etc.]
entryActions ← entryAction | ( entryActionList )
entryActionList ← entryAction | entryAction entryActionList
entryAction ← action | handlerAction
handlerAction ← ( handler handlerParameters )
handler ← ATOM [the name of the handler]
handlerParameters ← handlerParameter | handlerParameter handlerParameters
handlerParameter ← value
value ← ATOM, BOOL, INT, REAL, or ROPE [a button value]
action ← ROPE [editor specific formatted feedback ROPE]