GGHistoryImpl.mesa
Copyright Ó 1988, 1991, 1992 by Xerox Corporation. All rights reserved.
Pier, February 25, 1991 7:19 pm PST
Bier, March 13, 1991 5:44 pm PST
Doug Wyatt, April 16, 1992 5:38 pm PDT
DIRECTORY
Buttons, CedarProcess, Containers, Feedback, GGCaret, GGEvent, GGHistory, GGHistoryTypes, GGHistoryTypesOpaque, GGInterfaceTypes, GGModelTypes, GGParent, GGScene, GGSelect, GGSlice, GGSliceOps, GGState, GGUtility, GGWindow, IO, MBQueue, Process, RefTab, Rope, Rules, SlackProcess, TEditOps, ViewerClasses, ViewerOps, ViewerSpecs, ViewerTools;
GGHistoryImpl: CEDAR MONITOR
IMPORTS GGParent, CedarProcess, Containers, Feedback, GGCaret, GGEvent, GGHistory, GGScene, GGSelect, GGSlice, GGSliceOps, GGState, GGUtility, GGWindow, IO, MBQueue, Process, RefTab, Rope, Rules, SlackProcess, TEditOps, ViewerOps, ViewerSpecs, ViewerTools
EXPORTS GGHistory, GGHistoryTypes = BEGIN
Change: PUBLIC TYPE = GGHistoryTypesOpaque.Change; -- exported to GGHistoryTypes
GGData: TYPE = GGInterfaceTypes.GGData;
HistoryEvent: TYPE = GGHistoryTypes.HistoryEvent;
HistoryEventObj: TYPE = GGHistoryTypes.HistoryEventObj;
HistoryTool: TYPE = REF HistoryToolObj;
HistoryToolObj: PUBLIC TYPE = GGHistory.HistoryToolObj; -- exported to GGHistoryTypes
HistoryProc: TYPE = GGHistoryTypes.HistoryProc;
SubEvent: TYPE = GGHistoryTypes.SubEvent;
SubEventObj: TYPE = GGHistoryTypes.SubEventObj;
Layout: TYPE = GGHistory.Layout;
LayoutRec: TYPE = GGHistory.LayoutRec;
SlackHandle: TYPE = SlackProcess.SlackHandle;
Slice: TYPE = GGInterfaceTypes.Slice;
SliceWalkProc: TYPE = GGModelTypes.SliceWalkProc;
DebugHalt: SIGNAL = CODE;
KillError: SIGNAL = CODE;
noHistory: BOOL ¬ FALSE; -- for performance testing. Eliminates all history activity
tempCapture: BOOL ¬ TRUE; -- temporarily substitutes captures for currents. Required until slice property Get procs can return multiple values.
Booleans which control tricky spots to aid in bug tracking
doSelections: BOOL ¬ TRUE;
doUnlink: BOOL ¬ TRUE;
debugSlice: Slice ¬ NIL;
NewCurrent: PUBLIC PROC [name: Rope.ROPE, ggData: GGData] RETURNS [HistoryEvent ¬ NIL] = { -- equivalent to SetCurrent[ggData, Create[name] ]
IF noHistory THEN RETURN ELSE {
count: INT ¬ GGScene.CountSelectedSlices[ggData.scene, first, normal];
nameAndCount: Rope.ROPE ¬ IF count=0 THEN IO.PutFR1["%g no object", [rope[name]]]
ELSE IO.PutFR["%g %g object%g", [rope[name]], [integer[count]], [rope[IF count>1 THEN "s" ELSE NIL]] ];
IF tempCapture THEN {
temporarily do a capture instead of a NewCurrent for all operations except
transforms, interactive motion, undo, and set scale unit
SELECT TRUE FROM
Rope.Equal["Transform", name, FALSE], -- from GGEventImplC
Rope.Equal["Motion:", Rope.Substr[base: name, start: 0, len: 7], FALSE], -- from GGMouseEventImplA
Rope.Equal["Undo", Rope.Substr[base: name, start: 0, len: 4], FALSE], -- from UndoN
Rope.Equal["Set scale unit", name, FALSE] => { -- from GGEventImplB
KillAdvanceCapture[ggData];-- KillAdvanceCapture synchronizes with already launched AdvanceCapture
SetCurrent[ggData, Create[nameAndCount]];
RETURN[GetCurrent[ggData].event];
};
ENDCASE => {
temporary capture
SetCurrent[ggData, Create[nameAndCount]];
Capture[ggData, GetCurrent[ggData].event, FALSE];
NewCapture[name, ggData]; -- NewCapture synchronizes with already launched AdvanceCapture
RETURN[NIL];
};
}
ELSE {
KillAdvanceCapture[ggData];-- KillAdvanceCapture synchronizes with already launched AdvanceCapture
SetCurrent[ggData, Create[nameAndCount]];
RETURN[GetCurrent[ggData].event];
};
};
};
PushCurrent: PUBLIC PROC [ggData: GGData] = { -- pushes current event onto history list
IF noHistory THEN RETURN ELSE {
event: HistoryEvent ¬ GetCurrent[ggData].event;
IF GGSliceOps.debugHalt THEN SIGNAL DebugHalt;
IF event#NIL THEN {
SetSliceIndex: PROC [slice: Slice] RETURNS [done: BOOL ¬ FALSE] = {
slice.historyTop ¬ newIndex;
};
oldTail, tail: LIST OF HistoryEvent; -- will be list of stale events to dismantle
newIndex: INT ¬ ggData.history.currentIndex+1; -- get ready for new push
WalkSlicesInEvent[event, SetSliceIndex]; -- record the newIndex in slice.historyTop in each slice. slice.historyTop is the index of the latest history event a given slice is a member of
event.index ¬ newIndex;
Push[event, GetHistory[ggData] ];
ggData.history.currentIndex ¬ newIndex; -- record new index after Push succeeded
Maintain the maximum size of the history list
tail ¬ GetHistory[ggData]; -- get expanded history list
THROUGH [0..ggData.history.maxSize) DO IF tail=NIL THEN EXIT ELSE {
oldTail ¬ tail;
tail ¬ tail.rest;
};
ENDLOOP;
IF tail#NIL AND tail.first#NIL THEN { -- tail may be LIST[NIL]
MarkSlices: PROC [slice: Slice] RETURNS [done: BOOL ¬ FALSE] = {
TopHistory: PROC [slice: Slice] RETURNS [done: BOOL ¬ FALSE] = {
slice.historyTop ¬ newIndex;
};
DescendASlice[slice, TopHistory];
};
sliceList, ptr: LIST OF Slice;
[sliceList, ptr] ¬ GGUtility.StartSliceList[];
Here's a neat trick. Mark all the slices in the scene with the new Index by setting every historyTop value to the new index. Then, for every event in the tail, unlink any slices that do not appear in higher numbered history events. Objects in the scene will never be unlinked because they have just been marked with the highest possible number, the new index.
[] ¬ GGScene.WalkSlices[ggData.scene, first, MarkSlices];
FOR tailEvents: LIST OF HistoryEvent ¬ tail, tailEvents.rest UNTIL tailEvents=NIL DO
ResetEvent[tailEvents.first, sliceList, ptr];
ENDLOOP;
oldTail.rest ¬ NIL; -- this truncates the actual history list
UnlinkListedSlices[sliceList]; -- do your dirty work
};
};
};
};
KillAdvanceCapture: PUBLIC ENTRY PROC [ggData: GGData] = {
ENABLE UNWIND => NULL;
KillAdvanceCaptureInternal[ggData];
};
KillAdvanceCaptureInternal: INTERNAL PROC [ggData: GGData] = {
advanceProcess: CedarProcess.Process ¬ ggData.history.advanceProcess;
advanceEvent: HistoryEvent ¬ ggData.history.advanceEvent;
IF GGSliceOps.debugHalt THEN SIGNAL DebugHalt;
IF ggData.history.advanceProcess#NIL THEN {
ggData.history.advanceProcess ¬ NIL;
ggData.history.advanceEvent ¬ NIL;
IF CedarProcess.GetStatus[advanceProcess]=busy THEN CedarProcess.Abort[advanceProcess];
[] ¬ CedarProcess.Join[process: advanceProcess, wait: TRUE];
IF advanceEvent#NIL AND NOT Rope.Equal[advanceEvent.name, "advance", FALSE] THEN SIGNAL KillError; -- debugging check
IF advanceEvent#NIL THEN [] ¬ CedarProcess.Fork[action: DiscardAction, data: advanceEvent, options: [priority: tryPriority, usePriority: TRUE]]; -- detach process by dropping return value. Start it in the foreground because of the way CedarProcess works. DiscardAction can drop to the background.
};
};
DiscardAction: CedarProcess.ForkableProc = {
PROC [data: REF] RETURNS [results: REFNIL];
advanceEvent: HistoryEvent ¬ NARROW[data];
IF advanceEvent#NIL AND NOT Rope.Equal[advanceEvent.name, "advance", FALSE] THEN SIGNAL KillError; -- debugging check
IF advanceEvent#NIL THEN {
CedarProcess.SetPriority[priority: background];
Process.Yield[];
GGUtility.UnlinkCapturedScene[advanceEvent];
};
};
tryPriority: CedarProcess.Priority ¬ normal;
DoAdvanceCapture: PUBLIC ENTRY PROC [ggData: GGData] = {
ENABLE UNWIND => NULL;
IF noHistory THEN RETURN ELSE {
IF GGSliceOps.debugHalt THEN SIGNAL DebugHalt;
new Advance stuff
IF ggData.history.advanceProcess#NIL THEN KillAdvanceCaptureInternal[ggData]; -- important for synchronization
ggData.history.advanceEvent ¬ Create["advance"];
ggData.history.advanceProcess ¬ CedarProcess.Fork[action: CaptureAction, data: ggData, options: [priority: tryPriority, usePriority: TRUE] ]; -- remember the advance process. Start it in the foreground because of the way CedarProcess works. CaptureAction can drop to the background.
};
};
CaptureAction: CedarProcess.ForkableProc = {
PROC [data: REF] RETURNS [results: REFNIL];
ggData: GGData ¬ NARROW[data];
IF GGSliceOps.debugHalt THEN SIGNAL DebugHalt;
CedarProcess.SetPriority[priority: background];
Process.Yield[];
Capture[ggData, ggData.history.advanceEvent, FALSE];
};
NewCapture: PUBLIC ENTRY PROC [name: Rope.ROPE, ggData: GGData] = {
synchronize this process with the advance capture procces via a JOIN. If that process status=done, a new legit capture has completed. If advanceProcess=NIL, need to do a capture from scratch. Advance capture process is ABORTED (KillAdvanceCapture) by DoAdvanceCapture or NewCurrent before a new advance capture is started.
ENABLE UNWIND => NULL;
IF noHistory THEN RETURN ELSE {
count: INT ¬ GGScene.CountSelectedSlices[ggData.scene, first, normal];
nameAndCount: Rope.ROPE ¬ IF count=0 THEN IO.PutFR1["%g no object", [rope[name]]]
ELSE IO.PutFR["%g %g object%g", [rope[name]], [integer[count]], [rope[IF count>1 THEN "s" ELSE NIL]] ];
IF GGSliceOps.debugHalt THEN SIGNAL DebugHalt;
IF ggData.history.advanceProcess#NIL THEN {
a legitimate advance may be available
advanceStatus: CedarProcess.Status ¬
CedarProcess.Join[process: ggData.history.advanceProcess, wait: TRUE].status;
SELECT advanceStatus FROM
done => {
Whoppee!! Advance worked
ggData.history.advanceEvent.name ¬ nameAndCount;
SetCurrent[ggData, ggData.history.advanceEvent]; -- advanceEvent already has capture subevent on it. PushCurrent will eventually push it onto history list.
};
aborted => {
Bah!! Advance failed. This is a rare case not covered by KillAdvanceCapture
currentEvent: HistoryEvent ← NewCurrent[name, ggData];
Capture[ggData, currentEvent, FALSE];
SetCurrent[ggData, Create[nameAndCount]];
Capture[ggData, GetCurrent[ggData].event, FALSE];
};
ENDCASE => ERROR;
ggData.history.advanceProcess ¬ NIL;
ggData.history.advanceEvent ¬ NIL;
}
ELSE { -- no advance
currentEvent: HistoryEvent ← NewCurrent[name, ggData];
Capture[ggData, currentEvent, FALSE];
SetCurrent[ggData, Create[nameAndCount]];
Capture[ggData, GetCurrent[ggData].event, FALSE];
};
};
};
Capture: PROC [ggData: GGData, currentEvent: HistoryEvent, copyAll: BOOL ¬ FALSE] = {
adds a subevent which captures the scene list, making copies of objects that are likely to change (selected objects). Actions performed during the current operation should not add more subevents to this event.
IF currentEvent#NIL THEN {
captureRef: REF Change.capture;
virginList: LIST OF Slice;
virginMap: RefTab.Ref;
[virginList, virginMap] ¬ GGUtility.CopySceneClean[ggData.scene, copyAll];
captureRef ¬ NEW[Change.capture ¬ [capture[ggData.scene, virginList, virginMap] ] ];
Note[currentEvent, Uncapture, captureRef];
};
};
Uncapture: PROC [historyData: REF Change, currentEvent: HistoryEvent] = {
GGHistoryTypes.HistoryProc
This proc is called by the Undo mechanism. Caller should have done a global capture to cover this undo/uncapture operation, making undo of Uncapture possible.
IF noHistory THEN RETURN ELSE {
captureData: REF Change.capture;
captureData ¬ NARROW[historyData];
GGUtility.RestoreSceneClean[captureData.scene, captureData.virginData, captureData.virginMap];
};
};
Create: PUBLIC PROC [name: Rope.ROPE] RETURNS [HistoryEvent] = {
RETURN [NEW[HistoryEventObj ← [name, -1, LIST[NIL]] ]];
RETURN [IF noHistory THEN NIL ELSE NEW[HistoryEventObj ¬ [name, -1, LIST[NIL]] ]];
};
SetCurrent: PUBLIC PROC [ggData: GGData, event: HistoryEvent] = { -- sets current event to event
IF GGSliceOps.debugHalt THEN SIGNAL DebugHalt;
ggData.history.currentEvent ¬ event;
};
GetCurrent: PUBLIC PROC [ggData: GGData] RETURNS [event: HistoryEvent, index: INT] = {
RETURN[ggData.history.currentEvent, ggData.history.currentIndex];
};
DescendASlice: PROC [slice: Slice, walkProc: SliceWalkProc] = {
[] ¬ walkProc[slice];
[] ¬ GGParent.WalkChildren[slice, all, walkProc];
};
WalkSlicesInEvent: PROC [event: HistoryEvent, walkProc: SliceWalkProc] = {
call the walkProc for every Slice found in the event, including shadow slices and their original storage
IF event#NIL THEN FOR subevents: LIST OF SubEvent ¬ event.subevents, subevents.rest UNTIL subevents=NIL DO
IF subevents.first#NIL THEN {
subevent: SubEvent ¬ subevents.first;
historyData: REF Change ¬ subevent.historyData;
WITH historyData SELECT FROM
captureRef: REF Change.capture => {
FOR capturedSlices: LIST OF Slice ¬ captureRef.virginData, capturedSlices.rest UNTIL capturedSlices=NIL DO
found: BOOL ¬ FALSE;
nextVal: REF;
nextSlice: Slice ¬ capturedSlices.first;
[found, nextVal] ¬ RefTab.Fetch[captureRef.virginMap, nextSlice];
IF nextSlice.class#NIL THEN DescendASlice[nextSlice, walkProc];
IF found THEN {
nextOriginal: Slice ¬ NARROW[nextVal];
IF nextOriginal.class#NIL THEN DescendASlice[nextOriginal, walkProc];
};
ENDLOOP;
};
propsRef: REF Change.changingprops => {
IF propsRef.slice.class#NIL THEN DescendASlice[propsRef.slice, walkProc];
};
stateRef: REF Change.changingstate => NULL;
ENDCASE => ERROR;
};
ENDLOOP;
};
SetHistory: PUBLIC PROC [ggData: GGData, list: LIST OF HistoryEvent] = {
IF GGSliceOps.debugHalt THEN SIGNAL DebugHalt;
ggData.history.list ¬ list;
};
GetHistory: PUBLIC PROC [ggData: GGData] RETURNS [LIST OF HistoryEvent] = {
RETURN[ggData.history.list];
};
Push: PUBLIC PROC [event: HistoryEvent, list: LIST OF HistoryEvent] = {
IF GGSliceOps.debugHalt THEN SIGNAL DebugHalt;
IF event#NIL AND event.subevents#NIL THEN {
manipulates some pointers to change list in place
tempEvent: HistoryEvent;
newList: LIST OF HistoryEvent ¬ LIST[event];
splice the new list element between the first and second list elements
temp: LIST OF HistoryEvent ¬ list.rest;
list.rest ¬ newList;
newList.rest ¬ temp;
exchange the contents of the first and (new) second list elements
tempEvent ¬ list.first;
list.first ¬ list.rest.first;
list.rest.first ¬ tempEvent;
};
};
Note: PUBLIC PROC [event: HistoryEvent, historyProc: HistoryProc, historyData: REF Change] = {
IF event = NIL THEN RETURN;
IF GGSliceOps.debugHalt THEN SIGNAL DebugHalt;
Refuse to append any more subevents to a capture type subevent
IF GGSliceOps.noteNoMore AND event.subevents#NIL AND event.subevents.first#NIL AND event.subevents.first.historyData.kind=capture THEN RETURN;
event.subevents ¬ CONS[NEW[SubEventObj ¬
[historyProc, historyData]], event.subevents];
};
Undo: PUBLIC PROC [historyEvent: HistoryEvent, currentEvent: HistoryEvent] = {
calls historyProc[historyRef] for each subevent
in reverse order that subevents originally happened
IF historyEvent=NIL THEN RETURN;
IF GGSliceOps.debugHalt THEN SIGNAL DebugHalt;
FOR nextSubs: LIST OF SubEvent ¬ historyEvent.subevents, nextSubs.rest UNTIL nextSubs=NIL DO
sub: SubEvent ¬ nextSubs.first;
IF sub#NIL THEN sub.historyProc[sub.historyData, currentEvent];
ENDLOOP;
};
ResetHistory: PUBLIC ENTRY PROC [ggData: GGData] = {
ENABLE UNWIND => NULL;
sliceList, ptr: LIST OF Slice;
[sliceList, ptr] ¬ GGUtility.StartSliceList[];
KillAdvanceCaptureInternal[ggData];
FOR historyEventList: LIST OF HistoryEvent ¬ ggData.history.list, historyEventList.rest UNTIL historyEventList=NIL DO
ResetEvent[historyEventList.first, sliceList, ptr];
ENDLOOP;
UnlinkListedSlices[sliceList]; -- do your dirty work
ggData.history.list ¬ LIST[NIL];
ggData.history.currentIndex ¬ 0;
ggData.history.currentEvent ¬ NIL;
GGHistory.ClearTool[ggData];
};
ResetEvent: PROC [event: HistoryEvent, sliceList, ptr: LIST OF Slice] = {
ResetEvent appends unlinkable slices to sliceList. It does not do its own unlinking. Callers should gather up all the unlinkable slices for all resettable events by iterative calls to ResetEvent, then walk the list, WHICH WILL CONTAIN DUPLICATE SLICES, and unlink each slice INDIVIDUALLY using GGSlice.UnlinkSlice and NOT GGSliceOps.Unlink. This is necessary since some slices may appear multiple times in a history, having been first a child slice and then subsequently made top level or reparented numerous times.
IF event#NIL THEN {
This proc pays NO ATTENTION to whether slices in the event are in the scene, but only to the value of slice.historyTop. Caller must protect slices in scene if the scene is not to be trashed.
AddSliceToUnlink: PROC [slice: Slice] RETURNS [done: BOOL ¬ FALSE] = {
IF slice.class#NIL AND slice.historyTop<=index THEN [sliceList, ptr] ¬ GGUtility.AddSlice[slice, sliceList, ptr];
};
index: INT ¬ event.index;
WalkSlicesInEvent[event, AddSliceToUnlink];
IF doUnlink THEN FOR nextVictims: LIST OF Slice ← victims, nextVictims.rest UNTIL nextVictims=NIL DO GGSlice.UnlinkSlice[nextVictims.first] ENDLOOP;
Take apart the subevent list and the event reference to the subevent.
FOR subeventList: LIST OF SubEvent ¬ event.subevents, subeventList.rest UNTIL subeventList=NIL DO
IF subeventList.first#NIL THEN subeventList.first.historyData ¬ NIL;
ENDLOOP;
event.subevents ¬ NIL;
};
};
UnlinkListedSlices: PROC [sliceList: LIST OF Slice] = {
IF doUnlink THEN FOR slices: LIST OF Slice ¬ sliceList, slices.rest UNTIL slices=NIL DO
GGSlice.UnlinkSlice[slices.first]; -- GGSlice.UnlinkSlice deals with already unlinked slices
ENDLOOP;
};
Reset: PUBLIC PROC [event: HistoryEvent] = {
IF event#NIL THEN {
event.subevents ¬ NIL;
event.name ¬ NIL;
};
};
Empty: PUBLIC PROC [event: HistoryEvent] RETURNS [BOOL] = { RETURN [event=NIL OR event.subevents=NIL] };
UndoN: PUBLIC PROC [ggData: GGData, N: INT ¬ 0] = {
IF noHistory THEN RETURN ELSE {
placeHolder: Rope.ROPE = "UndoN Placeholder";
title: Rope.ROPE;
actualCount: INT ¬ 0;
currentEvent: HistoryEvent;
list: LIST OF HistoryEvent ¬ GGHistory.GetHistory[ggData];
IF list#NIL AND list.first#NIL THEN { -- list can be LIST[NIL]
don't call NewCapture, because have to capture all independent of selection
currentEvent ¬ GGHistory.NewCurrent[placeHolder, ggData];
Capture[ggData, currentEvent, TRUE]; -- capture the entire scene for Undo of Undo
THROUGH [0..N) DO
IF list#NIL AND list.first#NIL THEN {
GGHistory.Undo[historyEvent: list.first, currentEvent: NIL];
list ¬ list.rest;
actualCount ¬ actualCount+1;
};
ENDLOOP;
title ¬ GGHistory.GetCurrent[ggData].event.name ¬ IO.PutFLR["Undo %g event%g. Actually undid %g event%g.", LIST[[integer[N]], [rope[IF N>1 THEN "s" ELSE NIL ]], [integer[actualCount]], [rope[IF actualCount>1 THEN "s" ELSE NIL ]]] ];
GGHistory.PushCurrent[ggData];
The following smashes are done because Undo has made bogus slice descriptors all over the place
GGCaret.SitOn[ggData.caret, NIL]; -- smash the chair
GGCaret.NoAttractor[ggData.caret]; -- smash the attractor
GGState.SetSliceToExtend[ggData, NIL]; -- smash the extension
GGState.SetExtendMode[ggData, none]; -- smash the extension slice
GGSelect.DeselectAllAllClasses[ggData.scene]; -- smash existing select lists
BEGIN
Reselect: PROC [slice: Slice] RETURNS [done: BOOL ¬ FALSE] = {
GGSelect.ReselectSliceAllClasses[slice: slice, scene: ggData.scene]
};
IF doSelections THEN [] ¬ GGScene.WalkSlices[ggData.scene, first, Reselect];
END;
Feedback.Append[ggData.router, oneLiner, $Feedback, title];
GGWindow.RestoreScreenAndInvariants[paintAction: $PaintEntireScene, ggData: ggData, remake: triggerBag, edited: TRUE, okToSkipCapture: FALSE];
}
ELSE Feedback.Append[ggData.router, oneLiner, $Complaint, "No entries on the history list to Undo"];
};
};
Following taken from GGHistoryToolImpl.mesa on September 1, 1988
BadNumber: SIGNAL = CODE;
CapTool: PUBLIC PROC [caption: Rope.ROPE, ggData: GGData] = {
tool: HistoryTool ¬ ggData.history.tool;
IF tool#NIL THEN {
container: Containers.Container ¬ tool.layout.container;
container.name ¬ Rope.Concat["GGHistory for ", caption];
ViewerOps.PaintViewer[viewer: container, hint: caption];
};
};
BuildTool: PUBLIC PROC [caption: Rope.ROPE, ggData: GGData] RETURNS [HistoryTool]= {
... builds a GG history tool.
openHeight: INTEGER ¬ 120;
viewer: ViewerClasses.Viewer;
tool: HistoryTool;
layout: Layout;
name: Rope.ROPE ¬ Rope.Concat["GGHistory for ", caption];
viewer ¬ IF ggData.history.tool=NIL THEN NIL ELSE ggData.history.tool.layout.container;
IF viewer#NIL AND NOT (viewer.destroyed OR viewer.paintingWedged) THEN { -- already have this tool around
IF viewer.iconic THEN ViewerOps.OpenIcon[viewer];
Feedback.PutF[ggData.router, oneLiner, $Complaint, "GGHistory tool for %g already exists", [rope[caption]] ];
RETURN [ggData.history.tool];
};
tool ¬ NEW[HistoryToolObj];
layout ¬ tool.layout ¬ NEW[LayoutRec];
layout.container ¬ Containers.Create[[name: name, iconic: TRUE, column: right, scrollable: FALSE, data: ggData]];
[] ¬ BuildButton[layout, "Show", SynchShow, ggData];
[] ¬ BuildButton[layout, "Undo", SynchUndo, ggData];
[ ----, tool.eventNumArg] ¬
BuildDataFieldPair[layout, "since Gargoyle event number: ", EventNumberButton, ggData, 1];
layout.heightSoFar ¬ layout.heightSoFar + layout.entryVSpace/2;
[] ¬ BuildButton[layout, "Get", SynchGetSize, ggData];
[] ¬ BuildButton[layout, "Set", SynchSetSize, ggData];
[ ----, tool.sizeArg] ¬
BuildDataFieldPair[layout, "history size: ", HistorySizeButton, ggData, 1];
HRule[layout];
tool.textField ¬ ViewerOps.CreateViewer[flavor: $Text, info: [parent: layout.container,
wx: layout.entryLeft, wy: layout.heightSoFar, wh: 700, ww: 200, border: FALSE, scrollable: TRUE],
paint: FALSE];
-- make the textField grow as the container grows
Containers.ChildYBound[layout.container, tool.textField];
Containers.ChildXBound[layout.container, tool.textField];
ViewerOps.SetOpenHeight[layout.container, openHeight];
ViewerOps.OpenIcon[icon: layout.container, closeOthers: FALSE, bottom: TRUE, paint: TRUE];
RETURN [tool];
};
EventNumberButton: Buttons.ButtonProc = {
ButtonProc: TYPE = PROC [parent: Viewer, clientData: REF ANYNIL,
mouseButton: MouseButton ← red, shift, control: BOOLFALSE];
ggData: GGData ¬ NARROW[clientData];
tool: HistoryTool ¬ ggData.history.tool;
DataFieldButton[tool.eventNumArg, mouseButton#red];
};
HistorySizeButton: Buttons.ButtonProc = {
ggData: GGData ¬ NARROW[clientData];
tool: HistoryTool ¬ ggData.history.tool;
DataFieldButton[tool.sizeArg, mouseButton#red];
};
SynchShow: Buttons.ButtonProc = {
ggData: GGData ¬ NARROW[clientData];
handle: SlackHandle ¬ ggData.slackHandle;
event: LIST OF REF ¬ LIST[$HistoryShow];
GGEvent.PrintAllInput[ggData, event]; -- KAP
SlackProcess.QueueAction[handle, DoShow, event, ggData, NIL];
};
DoShow: PROC [clientData: REF ANY, inputAction: REF] = {
ggData: GGData ¬ NARROW[clientData];
tool: HistoryTool ¬ ggData.history.tool;
num, stop: INT ¬ 0;
h: IO.STREAM ¬ IO.ROS[];
list: LIST OF HistoryEvent ¬ GGHistory.GetHistory[ggData];
num ¬ GGHistory.GetCurrent[ggData].index; -- current event number
stop ¬ GetInt[tool.eventNumArg ! BadNumber => { stop ¬ 0; CONTINUE }]; -- "since event number" argument. Used for Show and Undo
WHILE num > stop AND list#NIL AND list.first#NIL DO
h.PutF["%g\t%g\n", [integer[num]], [rope[list.first.name]] ];
num ¬ num-1;
list ¬ list.rest;
ENDLOOP;
TEditOps.SetTextContents[tool.textField, IO.RopeFromROS[h]];
};
SynchUndo: Buttons.ButtonProc = {
ggData: GGData ¬ NARROW[clientData];
handle: SlackHandle ¬ ggData.slackHandle;
event: LIST OF REF ¬ LIST[$HistoryUndo];
GGEvent.PrintAllInput[ggData, event]; -- KAP
SlackProcess.QueueAction[handle, DoUndo, event, ggData, NIL];
};
DoUndo: PROC [clientData: REF ANY, inputAction: REF] = {
ggData: GGData ¬ NARROW[clientData];
{
tool: HistoryTool ¬ ggData.history.tool;
num: INT ¬ GGHistory.GetCurrent[ggData].index; -- current event number
stop: INT ¬ GetInt[tool.eventNumArg ! BadNumber => GOTO Bad]; -- since event number" argument. Used for Show and Undo
back: INT ← num-stop;
IF back > 0 THEN GGHistory.UndoN[ggData, back];
EXITS
Bad => Feedback.Append[ggData.router, oneLiner, $Complaint, "Please fill in the \"since Gargoyle event number\" field with a positive integer event number"];
};
};
SynchGetSize: Buttons.ButtonProc = {
ggData: GGData ¬ NARROW[clientData];
handle: SlackHandle ¬ ggData.slackHandle;
event: LIST OF REF ¬ LIST[$HistoryGetSize];
GGEvent.PrintAllInput[ggData, event]; -- KAP
SlackProcess.QueueAction[handle, DoGetSize, event, ggData, NIL];
};
DoGetSize: PROC [clientData: REF ANY, inputAction: REF] = {
ggData: GGData ¬ NARROW[clientData];
tool: HistoryTool ¬ ggData.history.tool;
SetInt[tool.sizeArg, ggData.history.maxSize];
};
SynchSetSize: Buttons.ButtonProc = {
ggData: GGData ¬ NARROW[clientData];
handle: SlackHandle ¬ ggData.slackHandle;
event: LIST OF REF ¬ LIST[$HistorySetSize];
GGEvent.PrintAllInput[ggData, event]; -- KAP
SlackProcess.QueueAction[handle, DoSetSize, event, ggData, NIL];
};
DoSetSize: PROC [clientData: REF ANY, inputAction: REF] = {
ggData: GGData ¬ NARROW[clientData];
{
tool: HistoryTool ¬ ggData.history.tool;
num: INT ¬ GetInt[tool.sizeArg ! BadNumber => GOTO Bad];
ggData.history.maxSize ¬ IF num<=0 THEN 1 ELSE num;
SetInt[tool.sizeArg, ggData.history.maxSize];
new maxSize takes effect on next operation
EXITS
Bad => Feedback.Append[ggData.router, oneLiner, $Complaint, "Please fill in the \"history size\" field with a positive integer size"];
};
};
ClearTool: PUBLIC PROC [ggData: GGData] = {
Clear the history tool viewers
tool: HistoryTool ¬ ggData.history.tool;
IF tool#NIL THEN {
TEditOps.SetTextContents[tool.textField, NIL];
TEditOps.SetTextContents[tool.eventNumArg, NIL];
};
};
Following excerpted from EditToolBuilderImpl.mesa on August 31, 1988
BuildButton: PROC [info: Layout, name: Rope.ROPE, proc: Buttons.ButtonProc, clientData: REF ANY ¬ NIL, fork: BOOL ¬ FALSE, gapAfter: BOOL ¬ TRUE, border: BOOL ¬ FALSE] RETURNS [button: Buttons.Button] = {
button ¬ MBQueue.CreateButton[q: queue,
info: [name: name, parent: info.container, wx: info.entryLeft, wy: info.heightSoFar, border: border],
proc: proc, clientData: clientData, paint: FALSE];
info.entryLeft ¬ info.entryLeft + button.ww;
IF gapAfter THEN info.entryLeft ¬ info.entryLeft + info.gapSize;
RETURN[ button ];
};
DataFieldButton: PROC [arg: ViewerClasses.Viewer, clear: BOOL] = {
IF clear THEN ViewerTools.SetContents[arg, NIL]; -- clear contents of field
ViewerTools.SetSelection[arg, NIL]; -- make pending delete selection of field contents
};
BuildDataFieldPair: PROC [info: Layout, buttonRope: Rope.ROPE, buttonProc: Buttons.ButtonProc, clientData: REF ANY ¬ NIL, lines: CARDINAL ¬ 2] RETURNS [button: Buttons.Button, arg: ViewerClasses.Viewer] = {
fudge: CARDINAL = 1;
button ¬ BuildButton[info: info, name: buttonRope, proc: buttonProc, clientData: clientData,
fork: FALSE, gapAfter: FALSE];
arg ¬ ViewerOps.CreateViewer[flavor: $Text, info: [parent: info.container,
wx: info.entryLeft, wy: info.heightSoFar+fudge,
ww: ViewerSpecs.openRightWidth-info.entryLeft-5,
wh: info.entryHeight*lines,
border: FALSE, scrollable: FALSE], paint: FALSE];
info.heightSoFar ¬ info.heightSoFar + info.entryHeight*lines;
info.entryLeft ¬ info.initLeft;
Containers.ChildXBound[info.container, arg];
};
GetInt: PROC [arg: ViewerClasses.Viewer] RETURNS [num: INT] = {
rope: Rope.ROPE ~ TEditOps.GetTextContents[arg];
h: IO.STREAM ~ IO.RIS[rope];
num ¬ IO.GetInt[h ! IO.Error, IO.EndOfStream => GOTO BadNum];
EXITS BadNum => SIGNAL BadNumber;
};
SetInt: PROC [arg: ViewerClasses.Viewer, num: INT] = {
h: IO.STREAM ~ IO.ROS[];
IO.Put1[h, IO.int[num]];
TEditOps.SetTextContents[arg, IO.RopeFromROS[h]];
};
HRule: PROC [info: Layout, thickness: CARDINAL ¬ 1, gapAbove, gapBelow: BOOL ¬ TRUE] = {
rule: Rules.Rule;
IF gapAbove THEN info.heightSoFar ¬ info.heightSoFar + info.entryVSpace*2;
rule ¬ Rules.Create[info: [parent: info.container, wx: 0, wy: info.heightSoFar, ww: ViewerSpecs.openRightWidth, wh: thickness], paint: FALSE];
Containers.ChildXBound[container: info.container, child: rule];
info.heightSoFar ¬ info.heightSoFar + thickness;
IF gapBelow THEN info.heightSoFar ¬ info.heightSoFar + info.entryVSpace*2;
};
queue: MBQueue.Queue ¬ MBQueue.Create[]; -- single queue, just for synchronization
END.