TEditScrollingImpl.mesa
Copyright © 1985 by Xerox Corporation. All rights reserved.
Michael Plass, April 14, 1986 11:41:20 am PST
Russ Atkinson (RRA) June 25, 1985 11:27:42 am PDT
Doug Wyatt, June 22, 1985 11:28:49 am PDT
DIRECTORY
NodeStyle USING [Ref, GetTopLeadingI, GetTopIndentI, GetLeadingI, GetBottomLeadingI],
NodeStyleOps USING [Alloc, Free, ApplyAll, OfStyle],
RopeReader USING [Ref, SetPosition, Backwards, GetRopeReader, FreeRopeReader],
Scaled USING [FromInt],
TEditDisplay USING [EstablishLine],
TEditDocument USING [LineTable, maxClip, Selection, SelectionId, SelectionPoint, TEditDocumentData],
TEditFormat USING [Allocate, FormatLine, LineInfo, Release],
TEditInput USING [MaxLevelShown],
TEditLocks USING [LockDocAndTdd, UnlockDocAndTdd],
TEditOps USING [RememberCurrentPosition],
TEditProfile USING [scrollBottomOffset, scrollTopOffset],
TEditSelection USING [InsertionPoint, pSel, sSel, fSel],
TEditScrolling USING [],
TEditTouchup USING [LockAfterRefresh, PreScrollDownRec, refresh, UnlockAfterRefresh],
TextEdit USING [FetchChar, Offset, RefTextNode, Size],
TextNode USING [Backward, BackwardClipped, BadArgs, FirstChild, Forward, ForwardClipped, LastLocWithin, Level, Location, LocNumber, LocOffset, LocWithin, Parent, Ref, RefTextNode, Root, StepForward],
ViewerForkers USING [ForkPaint],
ViewerLocks USING [CallUnderWriteLock],
ViewerOps USING [FetchProp, PaintViewer],
ViewerClasses USING [ScrollProc, Viewer];
TEditScrollingImpl: CEDAR PROGRAM
IMPORTS NodeStyle, NodeStyleOps, RopeReader, Scaled, TEditDisplay, TEditFormat, TEditLocks, TEditSelection, TextEdit, TEditInput, TEditOps, TEditProfile, TEditTouchup, TextNode, ViewerForkers, ViewerLocks, ViewerOps
EXPORTS TEditScrolling
= BEGIN
TEditDocumentData: TYPE = TEditDocument.TEditDocumentData;
Viewer: TYPE = ViewerClasses.Viewer;
forkPaints: BOOLFALSE;
RRA: for some reason forking the paints causes the scrollbar feedback to be highly inaccurate when perfoming explict scrolling operations. I strongly suspect that this is because the scrollbar painting is done in th wrong place. But we will not change this soon.
BackUp: PROC [viewer: Viewer, tdd: TEditDocumentData, pos: TextNode.Location, goal: INTEGER] RETURNS [newPos: TextNode.Location, lines, totalLeading, topIndent: INTEGER] = {
pos is the current top line in the viewer; goal is the distance to back up.
algorithm works by incrementally formatting lines prior to pos until it reaches goal height.
The incremental backup procedure preserves the following invariants:
newPos is the current line start
totalLeading is the leading from the baseline of newPos to the baseline of pos
topIndent is the distance from the viewer top to baseline of newPos if newPos goes at top
topLeading is the style value for topLeading corresponding to newPos
kind: NodeStyleOps.OfStyle ~ IF ViewerOps.FetchProp[viewer, $StyleKind] = $Print THEN print ELSE screen;
maxLevel: INTEGER ← tdd.clipLevel;
parent: TextNode.Ref;
level: INTEGER ← 0; -- in case we are doing level clipping
levelClipping: BOOL ← maxLevel < TEditDocument.maxClip;
IncrBackUp: PROC [pos: TextNode.Location, goal, prevTopLeading: INTEGER] RETURNS [prev: TextNode.Location, totalLeading, lines, topIndent, topLeading: INTEGER] = {
tPos: TextNode.Location;
leading, bottomLeading: INTEGER;
lastBreak: TextEdit.Offset;
breakList: LIST OF TextEdit.Offset; -- breaks between first and last
textNode: TextNode.RefTextNode;
where, endOffset, size: TextEdit.Offset;
lineInfo: TEditFormat.LineInfo;
IF pos.where=0
THEN DO
back up to previous text node
tempNode: TextNode.Ref;
IF levelClipping
THEN [tempNode, parent, level] ← TextNode.BackwardClipped[pos.node, maxLevel, parent, level]
ELSE [tempNode, parent, ----] ← TextNode.Backward[pos.node, parent];
IF tempNode=NIL OR parent=NIL THEN RETURN[pos, 0, 0, 0, 0];
IF (textNode ← tempNode)=NIL THEN LOOP;
size ← endOffset ← where ← TextEdit.Size[textNode];
EXIT;
ENDLOOP
ELSE {
back up past last char before pos
textNode ← pos.node;
size ← TextEdit.Size[textNode];
endOffset ← where ← pos.where-1
};
IF where < 4*MAX[12,goal]
THEN where ← 0 -- don't bother to search backwards for CR
ELSE {
stop: TextEdit.Offset ← MAX[0, where-5000]; -- limit reading to 5000 characters
RopeReader.SetPosition[rdr, textNode.rope, where];
UNTIL (where ← where-1)<=stop DO
IF RopeReader.Backwards[rdr]=15C THEN {where ← where+1; EXIT};
ENDLOOP
};
IF styleNode # textNode THEN NodeStyleOps.ApplyAll[style, styleNode ← textNode, kind];
leading ← NodeStyle.GetLeadingI[style];
topLeading ← IF where=0 THEN NodeStyle.GetTopLeadingI[style] ELSE leading;
bottomLeading ← totalLeading ← IF pos.node = textNode
THEN leading
ELSE
MAX[prevTopLeading, NodeStyle.GetBottomLeadingI[style]];
topIndent ← NodeStyle.GetTopIndentI[style]; -- in case this line appears at top of viewer
IF where=size THEN {
no more characters in the node. shows as blank line
lines ← 1; prev ← [textNode, where]; RETURN
};
tPos ← [textNode, where];
lines ← 0;
lineInfo ← TEditFormat.Allocate[];
DO
format lines from tPos to starting pos
lastBreak ← tPos.where;
TEditFormat.FormatLine[lineInfo: lineInfo, node: tPos.node, startOffset: tPos.where, nodeStyle: style, lineWidth: Scaled.FromInt[viewer.cw], kind: kind];
tPos ← lineInfo.nextPos;
IF lines > 0 THEN totalLeading ← totalLeading+leading;
lines ← lines+1;
IF tPos.node#textNode OR tPos.where>=endOffset THEN EXIT;
IF lastBreak # where THEN breakList ← CONS[lastBreak, breakList];
ENDLOOP;
lineInfo.Release[];
lineInfo ← NIL;
When reach here, have found all the line breaks from [textNode, where] to initial pos.
where holds the offset for the first one
lastBreak holds the offset for the last one
breakList holds the offsets for the previous ones
IF totalLeading+topIndent >= goal THEN { -- have enough. find correct line
discardLines: INTEGER ← (totalLeading+topIndent-goal)/leading; -- too many lines
SELECT discardLines FROM
<= 0 => {}; -- don't discard any
>= lines-1 => { -- discard all but one
where ← lastBreak;
lines ← 1;
totalLeading ← bottomLeading ;
};
ENDCASE => {
use breakList to find correct break
count: INTEGER; -- how far to go on list to find the break
lines ← lines-discardLines;
count ← lines-1; -- subtract 1 because lastBreak is not on list
totalLeading ← totalLeading-discardLines*leading;
FOR list: LIST OF TextEdit.Offset ← breakList, list.rest DO
IF (count ← count-1) = 0 THEN { where ← list.first; EXIT };
ENDLOOP;
};
};
prev ← [textNode, where];
};
rdr: RopeReader.Ref ← RopeReader.GetRopeReader[];
style: NodeStyle.Ref ← NodeStyleOps.Alloc[];
styleNode: TextNode.Ref;
remainder: INTEGER ← goal;
dy: INTEGERLAST[INTEGER];
topLeading: INTEGER ← 0;
IF pos.where=0 THEN {
need to get topLeading for pos.node
NodeStyleOps.ApplyAll[style, pos.node, kind];
topLeading ← NodeStyle.GetTopLeadingI[style];
Once implement minGaps between lines, will also need topAscent for this line. Should be able to get that from the line table. (Except if continue to use this for top offset in ScrollToPosition... perhaps can just change that to back up a fixed number of lines.)
};
newPos ← pos;
lines ← totalLeading ← topIndent ← 0;
UNTIL remainder<=0 DO
leading, newLines, newTopIndent: INTEGER;
[newPos, leading, newLines, newTopIndent, topLeading] ←
IncrBackUp[newPos, remainder, topLeading];
IF newLines <= 0 THEN EXIT; -- didn't get anything that time. at start of document.
totalLeading ← totalLeading+leading;
lines ← lines+newLines;
topIndent ← newTopIndent;
IF totalLeading+topIndent >= goal THEN EXIT; -- don't need to back up any farther
remainder ← remainder - leading;
ENDLOOP;
NodeStyleOps.Free[style];
RopeReader.FreeRopeReader[rdr];
};
ScrollTEditDocument: PUBLIC ViewerClasses.ScrollProc = {
[self: Viewer, op: ViewerClasses.ScrollOp, amount: INTEGER, shift: BOOL ← FALSE, control: BOOL ← FALSE] RETURNS [top: INTEGER, bottom: INTEGER]
inner: PROC [tdd: TEditDocumentData] = {
lines: TEditDocument.LineTable ← tdd.lineTable;
topLine: TextNode.Location ← lines[0].pos;
paintOp: REF ANY ← TEditTouchup.refresh;
iconic: BOOL = self.iconic;
levelChange: BOOLFALSE;
IF self.iconic THEN {
SELECT op FROM
up => TEditDisplay.EstablishLine[tdd, TextNode.LastLocWithin[tdd.text], 0];
down => TEditDisplay.EstablishLine[tdd, [TextNode.FirstChild[tdd.text],0], 0];
thumb => NULL;
query => NULL;
ENDCASE => ERROR;
RETURN
};
SELECT op FROM
up => {
line: INTEGER ← 0;
newLevel: INTEGER;
sSel: TEditDocument.Selection ~ TEditSelection.sSel;
doingSecondarySelect: BOOL ~ sSel # NIL AND sSel.viewer # NIL;
IF amount > 0 AND lines.lastLine = 0
THEN {
calculate next line start pos
IF lines[0].end = eon
THEN {
go to the next node, unless already at end of document
next: TextNode.Ref;
maxLevel: INTEGER = tdd.clipLevel;
next ← IF maxLevel < TEditDocument.maxClip THEN
TextNode.ForwardClipped[topLine.node,maxLevel].nx
ELSE TextNode.StepForward[topLine.node];
IF next # NIL THEN topLine ← [next, 0];
}
ELSE topLine.where ← topLine.where+lines[0].nChars
}
ELSE {
find new topLine on screen
UNTIL lines[line].yOffset+lines[line].descent >= amount
OR lines[line+1].pos.node=NIL DO
line ← line+1;
IF line >= lines.lastLine THEN EXIT; -- off end
ENDLOOP;
topLine ← lines[line].pos
};
newLevel ← SELECT TRUE FROM
doingSecondarySelect => tdd.clipLevel,
control AND shift => 1,
first level only
control => TEditDocument.maxClip,
all levels
shift => MIN[TEditDocument.maxClip, TEditInput.MaxLevelShown[tdd]+1],
move levels
ENDCASE => tdd.clipLevel; -- no change
IF newLevel # tdd.clipLevel THEN {
tdd.clipLevel ← newLevel;
levelChange ← TRUE;
};
};
down => {
numLines, totalLeading, topIndent: INTEGER;
[topLine, numLines, totalLeading, topIndent] ←
BackUp[self, tdd, topLine, MAX[amount, lines[0].yOffset]];
IF topLine # lines[0].pos THEN {
make room for the new stuff
op: REF TEditTouchup.PreScrollDownRec ← NEW[TEditTouchup.PreScrollDownRec];
op.lines ← numLines;
op.distance ← topIndent+totalLeading-lines[0].yOffset;
paintOp ← op
};
};
thumb => {
TEditOps.RememberCurrentPosition[self];
IF amount < 5
THEN ScrollToPosition[self, [TextNode.FirstChild[tdd.text],0], FALSE]
ELSE {
totalChars: TextEdit.Offset = TextNode.LocNumber[TextNode.LastLocWithin[tdd.text]]-1;
pos: TextNode.Location ← TextNode.LocWithin[tdd.text, (totalChars*amount)/100];
IF tdd.clipLevel < TEditDocument.maxClip THEN {
check level of target
delta: INTEGER ← TextNode.Level[pos.node]-tdd.clipLevel;
FOR i:INTEGER IN [0..delta) DO -- only do this if pos is too deep
pos ← [TextNode.Parent[pos.node],0];
ENDLOOP;
};
ScrollToPosition[self, pos, FALSE];
};
RETURN;
};
query => {
toTop, toBottom, toEnd, totalChars, t, b: INT;
ll: INTEGER = lines.lastLine;
toTop ← TextNode.LocOffset[[TextNode.FirstChild[tdd.text], 0], topLine
! TextNode.BadArgs => GO TO Bad];
toBottom ← TextNode.LocOffset[topLine, [lines[ll].pos.node, lines[ll].pos.where+lines[ll].nChars]
! TextNode.BadArgs => GO TO Bad];
toEnd ← TextNode.LocOffset[[lines[ll].pos.node, lines[ll].pos.where+lines[ll].nChars-1], TextNode.LastLocWithin[tdd.text]
! TextNode.BadArgs => GO TO Bad];
totalChars ← toTop+toBottom+toEnd;
IF totalChars<=0 THEN {bottom ← 100; RETURN};
make sure there's always something shown for big documents
t ← MIN[98, 100*toTop/totalChars];
b ← MAX[t+2, 100*(toTop+toBottom)/totalChars];
top ← t;
bottom ← b;
EXITS Bad => RETURN;
};
ENDCASE;
IF topLine # lines[0].pos OR levelChange THEN {
TEditDisplay.EstablishLine[tdd, topLine, 0];
DoPaint[self, IF levelChange THEN NIL ELSE paintOp];
};
};
LockAndDoIt[inner, self, NIL, FALSE];
};
ScrollToPosition: PUBLIC PROC [viewer: Viewer, pos: TextNode.Location, offset: BOOL] = {
kind: NodeStyleOps.OfStyle ~ IF ViewerOps.FetchProp[viewer, $StyleKind] = $Print THEN print ELSE screen;
inner: PROC [tdd: TEditDocumentData] = {
node: TextNode.RefTextNode ← pos.node;
where: INTMAX[0, MIN[pos.where, TextEdit.Size[node]-1]];
start: INT ← where;
lines: TEditDocument.LineTable ← tdd.lineTable;
repaint: BOOLFALSE;
topLine: TextNode.Location;
style: NodeStyle.Ref ← NodeStyleOps.Alloc[];
lineInfo: TEditFormat.LineInfo;
backStop: INTMAX[0, where-300]; -- limit looking back too far when searching for CR's
IF tdd.clipLevel < TEditDocument.maxClip AND tdd.clipLevel < TextNode.Level[node] THEN {
repaint ← TRUE;
tdd.clipLevel ← TEditDocument.maxClip; -- turn off level clipping --
};
UNTIL start<=backStop OR TextEdit.FetchChar[node, start-1].char=15C DO
start ← start - 1;
ENDLOOP;
topLine ← [node, start];
NodeStyleOps.ApplyAll[style, topLine.node, kind];
lineInfo ← TEditFormat.Allocate[];
DO
find the line containing [node, where]
TEditFormat.FormatLine[lineInfo: lineInfo, node: topLine.node, startOffset: topLine.where, nodeStyle: style, lineWidth: Scaled.FromInt[viewer.cw], kind: kind];
IF lineInfo.nextPos.node#node OR lineInfo.nextPos.where>where OR lineInfo.nextPos=topLine THEN EXIT;
topLine ← lineInfo.nextPos;
IF topLine.where+lineInfo.nChars>where THEN EXIT;
ENDLOOP;
IF offset THEN {
move goal line a bit down from the top
goal: INTEGER = style.GetLeadingI[] * TEditProfile.scrollTopOffset;
IF goal*2 < viewer.ch THEN topLine ← BackUp[viewer, tdd, topLine, goal].newPos;
};
lineInfo.Release[]; lineInfo ← NIL;
NodeStyleOps.Free[style];
IF repaint OR topLine # lines[0].pos THEN {
TEditDisplay.EstablishLine[tdd, topLine, 0];
DoPaint[viewer, IF repaint THEN NIL ELSE TEditTouchup.refresh];
};
};
IF pos.node # NIL THEN LockAndDoIt[inner, viewer, NIL, FALSE];
};
OnScreen: PROC [viewer: Viewer, point: TextNode.Location] RETURNS [BOOL] = {
OnScreen determines whether or not the given location is visible for the given viewer. It must be called when the document is locked!
IF viewer = NIL OR point.node = NIL THEN RETURN [FALSE];
IF viewer.destroyed OR viewer.iconic THEN RETURN [FALSE];
WITH viewer.data SELECT FROM
tdd: TEditDocumentData => {
Now we know that we have a Tioga document
lines: TEditDocument.LineTable ← tdd.lineTable;
found: BOOLFALSE;
IF lines # NIL AND lines.lastLine >= 0 THEN {
first: TextNode.Location ← lines[0].pos;
last: TextNode.Location ← lines[lines.lastLine].pos;
each: TextNode.Ref ← first.node;
last.where ← last.where + lines[lines.lastLine].nChars;
IF point.node = first.node AND point.where < first.where THEN GO TO quickOut;
IF point.node = last.node AND point.where > last.where THEN GO TO quickOut;
WHILE each # NIL DO
IF each = point.node THEN {found ← TRUE; EXIT};
IF each = last.node THEN EXIT;
each ← TextNode.Forward[each].nx;
ENDLOOP;
EXITS quickOut => {};
};
RETURN [found];
};
ENDCASE;
RETURN [FALSE];
};
AutoScroll: PUBLIC PROC [viewer: Viewer, tryToGlitch: BOOL, toEndOfDoc: BOOL, id: TEditDocument.SelectionId] = {
kind: NodeStyleOps.OfStyle ~ IF ViewerOps.FetchProp[viewer, $StyleKind] = $Print THEN print ELSE screen;
sel: TEditDocument.Selection = SELECT id FROM
primary => TEditSelection.pSel,
secondary => TEditSelection.sSel,
feedback => TEditSelection.fSel,
ENDCASE => ERROR;
inner: PROC [tdd: TEditDocumentData] = {
lines: TEditDocument.LineTable ← tdd.lineTable;
goal: TextNode.Location ← [NIL, 0];
glitchOK: BOOLFALSE;
TryToGlitch: PROC RETURNS [success: BOOLFALSE] = {
glitchLines: INTEGER = MIN[MAX[lines.lastLine/2, 1], TEditProfile.scrollBottomOffset];
Need to have the MAX or will fail to scroll in 1 or 2 line viewers.
tryLines: INTEGER = MAX[lines.lastLine/2-glitchLines, 2]; -- how far to search for caret
Needs to be bigger than glitchLines since burst input may move the caret several lines.
newPos: TextNode.Location;
style: NodeStyle.Ref;
lineInfo: TEditFormat.LineInfo;
lineCount: INTEGER ← 0;
pos: TextNode.Location ← lines[lines.lastLine].pos;
foundNode: BOOL;
NextNode: PROC [node: TextNode.Ref] RETURNS [next: TextNode.Ref] = {
next ← IF tdd.clipLevel < TEditDocument.maxClip
THEN TextNode.ForwardClipped[node,tdd.clipLevel].nx
ELSE TextNode.StepForward[node]
};
IF TEditProfile.scrollBottomOffset<=0 THEN {
never glitch in this case
RETURN [TRUE]
};
success ← FALSE;
style ← NodeStyleOps.Alloc[];
IF lines[lines.lastLine].end = eon
THEN {
pos ← [NextNode[lines[lines.lastLine].pos.node],0];
IF pos.node=NIL THEN { NodeStyleOps.Free[style]; RETURN [FALSE] }
}
ELSE pos.where ← pos.where+lines[lines.lastLine].nChars;
foundNode ← pos.node=goal.node;
NodeStyleOps.ApplyAll[style, pos.node, kind];
lineInfo ← TEditFormat.Allocate[];
THROUGH [0..MIN[tryLines, lines.lastLine-glitchLines]) DO
IF pos.node=NIL THEN {
NodeStyleOps.Free[style];
lineInfo.Release[];
RETURN [FALSE];
};
TEditFormat.FormatLine[lineInfo: lineInfo, node: pos.node, startOffset: pos.where, nodeStyle: style, lineWidth: Scaled.FromInt[viewer.cw], kind: kind];
newPos ← lineInfo.nextPos;
IF newPos.node=goal.node THEN foundNode ← TRUE;
IF foundNode AND (newPos.node#goal.node OR newPos.where>goal.where) THEN {
success ← TRUE;
EXIT};
lineCount ← lineCount+1;
IF newPos.node#pos.node THEN NodeStyleOps.ApplyAll[style, newPos.node, kind];
pos ← newPos;
ENDLOOP;
NodeStyleOps.Free[style];
lineInfo.Release[];
lineInfo ← NIL;
IF success THEN {
TEditDisplay.EstablishLine[tdd, lines[lineCount+glitchLines].pos, 0];
DoPaint[viewer, TEditTouchup.refresh];
};
};
{
SELECT TRUE FROM
toEndOfDoc => {
scroll to end of document; used in typescripts
lines: TEditDocument.LineTable = tdd.lineTable;
goal ← TextNode.LastLocWithin[TextNode.FirstChild[tdd.text]];
IF lines[lines.lastLine].pos.where+lines[lines.lastLine].nChars >= goal.where THEN
RETURN;
goal.where ← goal.where-1; -- last char correction
IF tryToGlitch THEN glitchOK ← TryToGlitch[];
};
sel.viewer # viewer => {};
ENDCASE => {
selPoint: TEditDocument.SelectionPoint = IF sel.insertion=before THEN sel.start ELSE sel.end;
clipped: BOOL = selPoint.clipped;
IF NOT clipped AND OnScreen[viewer, sel.end.pos] THEN RETURN;
selection is visible on screen
goal ← TEditSelection.InsertionPoint[sel];
IF TextNode.Root[goal.node] # tdd.text THEN RETURN;
make sure that selection didn't leave
IF sel.insertion=before AND goal.where>0 THEN goal.where ← goal.where-1;
IF NOT clipped AND tryToGlitch AND selPoint.line>0 THEN
glitchOK ← TryToGlitch[];
};
};
IF NOT glitchOK AND goal.node # NIL THEN
glitch failed; use general scroll
ScrollToPosition[viewer, goal, TRUE];
};
IF viewer=NIL THEN viewer ← TEditSelection.pSel.viewer;
LockAndDoIt[inner, viewer, sel, TRUE];
};
LockAndDoIt: PROC [inner: PROC [tdd: TEditDocumentData], viewer: Viewer, sel: TEditDocument.Selection ← NIL, ignoreIcon: BOOLTRUE] = {
withViewer: PROC = {
tdd: TEditDocumentData ← NIL;
IF viewer = NIL OR viewer.destroyed OR viewer.paintingWedged THEN RETURN;
IF ignoreIcon AND viewer.iconic THEN RETURN;
IF sel # NIL AND viewer = sel.viewer THEN tdd ← sel.data;
IF tdd = NIL THEN WITH viewer.data SELECT FROM
vtdd: TEditDocumentData => tdd ← vtdd;
ENDCASE;
IF tdd = NIL THEN RETURN;
IF viewer.iconic
THEN {
[] ← TEditLocks.LockDocAndTdd[tdd, "Scroll"];
inner[tdd ! UNWIND => TEditLocks.UnlockDocAndTdd[tdd]];
TEditLocks.UnlockDocAndTdd[tdd];
}
ELSE {
IF NOT TEditTouchup.LockAfterRefresh[tdd, "Scroll"] THEN RETURN;
inner[tdd ! UNWIND => TEditTouchup.UnlockAfterRefresh[tdd]];
TEditTouchup.UnlockAfterRefresh[tdd];
};
};
IF viewer = NIL OR viewer.destroyed OR viewer.paintingWedged THEN RETURN;
IF ignoreIcon AND viewer.iconic THEN RETURN;
ViewerLocks.CallUnderWriteLock[withViewer, viewer];
};
DoPaint: PROC [viewer: Viewer, op: REFNIL] = {
clearClient: BOOL ← op = NIL;
IF forkPaints
THEN ViewerForkers.ForkPaint[viewer: viewer, hint: client, clearClient: clearClient, whatChanged: op, tryShortCuts: TRUE]
ELSE ViewerOps.PaintViewer[viewer: viewer, hint: client, clearClient: clearClient, whatChanged: op];
};
END.