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 [mouse
GG: 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
REF ←
NIL] = {
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 REF ← NIL;
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.