GGActiveImpl.mesa
Copyright Ó 1988, 1989, 1991, 1992 by Xerox Corporation. All rights reserved.
Contents: Routines that support embedded buttons within Gargoyle.
Kenneth A. Pier, October 1, 1992 2:02 pm PDT
Bier, March 16, 1993 3:01 pm PST
Doug Wyatt, April 20, 1992 12:24 pm PDT
DIRECTORY
BiScrollers, CodeTimer, Commander, EBEditors, EBEditorsExtras, EBNullDoc, EBTypes, EmbeddedButtons, Feedback, FeedbackTypes, GGActive, GGAlign, GGBasicTypes, GGControlPanelTypes, GGCoreTypes, GGEmbedTypes, GGEvent, GGEventExtras, GGHistoryTypes, GGInterfaceTypes, GGModelTypes, GGMouseEvent, GGMultiGravity, GGProps, GGScene, GGSliceOps, GGState, GGUIUtility, GGUserInput, GGWindow, Imager, ImagerTransformation, InputFocus, IO, List, MultiCursors, Rope, ScreenCoordsTypes, TIPUser, ViewerClasses, ViewerOps;
GGActiveImpl: CEDAR PROGRAM
IMPORTS BiScrollers, CodeTimer, Commander, EBEditors, EBEditorsExtras, EBNullDoc, EmbeddedButtons, Feedback, GGAlign, GGEventExtras, GGMouseEvent, GGMultiGravity, GGProps, GGScene, GGSliceOps, GGState, GGUIUtility, GGUserInput, ImagerTransformation, InputFocus, IO, List, MultiCursors, TIPUser, ViewerOps
EXPORTS GGActive, GGInterfaceTypes = BEGIN
ActiveDoc: TYPE = EBTypes.ActiveDoc;
ActiveButton: TYPE = EBTypes.ActiveButton;
BoundBox: TYPE = GGModelTypes.BoundBox;
Color: TYPE = Imager.Color;
ControlsObj: PUBLIC TYPE = GGControlPanelTypes.ControlsObj; -- exported to GGInterfaceTypes
EmbedDataObj: PUBLIC TYPE = GGEmbedTypes.EmbedDataObj; -- exported to GGInterfaceTypes
FeatureData: TYPE = GGModelTypes.FeatureData;
GGData: TYPE = GGInterfaceTypes.GGData;
HistoryEvent: TYPE = GGHistoryTypes.HistoryEvent;
MsgRouter: TYPE = FeedbackTypes.MsgRouter;
Point: TYPE = GGBasicTypes.Point;
Scene: TYPE = GGModelTypes.Scene;
SequenceOfReal: TYPE = GGCoreTypes.SequenceOfReal;
Slice: TYPE = GGModelTypes.Slice;
SliceDescriptor: TYPE = GGModelTypes.SliceDescriptor;
StrokeEnd: TYPE = Imager.StrokeEnd;
TIPScreenCoords: TYPE = ScreenCoordsTypes.TIPScreenCoords;
TIPScreenCoordsRec: TYPE = ScreenCoordsTypes.TIPScreenCoordsRec;
Transformation: TYPE = ImagerTransformation.Transformation;
UserInputProc: TYPE = GGEvent.UserInputProc;
Viewer: TYPE = ViewerClasses.Viewer;
Color Palette Mini-Application
InitPaletteApplication: PROC = {
EmbeddedButtons.RegisterApplication[$Palette, PaletteNotify];
};
PaletteNotify: EmbeddedButtons.RegisteredNotifyProc = {
PROC[events: LIST OF REF, buttonInfo: ButtonInfo];
ggData: GGData ¬ NARROW[buttonInfo.doc.theDoc];
sliceD: SliceDescriptor ¬ NARROW[buttonInfo.button];
FOR list: LIST OF REF ¬ events, list.rest UNTIL list = NIL DO
SELECT list.first FROM
$TransferFillColor => [] ¬ GGMouseEvent.SetFillColorRemote[ggData, sliceD];
$TransferStrokeColor => [] ¬ GGMouseEvent.SetStrokeColorRemote[ggData, sliceD];
$TransferBothColors => {
[] ¬ GGMouseEvent.SetFillColorRemote[ggData, sliceD];
[] ¬ GGMouseEvent.SetStrokeColorRemote[ggData, sliceD];
};
ENDCASE;
ENDLOOP;
};
ActiveDoc Class
ggActiveDocClass: EBEditors.ActiveDocClass ¬ NEW[EBEditors.ActiveDocClassObj ¬ [
name: $Gargoyle,
getRef: GGGetRef,
setRef: GGSetRef,
mapRef: GGMapRef,
getDocName: GGGetDocName,
feedback: GGButtonFeedback, -- Feedback is the name of an interface
inButton: GGInButton
]];
LookupDoc: PUBLIC PROC [ggData: GGData] RETURNS [doc: ActiveDoc] ~ {
Gargoyle does us a favor here. When a new file is loaded into a viewer, ggData.behavior.activeDoc ← NIL. Thus, we can tell if we need to create a new activeDoc.
IF ggData.behavior.activeDoc = NIL THEN {
doc ¬ EBEditors.CreateActiveDoc[ggData, ggActiveDocClass];
ggData.behavior.activeDoc ¬ doc;
}
ELSE doc ¬ NARROW[ggData.behavior.activeDoc]
};
NullDoc: PUBLIC PROC [ggData: GGData] RETURNS [doc: ActiveDoc] ~ {
doc ¬ EBNullDoc.Create["GGNullDoc"];
How do we find out what buttons and variables should be added? Hard code? User profile? Track GGStateImpl?. KAP. September 30, 1992.
[] ← EBNullDoc.CreateBOOLButton[doc, "Midpoints", FALSE];
[] ← EBNullDoc.CreateBOOLButton[doc, "ShowAlignments", FALSE];
[] ← EBNullDoc.CreateBOOLButton[doc, "DoubleBuffer", TRUE];
[] ← EBNullDoc.CreateBOOLButton[doc, "Active", FALSE]; -- scene itself is not active
[] ← EBNullDoc.CreateBOOLButton[doc, "Editable", TRUE]; -- scene is editable
[] ← EBNullDoc.CreateBOOLButton[doc, "Palette", FALSE];
[] ← EBNullDoc.CreateEnumeratedButton[doc, "ScreenStyle", $SpecifiedFonts, LIST[$SpecifiedFonts, $AlternateFonts, $WYSIWYG]];
[] ← EBNullDoc.CreateBOOLButton[doc, "Gravity", TRUE];
[] ← EBNullDoc.CreateREALButton[doc, "GravityExtent", 25.0];
[] ← EBNullDoc.CreateEnumeratedButton[doc, "GravityType", $PreferPoints, LIST[$PreferLines, $PreferPoints]];
[] ← EBNullDoc.CreateBOOLButton[doc, "Auto", FALSE];
EBNullDoc.Instantiate[doc];
};
<<GGInButton: EBEditors.InButtonProc = {
PROC [button: ActiveButton, doc: ActiveDoc, x, y: INTEGER] RETURNS [BOOL];
pt: Imager.VEC;
feature: FeatureData;
ggData: GGData ¬ NARROW[doc.theDoc];
sliceD: SliceDescriptor ¬ NARROW[button];
sceneBag: GGAlign.TriggerBag ¬ GGAlign.CreateTriggerBag[];
completeD: SliceDescriptor ¬ GGSliceOps.NewParts[sliceD.slice, NIL, slice];
tsc: TIPScreenCoords ¬ NEW[TIPScreenCoordsRec ¬ [x, y, FALSE] ];
[] ¬ ViewerOps.MouseInViewer[tsc]; -- modifies tsc to viewer coords
pt ¬ GGWindow.ViewerToWorld[[tsc.mouseX, tsc.mouseY], ggData];
[] ¬ GGAlign.AddSliceFeature[completeD, sceneBag];
feature ¬ GGMultiGravity.FacesPreferred[ pt, 18.0, GGAlign.emptyAlignBag, sceneBag, ggData].feature;
IF feature = NIL THEN RETURN[FALSE]
ELSE {
scale: REAL ← GGState.GetScaleUnit[ggData];
Feedback.PutF[ggData.router, oneLiner, $DuringMouse, "Cursor on button at [%g, %g]",
[real[pt.x/scale]], [real[pt.y/scale]] ];
RETURN[TRUE];
};
};
>>
GGInButton: EBEditors.InButtonProc = {
PROC [button: ActiveButton, doc: ActiveDoc, x, y: INTEGER] RETURNS [BOOL];
RETURN[TRUE];
};
GGGetDocName: EBEditors.GetDocNameProc ~ {
PROC [doc: ActiveDoc] RETURNS [name: ROPE];
ggData: GGData ¬ NARROW[doc.theDoc];
name ¬ GGState.GetFullName[ggData];
};
GGGetRef: EBEditors.GetRefProc = {
PROC [key: ATOM, button: ActiveButton, doc: ActiveDoc] RETURNS [ref: REF];
sliceD: SliceDescriptor ¬ NARROW[button];
isUnique: BOOL ¬ TRUE;
[ref, isUnique] ¬ GGProps.Get[sliceD.slice, sliceD.parts, key];
IF ref = NIL THEN { -- try inheritance
ggData: GGData ¬ NARROW[doc.theDoc];
rootSlice: Slice ¬ ggData.rootSlice;
[ref, isUnique] ¬ GGProps.Get[rootSlice, NIL, key];
};
};
GGSetRef: EBEditors.SetRefProc = {
PROC [key: ATOM, button: ActiveButton, doc: ActiveDoc, ref: REF];
sliceD: SliceDescriptor ¬ NARROW[button];
GGProps.Put[sliceD.slice, sliceD.parts, key, ref];
};
GGMapRef: EBEditors.MapRefProc = {
PROC [doc: ActiveDoc, docClass: ActiveDocClass, mapProc: EachButtonProc];
ForEachButton: PROC [leaf: Slice] RETURNS [done: BOOL ¬ FALSE] = {
leafD: SliceDescriptor ¬ GGSliceOps.NewParts[leaf, NIL, slice];
buttonData: REF ¬ GGProps.Get[leaf, leafD.parts, $ButtonData].val;
IF buttonData = NIL THEN RETURN;
done ¬ mapProc[leafD, doc];
};
ggData: GGData ¬ NARROW[doc.theDoc];
aborted ¬ GGScene.WalkSlices[ggData.scene, leaf, ForEachButton];
};
GGButtonFeedback: EBEditors.FeedbackProc = {
PROC[button: ActiveButton, doc: ActiveDoc, feedback: REF] RETURNS [REF];
ggData: GGData ¬ NARROW[doc.theDoc];
feedbackRef: REF;
WITH feedback SELECT FROM
rope: Rope.ROPE => feedbackRef ¬ GGUIUtility.ParseFeedbackRope[rope];
ENDCASE => feedbackRef ¬ feedback;
WITH feedbackRef SELECT FROM
a: ATOM => QueueButtonEvent[LIST[a], ggData, button];
l: LIST OF REF => HandleList[l, ggData, button];
ENDCASE => ERROR;
RETURN[feedbackRef];
};
Property support
NoOpFileIn: GGProps.FileinProc = {
PROC [s: STREAM] RETURNS [val: REF];
val ¬ IO.GetRope[s];
};
ButtonDataFileOut: GGProps.FileoutProc = {
PROC [s: STREAM, val: REF] RETURNS [vf: ValFormat ← delimited];
rope: Rope.ROPE;
rope ¬ EBEditors.RopeFromButtonData[val];
s.PutRope[rope];
};
ButtonDataFileIn: GGProps.FileinProc = {
PROC [s: STREAM] RETURNS [val: REF];
rope: Rope.ROPE ¬ IO.GetRope[s];
val ¬ EBEditors.ButtonDataFromRope[rope, FALSE];
};
ButtonDataCopy: GGProps.CopyProc = {
PROC [val: REF] RETURNS [copy: REF];
valAsRope: Rope.ROPE ¬ EBEditors.RopeFromButtonData[val];
copy ¬ EBEditors.ButtonDataFromRope[valAsRope, FALSE];
};
Gargoyle as a Button Application
An obsolete comment: However, if the combination $EBApplications $GetKeyValue $<some value name> occurs, the named value is retrieved from the active document. This allows a button to base its operation on any of the values in the document.
GargoyleHandler: EmbeddedButtons.RegisteredNotifyProc = {
PROC [events: LIST OF REF, buttonInfo: ButtonInfo];
events will be a list of Gargoyle actions, such as (LineWidth "2.3").
viewer: ViewerClasses.Viewer;
inputFocus: InputFocus.Focus;
inputFocus ¬ InputFocus.GetInputFocus[];
IF inputFocus # NIL THEN {
viewer ¬ inputFocus.owner;
IF viewer # NIL AND viewer.class.flavor = $ActionArea THEN {
ggData: GGData ¬ NARROW[BiScrollers.ClientDataOfViewer[viewer]];
HandleList[events, ggData, buttonInfo.button];
};
};
};
GargoyleAsControlPanelHandler: EmbeddedButtons.RegisteredNotifyProc = {
PROC [events: LIST OF REF, buttonInfo: ButtonInfo];
events will be a list of Gargoyle actions, such as (LineWidth "2.3").
ggData: GGData ¬ NARROW[buttonInfo.doc.theDoc];
HandleList[events, ggData, buttonInfo.button];
};
ControlPanelButtonHandler: PUBLIC PROC [ggData: GGData, events: LIST OF REF, buttonInfo: EBTypes.ButtonInfo] = {
Send the event to the named ggData, but extract any needed info from the named buttonInfo to prepare the event.
HandleList[events, ggData, buttonInfo.button];
};
TransferButtonDashes: UserInputProc = {
PROC [ggData: GGData, event: LIST OF REF];
sliceD: SliceDescriptor ¬ NARROW[event.rest.first];
dashed: BOOL ¬ FALSE;
pattern: SequenceOfReal ¬ NIL;
offset, length: REAL ¬ 0.0;
isUnique: BOOL ¬ TRUE;
targetGGData: GGData ¬ GGState.GetGGInputFocus[];
IF targetGGData = NIL THEN {
Feedback.Append[ggData.router, oneLiner, $Complaint, "SetStrokeColorRemote failed: Place input focus in a Gargoyle viewer"];
RETURN;
};
[dashed, pattern, offset, length, isUnique] ¬ GGSliceOps.GetDashed[sliceD.slice, sliceD.parts];
IF isUnique THEN {
GGEventExtras.SetDashed[ggData, dashed, pattern, offset, length];
}
ELSE Feedback.Append[targetGGData.router, oneLiner, $Complaint, "TransferButtonDashes failed: the button has multiple dash patterns"];
};
TransferButtonStrokeWidth: UserInputProc = {
PROC [ggData: GGData, event: LIST OF REF];
sliceD: SliceDescriptor ¬ NARROW[event.rest.first];
strokeWidth: REAL;
isUnique: BOOL ¬ TRUE;
targetGGData: GGData ¬ GGState.GetGGInputFocus[];
IF targetGGData = NIL THEN {
Feedback.Append[ggData.router, oneLiner, $Complaint, "SetStrokeColorRemote failed: Place input focus in a Gargoyle viewer"];
RETURN;
};
[strokeWidth, isUnique] ¬ GGSliceOps.GetStrokeWidth[sliceD.slice, sliceD.parts];
IF isUnique THEN {
GGEventExtras.SetStrokeWidth[ggData, strokeWidth];
}
ELSE Feedback.Append[targetGGData.router, oneLiner, $Complaint, "TransferButtonStrokeWidth failed: the button has multiple dash patterns"];
};
TransferButtonStrokeEnd: UserInputProc = {
PROC [ggData: GGData, event: LIST OF REF];
sliceD: SliceDescriptor ¬ NARROW[event.rest.first];
strokeEnd: StrokeEnd;
isUnique: BOOL ¬ TRUE;
targetGGData: GGData ¬ GGState.GetGGInputFocus[];
IF targetGGData = NIL THEN {
Feedback.Append[ggData.router, oneLiner, $Complaint, "SetStrokeColorRemote failed: Place input focus in a Gargoyle viewer"];
RETURN;
};
[strokeEnd, isUnique] ¬ GGSliceOps.GetStrokeEnd[sliceD.slice, sliceD.parts];
IF isUnique THEN {
GGEventExtras.SetStrokeEnd[ggData, strokeEnd];
}
ELSE Feedback.Append[targetGGData.router, oneLiner, $Complaint, "TransferButtonStrokeEnd failed: the button has multiple dash patterns"];
};
Notify procedures
ActiveInputHandler: PUBLIC PROC [self: Viewer, ggData: GGData,
input: LIST OF REF, notify: PROC [LIST OF REF]] = {
event: EBTypes.Event;
IF ggData.embed.beingBorn THEN RETURN; -- don't handle input while window is being built
CodeTimer.StartInt[$RawInputNotify, $Gargoyle];
IF EBEditors.ValidEvent[event ¬ EBEditors.GetEvent[input]] THEN { -- raw input
bs: BiScrollers.BiScroller ~ BiScrollers.QuaBiScroller[self];
mouseAction: BOOL ~ EBEditors.MouseAction[event];
mouseAllUp: BOOL ~ EBEditors.MouseAllUp[event];
button: ActiveButton ¬ NIL; -- will be set to the active button, if any
doc: ActiveDoc ¬ NIL;
IF GGState.GetActive[ggData] -- activity enabled
AND ggData.behavior.rawInputMode=$None -- not in $Gargoyle mode
AND mouseAction -- it's some kind of mouse action
THEN { -- test for an active button at the cursor position
GetMousePosition: PROC [mc: TIPScreenCoordsRec] RETURNS [mouseGG: Point] ~ {
viewerToClient: Transformation ~ bs.class.style.GetTransforms[bs].viewerToClient;
mouseBiScroller: TIPScreenCoords ~ NEW[TIPScreenCoordsRec ¬ mc];
v: Viewer; inClient: BOOL;
[v, inClient] ¬ ViewerOps.MouseInViewer[mouseBiScroller];
IF NOT inClient THEN Feedback.PutFL[ggData.router, oneLiner, $Debug, "GG: %g,%g; ", LIST[[integer[mouseBiScroller.mouseX]], [integer[mouseBiScroller.mouseY]]] ];
mouseGG ¬ ImagerTransformation.Transform[m: viewerToClient, v: [mouseBiScroller.mouseX, mouseBiScroller.mouseY]];
};
mouseGG: Point ~ GetMousePosition[EBEditors.MouseCoords[event]];
feature: FeatureData;
IF EBEditorsExtras.MouseMotion[event] AND EBEditors.MouseAllUp[event] THEN {
If the cursor is moving, then do nothing (for now). This will help EmbeddedButtons keep up on large complex illustrations.
feature ¬ NIL;
Here is the plan: Use UserInputOpsExtras2.GetPosition:
If the coordinates of this event are the most recent coordinates TIP has seen, then check to see if the cursor is over a button and update the cursor pattern accordingly. Otherwise, ignore this event.
cursorCoords, latestCursorCoords: Point ¬ [0,0];
handle: UserInput.Handle ¬ EBEditorsExtras.GetHandle[event];
cursorCoords ¬ UserInputOpsExtras2.GetPosition[handle];
latestCursorCoords ¬ UserInputOpsExtras2.GetLatestPosition[handle];
IF PointsEqual[latestCursorCoords, cursorCoords] THEN
feature ¬ GGMultiGravity.FacesPreferred[mouseGG, 18.0, GGAlign.emptyAlignBag, ggData.hitTest.sceneBag, ggData].feature
ELSE feature ¬ NIL;
}
ELSE {
feature ¬ GGMultiGravity.FacesPreferred[mouseGG, 18.0, GGAlign.emptyAlignBag, ggData.hitTest.sceneBag, ggData].feature;
};
IF feature#NIL THEN {
doc ¬ LookupDoc[ggData];
WITH feature.shape SELECT FROM
sliceD: SliceDescriptor => {
buttonData: REF ~ GGGetRef[$ButtonData, sliceD, doc];
IF buttonData#NIL THEN button ¬ sliceD; -- found an active button
};
ENDCASE;
};
};
IF button#NIL THEN {
EBEditors.HandleEvent[event, button, doc];
}
ELSE {
IF GGState.GetReadOnly[ggData] THEN {
Ignore this gargoyle action and display the "read-only" cursor, currently textPointer
IF MultiCursors.GetACursor[NIL]#textPointer
THEN MultiCursors.SetACursor[textPointer, NIL];
}
ELSE {
result: LIST OF REF ¬ EBEditors.ParseEvent[ggData.tipTable, event];
result ← CopyEventCoordinates[result];
IF mouseAction THEN {
cursorType: MultiCursors.CursorType;
cursorType ¬ ggData.controls.cursor;
IF cursorType#MultiCursors.GetACursor[NIL]
THEN MultiCursors.SetACursor[cursorType, NIL];
The only other SetACursor call in Gargoyle is in GGWindowImpl.SetCursorLooks.
};
IF NOT mouseAllUp THEN ggData.behavior.rawInputMode ¬ $Gargoyle;
IF result#NIL THEN notify[result];
};
};
IF mouseAllUp THEN ggData.behavior.rawInputMode ¬ $None;
}
ELSE notify[input];
CodeTimer.StopInt[$RawInputNotify, $Gargoyle];
};
CopyEventCoordinates: PROC [event: LIST OF REF] RETURNS [newEvent: LIST OF REFNIL] = {
This copies the list, substituting REF Imager.VEC for TIPScreenCoords. This is useful and also avoids the problem of TIP overwritting the TIPScreenCoords storage!
tail: LIST OF REFNIL;
FOR list: LIST OF REF ← event, list.rest UNTIL list = NIL DO
IF tail = NIL
THEN tail ← newEvent ← CONS[list.first, NIL]
ELSE tail ← tail.rest ← CONS[list.first, NIL];
WITH list.first SELECT FROM
z: TIPScreenCoords => tail.first ← NEW [Imager.VEC ← [z.mouseX, z.mouseY] ];
ENDCASE;
ENDLOOP;
};
QueueButtonEvent: PROC [event: LIST OF REF, ggData: GGData, button: REF] = {
newEvent: LIST OF REF;
IF event.first = $SelectButton OR event.first = $BeginButton OR event.first = $TransferButtonDashes OR event.first = $TransferButtonStrokeWidth OR event.first = $TransferButtonStrokeEnd THEN {
sliceD: SliceDescriptor ¬ NARROW[button];
newEvent ¬ LIST[event.first, sliceD]; -- make a copy of the eventList, because EmbeddedButtons resuses the original
}
ELSE IF event.first = $ButtonFillColorFromIntensity OR event.first = $ButtonStrokeColorFromIntensity THEN {
sliceD: SliceDescriptor ¬ NARROW[button];
newEvent ¬ LIST[event.first, event.rest.first, sliceD]; -- make a copy of the eventList, because EmbeddedButtons resuses the original
}
ELSE newEvent ¬ List.Append[event]; -- make a copy of the eventList, because EmbeddedButtons resuses the original
GGUserInput.EventNotify[ggData, newEvent];
};
HandleList: PROC [list: LIST OF REF, ggData: GGData, button: REF] = {
WITH list.first SELECT FROM
l: LIST OF REF => {
FOR list ¬ list, list.rest UNTIL list = NIL DO
WITH list.first SELECT FROM
childList: LIST OF REF => HandleList[childList, ggData, button];
ENDCASE => SIGNAL SyntaxError[msg: "Input list mixes LIST elements and ATOM elements"];
ENDLOOP;
};
ENDCASE => QueueButtonEvent[list, ggData, button];
};
SyntaxError: PUBLIC SIGNAL[msg: Rope.ROPE] = CODE;
GGActiveHandler: Commander.CommandProc = {};
Initialization
ggActionAreaClass: ViewerClasses.ViewerClass ~ ViewerOps.FetchViewerClass[$ActionArea];
ggActionAreaClass.tipTable ¬ TIPUser.TransparentTIPTable[]; -- all viewers made after this call will be able to be active
GGProps.Register[$ButtonData, ButtonDataFileIn, ButtonDataFileOut, ButtonDataCopy];
InitPaletteApplication[];
GGUserInput.RegisterAction[$TransferButtonDashes, TransferButtonDashes, none];
GGUserInput.RegisterAction[$TransferButtonStrokeWidth, TransferButtonStrokeWidth, none];
GGUserInput.RegisterAction[$TransferButtonStrokeEnd, TransferButtonStrokeEnd, none];
Commander.Register[key: "GGActive", proc: GGActiveHandler, doc: "Does nothing. Active Gargoyle is already loaded", clientData: NIL, interpreted: TRUE];
EmbeddedButtons.RegisterApplication[$Gargoyle, GargoyleHandler];
EmbeddedButtons.RegisterApplication[$GargoyleAsControlPanel, GargoyleAsControlPanelHandler];
END.