<<-- TEditSelectionImpl.mesa Edited by Paxton on May 23, 1983 12:03 pm>> <> <> DIRECTORY Carets USING [StartCaret, StopCaret], Graphics USING [black, Context, DrawBox, SetColor, SetPaintMode, SetStipple], EditSpan USING [CompareNodeOrder, NodeOrder], InputFocus USING [Focus, GetInputFocus, SetInputFocus], Process USING [Detach], TextEdit USING [FetchChar, Offset, Size], TextLooks USING [Looks, noLooks], TextNode USING [FirstChild, ForwardClipped, LastWithin, Level, Location, NarrowToTextNode, Offset, Parent, Ref, RefTextNode, Root, Span, StepBackward, StepForward], TEditDocument USING [BeforeAfter, LineTable, maxClip, PunctuationPosition, SelectionGrain, SelectionId, SelectionPoint, Selection, TEditDocumentData], TEditFormat USING [LineInfo], TEditSelection USING [Alloc, Copy, Create, Free, GetCachedLineInfo, IsDown, ForceDown, LockBothSelections, LockSel, fSel, nilSel, pSel, sSel, UnlockBothSelections, UnlockSel], TEditTouchup USING [LockAfterRefresh, UnlockAfterRefresh], ViewerOps USING [AddProp, FetchProp, PaintViewer], ViewerClasses USING [ModifyProc, Viewer]; TEditSelectionImpl: CEDAR PROGRAM IMPORTS Carets, EditSpan, Graphics, InputFocus, Process, TextEdit, TextNode, TEditSelection, TEditTouchup, ViewerOps EXPORTS TEditSelection = BEGIN OPEN TEditDocument, TEditSelection, TEditTouchup; ------ Selection Display and Control ------ MakePointSelection: PUBLIC PROC [selection: Selection, pos: TextNode.Location] = BEGIN <<-- make a point selection at pos out the the current primary selection>> tSel: Selection _ Alloc[]; Copy[source: selection, dest: tSel]; tSel.start.pos _ tSel.end.pos _ pos; tSel.granularity _ point; tSel.pendingDelete _ FALSE; tSel.insertion _ before; MakeSelection[selection: primary, new: tSel]; Free[tSel]; END; ChangeSelections: PROC [proc: PROC [tSel: Selection], sel: Selection] = { tSel: Selection _ Alloc[]; LockBothSelections["ChangeBothSelections"]; { ENABLE UNWIND => UnlockBothSelections[]; Copy[source: sel, dest: tSel]; proc[tSel] }; UnlockBothSelections[]; Free[tSel] }; PushOrExchangeSelections: PUBLIC PROC = { DoPush: PROC [tSel: Selection] = { MakeSelection[IF sSel.viewer=NIL THEN NIL ELSE sSel, primary]; MakeSelection[IF tSel.viewer=NIL THEN NIL ELSE tSel, secondary] }; ChangeSelections[DoPush, pSel] }; MakePrimary: PUBLIC PROC = { -- make secondary selection be the primary DoMakePrimary: PROC [tSel: Selection] = { Deselect[secondary]; MakeSelection[tSel, primary] }; ChangeSelections[DoMakePrimary, sSel] }; MakeSecondary: PUBLIC PROC = { -- make secondary selection be the primary DoMakeSecondary: PROC [tSel: Selection] = { Deselect[primary]; MakeSelection[tSel, secondary] }; ChangeSelections[DoMakeSecondary, pSel] }; CancelPrimary: PUBLIC PROC = { MakeSelection[NIL, primary] }; CancelSecondary: PUBLIC PROC = { MakeSelection[NIL, secondary] }; CancelFeedback: PUBLIC PROC = { MakeSelection[NIL, feedback] }; FakeSecondary: PUBLIC PROC [sel: Selection] = { LockSel[secondary, "FakeSecondary"]; { ENABLE UNWIND => UnlockSel[secondary]; IF sSel.viewer # NIL THEN Deselect[secondary]; Copy[source: sel, dest: sSel] }; UnlockSel[secondary] }; Deselect: PUBLIC PROC [selection: SelectionId _ primary] = { <> LockSel[selection, "Deselect"]; { ENABLE UNWIND => UnlockSel[selection]; sel: Selection = SELECT selection FROM primary => pSel, secondary => sSel, feedback => fSel, ENDCASE => ERROR; viewer: ViewerClasses.Viewer = sel.viewer; op: ATOM = SELECT selection FROM primary => $TakeDownPSel, secondary => $TakeDownSSel, feedback => $TakeDownFSel, ENDCASE => ERROR; down: BOOL = IsDown[selection]; Copy[source: nilSel, dest: sel]; IF viewer#NIL AND ~down THEN { -- take the selection down from the screen ViewerOps.PaintViewer[viewer, client, FALSE, op]; IF ~IsDown[selection] THEN ForceDown[selection]; <> }}; UnlockSel[selection] }; MakeSelection: PUBLIC PROC [new: Selection _ NIL, selection: SelectionId _ primary, startValid, endValid: BOOLEAN _ FALSE, forkPaint: BOOL _ TRUE] = { <> sel: Selection; op: ATOM; LockSel[selection, "MakeSelection"]; { ENABLE UNWIND => UnlockSel[selection]; SELECT selection FROM primary => BEGIN if: ViewerClasses.Viewer = InputFocus.GetInputFocus[].owner; sel _ pSel; op _ $ShowPSel; IF new=NIL THEN {IF if#NIL THEN InputFocus.SetInputFocus[NIL]} ELSE IF if#new.viewer THEN InputFocus.SetInputFocus[new.viewer]; END; feedback => { sel _ fSel; op _ $ShowFSel }; secondary => { sel _ sSel; op _ $ShowSSel }; ENDCASE => ERROR; IF new=NIL OR new.viewer # sel.viewer THEN Deselect[selection]; IF new#NIL THEN { IF selection#feedback AND fSel.viewer=new.viewer THEN Deselect[feedback]; <> Copy[source: new, dest: sel]; sel.start.metricsValid _ startValid; sel.end.metricsValid _ endValid; IF sel.granularity=point THEN sel.pendingDelete _ FALSE; IF forkPaint THEN { showSel: REF ShowSelRec = SELECT selection FROM primary => showPSel, secondary => showSSel, ENDCASE => showFSel; IF showSel.process = NIL THEN TRUSTED {Process.Detach[showSel.process _ FORK ShowSel[showSel, op ! ABORTED => CONTINUE]]} } ELSE ViewerOps.PaintViewer[sel.viewer, client, FALSE, op] }}; UnlockSel[selection] }; ShowSelRec: TYPE = RECORD [ process: PROCESS, selection: SelectionId ]; showPSel: REF ShowSelRec _ NEW[ShowSelRec _ [NIL, primary]]; showSSel: REF ShowSelRec _ NEW[ShowSelRec _ [NIL, secondary]]; showFSel: REF ShowSelRec _ NEW[ShowSelRec _ [NIL, feedback]]; ShowSel: PROC [my: REF ShowSelRec, op: ATOM] = { selection: SelectionId _ my.selection; sel: Selection _ SELECT selection FROM primary => pSel, secondary => sSel, feedback => fSel, ENDCASE => ERROR; LockSel[selection, "ShowSel"]; { ENABLE UNWIND => UnlockSel[selection]; viewer: ViewerClasses.Viewer _ sel.viewer; IF viewer#NIL THEN { tdd: TEditDocumentData _ NARROW[viewer.data]; IF tdd # NIL AND LockAfterRefresh[tdd, "ShowSel"] THEN { ViewerOps.PaintViewer[viewer, client, FALSE, op]; UnlockAfterRefresh[tdd] }}; my.process _ NIL }; UnlockSel[selection] }; lightGrey: CARDINAL = 0208H; darkGrey: CARDINAL = 0A5A5H; veryDarkGrey: CARDINAL _ 0EBEBH; SelColor: TYPE = {black, lightGrey, darkGrey, veryDarkGrey} _ black; SelBound: TYPE = {solid, line} _ solid; feedbackLineWidth: CARDINAL _ 4; feedbackLineRaise: INTEGER _ 1; MarkSelection: PUBLIC PROC [dc: Graphics.Context, viewer: ViewerClasses.Viewer, selection: Selection, id: SelectionId] = { WITH viewer.data SELECT FROM tdd: TEditDocumentData=> { OPEN selection; lines: TEditDocument.LineTable = tdd.lineTable; vHeight: INTEGER = viewer.ch; selBound: SelBound = IF selection.pendingDelete AND id#feedback THEN solid ELSE line; selColor: SelColor = IF id=primary THEN black ELSE IF id=feedback THEN veryDarkGrey ELSE IF selection.pendingDelete THEN lightGrey ELSE darkGrey; EffectSelect: PROC [x, y, w, h: INTEGER] = BEGIN OPEN Graphics; lineHeight: CARDINAL _ 2; bottom: INTEGER _ vHeight-y-h; IF id=feedback THEN { lineHeight _ feedbackLineWidth; bottom _ bottom - feedbackLineRaise }; SELECT selColor FROM black => SetColor[dc, black]; darkGrey => SetStipple[dc, darkGrey]; lightGrey => SetStipple[dc, lightGrey]; veryDarkGrey=> SetStipple[dc, veryDarkGrey]; ENDCASE => ERROR; SELECT selBound FROM solid => DrawBox[dc, [x, bottom, x+w, vHeight-y]]; ENDCASE => DrawBox[dc, [x, bottom, x+w, bottom+lineHeight]]; END; IF end.line<0 OR start.line=LAST[INTEGER] OR (start.line=end.line AND end.clipped) THEN RETURN; -- not visible [] _ Graphics.SetPaintMode[dc, invert]; IF start.line = end.line OR (start.line < 0 AND end.line = 0) THEN { -- one liner x, y: INTEGER; IF start.line < 0 OR start.clipped THEN { -- select from beginning of line line: INTEGER _ MAX[0,start.line]; x _ lines[line].xOffset; y _ lines[line].yOffset-lines[line].ascent } ELSE { x _ start.x; y _ start.y }; IF end.clipped THEN ERROR; -- previous tests imply ~end.clipped EffectSelect[x, y, end.x-x+end.w, end.h] } ELSE IF start.line=lines.lastLine AND end.line>lines.lastLine THEN -- one liner EffectSelect[start.x, start.y, lines[start.line].width + lines[start.line].xOffset - start.x, start.h] ELSE BEGIN sl: INTEGER = NormalizeLineIndex[lines, start.line]; el: INTEGER = NormalizeLineIndex[lines, end.line]; EffectSelAll: PROC [n: INTEGER] = INLINE { EffectSelect[lines[n].xOffset, lines[n].yOffset-lines[n].ascent, lines[n].width, lines[n].ascent+lines[n].descent] }; IF sl=start.line AND ~start.clipped THEN -- select end portion of sl EffectSelect[start.x, start.y, (lines[sl].width + lines[sl].xOffset - start.x), start.h] ELSE EffectSelAll[sl];-- select all of sl FOR n: INTEGER IN (sl..el) DO -- select all of the intermediate lines EffectSelAll[n]; ENDLOOP; IF end.clipped THEN NULL -- end.line is not actually part of the selection ELSE IF el=end.line THEN -- end.line is on the screen, so select initial part of el EffectSelect[lines[el].xOffset, end.y, end.x - lines[el].xOffset + end.w, end.h] ELSE EffectSelAll[el];-- end.line is off screen, so select all of el END; [] _ Graphics.SetPaintMode[dc, opaque]; }; ENDCASE; }; NormalizeLineIndex: PROC [lines: TEditDocument.LineTable, line: INTEGER] RETURNS [INTEGER] = INLINE { RETURN [MAX[0,MIN[lines.lastLine,line]]] }; ExtendSelection: PUBLIC PROC [dc: Graphics.Context, viewer: ViewerClasses.Viewer, old, new: Selection, id: SelectionId, updateEnd: BOOLEAN] = BEGIN <<-- when done, old is valid (it's = pSel, e.g.) and new^ = old^>> TooHard: PROC = BEGIN MarkSelection[dc, viewer, old, id]; -- take it down FixupSelection[new, viewer]; -- get position info about the new selection MarkSelection[dc, viewer, new, id]; -- put it up Copy[source: new, dest: old]; END; IF updateEnd THEN BEGIN -- update right end BumpLoc: PROC [loc: TextNode.Location] RETURNS [TextNode.Location] = { n: TextNode.RefTextNode = TextNode.NarrowToTextNode[loc.node]; where: TextNode.Offset _ loc.where+1; IF where >= TextEdit.Size[n] THEN RETURN [[TextNode.StepForward[n],0]]; RETURN [[n,where]]; }; tooHard: BOOLEAN = old.end.line NOT IN [0..LAST[INTEGER]); sameNode: BOOLEAN = (new.end.pos.node = old.end.pos.node); IF tooHard THEN {TooHard; RETURN}; IF new.end.pos=old.end.pos THEN NULL ELSE IF (sameNode AND new.end.pos.where>old.end.pos.where) OR (~sameNode AND EditSpan.CompareNodeOrder[new.end.pos.node, old.end.pos.node]=after) THEN BEGIN -- new end after old end new.start _ old.end; new.start.pos _ BumpLoc[new.start.pos]; IF sameNode AND new.start.pos.node # new.end.pos.node THEN new.start.pos _ new.end.pos; <> FixupSelection[new, viewer]; MarkSelection[dc, viewer, new, id]; old.end _ new.end; END ELSE BEGIN -- new end before old end save: TextNode.Location = new.end.pos; new.start _ new.end; new.start.pos _ BumpLoc[save]; IF sameNode AND new.start.pos.node # new.end.pos.node THEN new.start.pos _ old.end.pos; <> new.end _ old.end; FixupSelection[new, viewer, TRUE, FALSE]; -- fix start MarkSelection[dc, viewer, new, id]; old.end _ new.start; old.end.pos _ save; FixupSelection[old, viewer, FALSE, TRUE]; END; END ELSE BEGIN -- update start DecLoc: PROC [loc: TextNode.Location] RETURNS [TextNode.Location] = { n: TextNode.RefTextNode; IF loc.where > 0 THEN RETURN [[loc.node, loc.where-1]]; n _ TextNode.NarrowToTextNode[TextNode.StepBackward[loc.node]]; RETURN [[n,MAX[TextEdit.Size[n],1]-1]] }; tooHard: BOOLEAN = old.start.line NOT IN [0..LAST[INTEGER]); sameNode: BOOLEAN = (new.start.pos.node = old.start.pos.node); IF tooHard THEN {TooHard; RETURN}; IF new.start.pos=old.start.pos THEN NULL ELSE IF (sameNode AND new.start.pos.where> <<-- if pos was filtered, return line where it would have been and set clipped=TRUE>> foundPos: BOOLEAN _ FALSE; tdd: TEditDocumentData = NARROW[viewer.data]; lines: TEditDocument.LineTable; lineInfo: TEditFormat.LineInfo; nChars: INTEGER; IsPosPastLine: PROC [pos: TextNode.Location, line: INTEGER] RETURNS [BOOLEAN] = { next: TextNode.Offset; next _ lines[line].pos.where+lines[line].nChars; IF pos.where >= next AND lines[line].end # eon THEN RETURN [TRUE]; -- it is past this line RETURN [FALSE] }; FindPos: PROC [pos: TextNode.Location] RETURNS [found: BOOLEAN, line: INTEGER] = { found _ FALSE; FOR n: INTEGER IN [firstLine..lines.lastLine] DO IF lines[n].pos.node=pos.node THEN { IF pos.where < lines[n].pos.where THEN EXIT; -- have gone past it found _ TRUE; line _ n } ELSE IF found THEN EXIT; -- have gone past pos.node ENDLOOP}; IF tdd = NIL THEN RETURN; lines _ tdd.lineTable; sp.pos _ pos; IF (lines[0].pos.node=pos.node AND lines[0].pos.where>pos.where) THEN BEGIN sp.y _ sp.line _ -LAST[INTEGER]; -- before beginning of text RETURN; END; IF lines[lines.lastLine].pos.node=pos.node AND IsPosPastLine[pos,lines.lastLine] THEN BEGIN sp.y _ sp.line _ LAST[INTEGER]; -- after end of text RETURN; END; firstLine _ MIN[lines.lastLine,MAX[firstLine,0]]; [foundPos, sp.line] _ FindPos[pos]; sp.clipped _ FALSE; IF ~foundPos THEN SELECT EditSpan.CompareNodeOrder[pos.node, lines[0].pos.node] FROM same => ERROR; disjoint => ERROR CannotFindIt; before => { sp.line _ sp.y _ -LAST[INTEGER]; RETURN }; -- before beginning ENDCASE => -- pos.node comes after first line node IF tdd.clipLevel = maxClip AND tdd.commentFilter=includeComments THEN { <<-- nothing special going on, so would have found it if not after end>> sp.line _ sp.y _ LAST[INTEGER]; RETURN } -- after end ELSE { -- must think about this harder SELECT EditSpan.CompareNodeOrder[pos.node, lines[lines.lastLine].pos.node] FROM same => ERROR; disjoint => ERROR CannotFindIt; -- may have been deleted or moved after => { -- it really is after the end sp.line _ sp.y _ LAST[INTEGER]; RETURN } ENDCASE => { -- it got clipped n: TextNode.Ref _ pos.node; delta: INTEGER _ TextNode.Level[n]-tdd.clipLevel; firstLine _ 0; -- need to let FindPos look at all the previous lines FOR i:INTEGER IN [0..delta) DO n _ TextNode.Parent[n]; ENDLOOP; n _ TextNode.ForwardClipped[n,tdd.clipLevel].nx; [foundPos, sp.line] _ FindPos[[n,0]]; IF ~foundPos THEN ERROR CannotFindIt; -- may have been restored by Undo sp.clipped _ TRUE; }; }; IF lineOnly OR sp.clipped THEN RETURN; -- otherwise, compute metrics sp.y _ lines[sp.line].yOffset-lines[sp.line].ascent; sp.h _ lines[sp.line].ascent+lines[sp.line].descent; [lineInfo, nChars] _ GetCachedLineInfo[viewer, tdd, sp.line]; IF lineInfo[0]>=LAST[INTEGER] THEN BEGIN -- empty line sp.x _ lines[sp.line].xOffset; sp.w _ 0; END ELSE IF pos.where>lines[sp.line].pos.where+nChars THEN BEGIN -- past this line IF sp.line=lines.lastLine THEN sp.line _ sp.y _ LAST[INTEGER] ELSE {sp.x _ lines[sp.line].width; sp.w _ 0}; END ELSE BEGIN cx: INTEGER _ lines[sp.line].xOffset; cw: INTEGER _ lineInfo[0]; last: INTEGER = pos.where-lines[sp.line].pos.where; FOR n: INTEGER IN [1..last] DO cx _ cx + cw; IF (cw_lineInfo[n]) = LAST[INTEGER] THEN BEGIN <<-- off end; selection past end of screen>> IF pos.where > lines[sp.line].pos.where+n AND pos.where # TextEdit.Size[TextNode.NarrowToTextNode[pos.node]] THEN sp.line _ LAST[INTEGER]; cw _ 0; EXIT; END; ENDLOOP; sp.x _ cx; sp.w _ cw; END; sp.metricsValid _ TRUE; END; FixupSelection: PUBLIC PROC [selection: Selection, viewer: ViewerClasses.Viewer, start, end: BOOLEAN _ TRUE] = BEGIN <<-- recompute the xywh fields in the selection>> <<-- fixes selection screen info assuming start.pos and end.pos are correct>> IF start THEN selection.start _ ComputePosPoint[viewer, selection.start.pos]; IF end THEN BEGIN IF selection.start.pos=selection.end.pos THEN selection.end _ selection.start -- no need to re-compute ELSE IF selection.start.line=LAST[INTEGER] THEN -- scrolled off bottom {selection.end.line _ LAST[INTEGER]; selection.end.metricsValid _ TRUE} ELSE { firstLine: INTEGER = IF selection.start.line<=0 THEN 0 ELSE IF selection.start.clipped THEN selection.start.line-1 ELSE selection.start.line; selection.end _ ComputePosPoint[viewer, selection.end.pos, firstLine] }; END; END; FixupCaret: PUBLIC PROC [selection: Selection] = BEGIN OPEN selection; <<-- recompute the xy fields in the caret>> lines: TEditDocument.LineTable = data.lineTable; node: TextNode.RefTextNode _ TextNode.NarrowToTextNode[end.pos.node]; size: TextNode.Offset _ TextEdit.Size[node]; PutCaretOnNextLine: PROC = BEGIN SELECT end.line FROM IN [0..lines.lastLine) => BEGIN nextLine: INTEGER _ end.line+1; caretX _ lines[nextLine].xOffset; caretY _ viewer.ch - lines[nextLine].yOffset - lines[nextLine].descent; END; = lines.lastLine => BEGIN caretX _ lines[end.line].xOffset; caretY _ viewer.ch - end.y - end.h - end.h; END; ENDCASE => ERROR; END; IF insertion=before THEN BEGIN IF start.clipped THEN caretY _ LAST[INTEGER] -- caret not visible --ELSE IF start.line=lines.lastLine AND granularity=point AND --start.pos.where=size AND size>0 AND TextEdit.FetchChar[node, size-1]=15C --THEN PutCaretOnNextLine ELSE BEGIN caretX _ start.x; caretY _ IF start.line NOT IN [0..lines.lastLine] THEN LAST[INTEGER] ELSE viewer.ch - start.y - start.h; END; END ELSE BEGIN IF end.clipped THEN caretY _ LAST[INTEGER] -- caret not visible ELSE IF end.pos.where IN [0..size) AND end.line IN [0..lines.lastLine] AND TextEdit.FetchChar[node,end.pos.where]=15C THEN PutCaretOnNextLine ELSE BEGIN caretX _ end.x+end.w; caretY _ IF end.line NOT IN [0..lines.lastLine] THEN LAST[INTEGER] ELSE viewer.ch - end.y - end.h; END END; END; KillSelection: PUBLIC PROCEDURE = BEGIN <<-- selection actually get taken down in our InputModify Proc>> IF InputFocus.GetInputFocus[]#NIL THEN InputFocus.SetInputFocus[]; END; ModifyPSel: PROC [proc: PROC [root: TextNode.Ref, tSel: Selection]] = { tSel: Selection; { ENABLE UNWIND => { UnlockSel[primary]; IF tSel # NIL THEN Free[tSel] }; root: TextNode.Ref; LockSel[primary, "CallWithSelLock"]; IF (root _ SelectionRoot[pSel])=NIL THEN { UnlockSel[primary]; RETURN }; tSel _ Alloc[]; TEditSelection.Copy[source: pSel, dest: tSel]; proc[root, tSel]; MakeSelection[new: tSel] }; Free[tSel]; tSel _ NIL; UnlockSel[primary] }; SelectEverything: PUBLIC PROC = { -- expand to include everything DoSelEverything: PROC [root: TextNode.Ref, tSel: Selection] = { root _ TextNode.Root[tSel.start.pos.node]; tSel.start.pos _ [TextNode.FirstChild[root], 0]; tSel.end.pos.node _ TextNode.LastWithin[root]; tSel.end.pos.where _ TextEdit.Size[TextNode.NarrowToTextNode[tSel.end.pos.node]]-1; tSel.granularity _ branch }; ModifyPSel[DoSelEverything] }; PendingDeleteSelection: PUBLIC PROC = { DoPDel: PROC [root: TextNode.Ref, tSel: Selection] = { tSel.pendingDelete _ TRUE }; ModifyPSel[DoPDel] }; NotPendingDeleteSelection: PUBLIC PROC = { DoNotPDel: PROC [root: TextNode.Ref, tSel: Selection] = { tSel.pendingDelete _ FALSE }; ModifyPSel[DoNotPDel] }; CaretBeforeSelection: PUBLIC PROC = { DoCaretBeforeSelection: PROC [root: TextNode.Ref, tSel: Selection] = { tSel.insertion _ before }; ModifyPSel[DoCaretBeforeSelection] }; CaretAfterSelection: PUBLIC PROC = { DoCaretAfterSelection: PROC [root: TextNode.Ref, tSel: Selection] = { IF tSel.granularity # point THEN tSel.insertion _ after }; ModifyPSel[DoCaretAfterSelection] }; ------ Misc functions ------ SelectionRoot: PUBLIC PROC [s: Selection _ pSel] RETURNS [root: TextNode.Ref] = { tdd: TEditDocument.TEditDocumentData; IF s.viewer=NIL THEN RETURN [TextNode.Root[s.start.pos.node]]; tdd _ NARROW[s.viewer.data]; IF tdd = NIL THEN RETURN [NIL]; RETURN [tdd.text] }; InputModify: PUBLIC ViewerClasses.ModifyProc = BEGIN tdd: TEditDocumentData = NARROW[self.data]; IF tdd = NIL THEN RETURN; SELECT change FROM set, pop => Carets.StartCaret[self, pSel.caretX, pSel.caretY, primary]; kill => BEGIN prop: TEditDocument.Selection _ NARROW[ViewerOps.FetchProp[self, $SelectionHistory]]; Carets.StopCaret[primary]; IF prop=NIL THEN ViewerOps.AddProp[self, $SelectionHistory, prop _ Create[]]; LockSel[primary, "InputModify"]; { ENABLE UNWIND => UnlockSel[primary]; Copy[source: pSel, dest: prop]; Deselect[primary] }; UnlockSel[primary]; END; push => Carets.StopCaret[primary]; ENDCASE => ERROR; END; END.