-- TEditScrollingImpl.mesa; Edited by Paxton on December 29, 1982 8:45 am
-- Edited by McGregor on July 30, 1982 10:04 am
Last Edited by: Maxwell, January 4, 1983 3:53 pm
Last Edited by: Plass, April 15, 1983 1:48 pm
Last Edited by: Russ Atkinson, April 15, 1983 12:38 pm
DIRECTORY
NodeStyle USING [Ref, Alloc, Free, ApplyAll, GetTopLeadingI, GetTopIndentI, GetLeadingI, GetBottomLeadingI],
RopeReader USING [Ref, SetPosition, Backwards, GetRopeReader, FreeRopeReader],
TEditDisplay USING [EstablishLine],
TEditDocument USING [LineTable, maxClip, Selection, SelectionId, SelectionPoint, TEditDocumentData],
TEditFormat USING [GetLineInfo],
TEditInput USING [MaxLevelShown],
TEditLocks USING [LockDocAndTdd, UnlockDocAndTdd],
TEditOps USING [RememberCurrentPosition],
TEditProfile USING [scrollBottomOffset, scrollTopOffset],
TEditSelection USING [InsertionPoint, pSel, sSel, fSel],
TEditScrolling,
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, NarrowToTextNode, Parent, pZone, Ref, RefTextNode, Root, StepForward],
ViewerOps USING [PaintViewer],
ViewerClasses USING [ScrollProc, Viewer];
TEditScrollingImpl: CEDAR PROGRAM
IMPORTS NodeStyle, RopeReader, TEditDisplay, TEditFormat, TEditLocks, TEditSelection, TextEdit, TEditInput, TEditOps, TEditProfile, TEditTouchup, TextNode, ViewerOps
EXPORTS TEditScrolling =
BEGIN OPEN TEditDocument, TextNode;
BackUp: PROC
[viewer: ViewerClasses.Viewer, tdd: TEditDocumentData, pos: Location, goal: INTEGER]
RETURNS [newPos: Location, lines, totalLeading, topIndent: INTEGER] = BEGIN
pos argument is the current top line in the viewer. goal argument is the distance to back up.
algorithm works by incrementally formatting lines prior to pos until 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
maxLevel: INTEGER ← tdd.clipLevel;
parent: TextNode.Ref;
level: INTEGER ← 0; -- in case we are doing level clipping
levelClipping: BOOLEAN ← maxLevel < maxClip;
IncrBackUp: PROC [pos: Location, goal, prevTopLeading: INTEGER]
RETURNS [prev: Location, totalLeading, lines, topIndent, topLeading: INTEGER] =
BEGIN
tPos: Location;
leading, bottomLeading: INTEGER;
lastBreak: TextEdit.Offset;
breakList: LIST OF TextEdit.Offset; -- breaks between first and last
textNode: RefTextNode;
where, endOffset, size: TextEdit.Offset;
IF pos.where=0 THEN DO -- back up to previous text node
tempNode: TextNode.Ref;
IF levelClipping THEN
[tempNode, parent, level] ← BackwardClipped[pos.node, maxLevel, parent, level]
ELSE [tempNode, parent, ----] ← Backward[pos.node, parent];
IF tempNode=NIL OR parent=NIL THEN RETURN[pos, 0, 0, 0, 0];
IF (textNode ← NarrowToTextNode[tempNode])=NIL THEN LOOP;
size ← endOffset ← where ← TextEdit.Size[textNode];
EXIT;
ENDLOOP
ELSE { -- back up past last char before pos
textNode ← NarrowToTextNode[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 NodeStyle.ApplyAll[style, styleNode ← textNode];
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;
DO -- format lines from tPos to starting pos
lastBreak ← tPos.where;
tPos ← TEditFormat.GetLineInfo[viewer, tdd, tPos, style].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 ← TextNode.pZone.CONS[lastBreak, breakList];
ENDLOOP;
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 => NULL; -- 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];
END;
rdr: RopeReader.Ref ← RopeReader.GetRopeReader[];
style: NodeStyle.Ref ← NodeStyle.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
NodeStyle.ApplyAll[style, pos.node];
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;
NodeStyle.Free[style];
RopeReader.FreeRopeReader[rdr];
END;
ScrollTEditDocument: PUBLIC ViewerClasses.ScrollProc = BEGIN
tdd: TEditDocumentData = NARROW[self.data];
lines: LineTable;
topLine: Location;
paintOp: REF ANY ← TEditTouchup.refresh;
iconic: BOOL = self.iconic;
levelChange: BOOLFALSE;
IF tdd = NIL THEN RETURN;
IF iconic THEN {
[] ← TEditLocks.LockDocAndTdd[tdd, "ScrollTEditDocument", read];
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;
TEditLocks.UnlockDocAndTdd[tdd];
RETURN };
IF ~TEditTouchup.LockAfterRefresh[tdd, "ScrollTEditDocument"] THEN RETURN;
lines ← tdd.lineTable;
topLine ← lines[0].pos;
SELECT op FROM
up => BEGIN
line: INTEGER ← 0;
newLevel: INTEGER;
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 < 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
control AND shift => 1, -- first level only
control => maxClip, -- all levels
shift => MIN[maxClip, TEditInput.MaxLevelShown[tdd]+1], -- move levels
ENDCASE => tdd.clipLevel; -- no change
IF newLevel # tdd.clipLevel THEN { tdd.clipLevel ← newLevel; levelChange ← TRUE }
END;
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 ←
TextNode.pZone.NEW[TEditTouchup.PreScrollDownRec];
op.lines ← numLines;
op.distance ← topIndent+totalLeading-lines[0].yOffset;
paintOp ← op };
};
thumb => BEGIN
TEditOps.RememberCurrentPosition[self];
IF amount < 5 THEN ScrollToPosition[self, [FirstChild[tdd.text],0], FALSE]
ELSE BEGIN
totalChars: TextEdit.Offset = LocNumber[LastLocWithin[tdd.text]]-1;
pos: TextNode.Location ← LocWithin[tdd.text, (totalChars*amount)/100];
IF tdd.clipLevel < 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];
END;
TEditTouchup.UnlockAfterRefresh[tdd];
RETURN;
END;
query => BEGIN
toTop, toBottom, toEnd, totalChars, t, b: INT;
ll: INTEGER = lines.lastLine;
toTop ← LocOffset[[TextNode.FirstChild[tdd.text], 0], topLine ! BadArgs => GOTO Bad];
toBottom ← LocOffset[topLine, [lines[ll].pos.node, lines[ll].pos.where+lines[ll].nChars] !
BadArgs => GOTO Bad];
toEnd ← LocOffset[[lines[ll].pos.node, lines[ll].pos.where+lines[ll].nChars-1],
LastLocWithin[tdd.text] ! BadArgs => GOTO Bad];
totalChars ← toTop+toBottom+toEnd;
TEditTouchup.UnlockAfterRefresh[tdd];
IF totalChars<=0 THEN RETURN[0, 100];
-- make sure there's always something shown for big documents
t ← MIN[98, 100*toTop/totalChars];
b ← MAX[t+2, 100*(toTop+toBottom)/totalChars];
RETURN[t, b]; -- so compiler will coerce to short integers
EXITS Bad => { TEditTouchup.UnlockAfterRefresh[tdd]; RETURN };
This ugly hack is to protect again messed up line table.
END;
ENDCASE;
IF topLine # lines[0].pos OR levelChange THEN BEGIN
TEditDisplay.EstablishLine[tdd, topLine, 0];
IF levelChange THEN ViewerOps.PaintViewer[self, client] -- complete repaint
ELSE ViewerOps.PaintViewer[self, client, FALSE, paintOp];
END;
TEditTouchup.UnlockAfterRefresh[tdd];
END;
ScrollToPosition: PUBLIC PROC [viewer: ViewerClasses.Viewer, pos: TextNode.Location, offset: BOOLEAN] = BEGIN
tdd: TEditDocumentData = NARROW[viewer.data];
lines: LineTable;
node: TextNode.RefTextNode ← TextNode.NarrowToTextNode[pos.node];
where: INTMAX[0, MIN[pos.where, TextEdit.Size[node]-1]];
start: INT ← where;
icon: BOOL = viewer.iconic;
repaint: BOOLFALSE;
nChars: CARDINAL;
newPos, topLine: TextNode.Location;
style: NodeStyle.Ref ← NodeStyle.Alloc[];
leading: INTEGER;
backStop: INTMAX[0, where-300]; -- limit looking back too far when searching for CR's
Unlock: PROC = {
IF icon THEN TEditLocks.UnlockDocAndTdd[tdd]
ELSE TEditTouchup.UnlockAfterRefresh[tdd] };
IF tdd = NIL THEN RETURN;
IF icon THEN [] ← TEditLocks.LockDocAndTdd[tdd, "ScrollToPosition", read]
ELSE IF ~TEditTouchup.LockAfterRefresh[tdd, "ScrollToPosition"] THEN RETURN;
IF tdd.clipLevel < maxClip AND tdd.clipLevel < TextNode.Level[node] THEN {
repaint ← TRUE; tdd.clipLevel ← maxClip; -- turn off level clipping -- };
lines ← tdd.lineTable;
UNTIL start<=backStop OR TextEdit.FetchChar[node, start-1]=15C DO
start ← start - 1;
ENDLOOP;
topLine ← [node, start];
NodeStyle.ApplyAll[style, topLine.node];
DO -- find the line containing [node, where]
[----, ----, ----, newPos, nChars, leading] ← TEditFormat.GetLineInfo[
viewer, tdd, topLine, style];
IF newPos.node#node OR newPos.where>where OR newPos=topLine THEN EXIT;
topLine ← newPos;
IF topLine.where+nChars>where THEN EXIT;
ENDLOOP;
NodeStyle.Free[style];
IF offset THEN BEGIN-- move goal line a bit down from the top
goal: INTEGER = leading * TEditProfile.scrollTopOffset;
IF goal*2 < viewer.ch THEN topLine ← BackUp[viewer, tdd, topLine, goal].newPos;
END;
IF repaint OR topLine # lines[0].pos THEN BEGIN
TEditDisplay.EstablishLine[tdd, topLine, 0];
Unlock[];
IF repaint THEN ViewerOps.PaintViewer[viewer, client]
ELSE ViewerOps.PaintViewer[viewer, client, FALSE, TEditTouchup.refresh];
END
ELSE Unlock[];
END;
OnScreen: PROC [viewer: ViewerClasses.Viewer, point: TextNode.Location] RETURNS [BOOL] = {
OnScreen determines whether or not the given location is visible for the given viewer.
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: TEditDocument.TEditDocumentData => {
Now we know that we have a Tioga document
IF TEditTouchup.LockAfterRefresh[tdd, "OnScreen"] THEN {
At this point tdd is really and truly locked up. We must release it at the end of the block or bad things will happen.
ENABLE {UNWIND => TEditTouchup.UnlockAfterRefresh[tdd]};
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 => {};
};
TEditTouchup.UnlockAfterRefresh[tdd];
RETURN [found];
};
};
ENDCASE;
RETURN [FALSE];
};
AutoScroll: PUBLIC PROC [
viewer: ViewerClasses.Viewer ← TEditSelection.pSel.viewer,
tryToGlitch: BOOLEANTRUE,
toEndOfDoc: BOOLEANFALSE,
id: TEditDocument.SelectionId ← primary] = BEGIN
tdd: TEditDocumentData;
goal: TextNode.Location;
lines: LineTable;
onScreen: BOOL;
sel: Selection = SELECT id FROM
primary => TEditSelection.pSel,
secondary => TEditSelection.sSel,
feedback => TEditSelection.fSel,
ENDCASE => ERROR;
TryToGlitch: PROC RETURNS [success: BOOLEAN] = BEGIN
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;
lineCount: INTEGER ← 0;
pos: TextNode.Location ← lines[lines.lastLine].pos;
foundNode: BOOLEAN;
NextNode: PROC [node: TextNode.Ref] RETURNS [next: TextNode.Ref] = {
next ← IF tdd.clipLevel < maxClip THEN
TextNode.ForwardClipped[node,tdd.clipLevel].nx
ELSE TextNode.StepForward[node] };
IF TEditProfile.scrollBottomOffset<=0 THEN {
TEditTouchup.UnlockAfterRefresh[tdd];
RETURN [TRUE] }; -- never glitch in this case
success ← FALSE;
style ← NodeStyle.Alloc[];
IF lines[lines.lastLine].end = eon THEN {
pos ← [NextNode[lines[lines.lastLine].pos.node],0];
IF pos.node=NIL THEN { NodeStyle.Free[style]; RETURN [FALSE] }}
ELSE pos.where ← pos.where+lines[lines.lastLine].nChars;
foundNode ← pos.node=goal.node;
NodeStyle.ApplyAll[style, pos.node];
THROUGH [0..MIN[tryLines, lines.lastLine-glitchLines]) DO
IF pos.node=NIL THEN { NodeStyle.Free[style]; RETURN [FALSE] };
[----, ----, ----, newPos, ----, ----] ← TEditFormat.GetLineInfo[
viewer, tdd, pos, style];
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 NodeStyle.ApplyAll[style, newPos.node];
pos ← newPos;
ENDLOOP;
NodeStyle.Free[style];
IF success THEN BEGIN
TEditDisplay.EstablishLine[tdd, lines[lineCount+glitchLines].pos, 0];
TEditTouchup.UnlockAfterRefresh[tdd];
ViewerOps.PaintViewer[viewer, client, FALSE, TEditTouchup.refresh];
END;
END;
IF viewer=NIL OR viewer.iconic OR viewer.destroyed THEN RETURN;
onScreen ← OnScreen[viewer, sel.start.pos]; -- determines if selection is on the screen
tdd ← IF viewer=sel.viewer THEN sel.data ELSE NARROW[viewer.data];
IF tdd = NIL THEN RETURN;
IF ~TEditTouchup.LockAfterRefresh[tdd, "AutoScroll"] THEN RETURN;
lines ← tdd.lineTable;
IF toEndOfDoc THEN BEGIN-- scroll to end of document; used in typescripts
lines: LineTable = tdd.lineTable;
goal ← TextNode.LastLocWithin[TextNode.FirstChild[tdd.text]];
IF lines[lines.lastLine].pos.where+lines[lines.lastLine].nChars >= goal.where
THEN {TEditTouchup.UnlockAfterRefresh[tdd]; RETURN};
goal.where ← goal.where-1; -- last char correction
IF tryToGlitch AND TryToGlitch[] THEN RETURN;
END
ELSE IF sel.viewer # viewer THEN {TEditTouchup.UnlockAfterRefresh[tdd]; RETURN}
ELSE BEGIN-- scroll to end of selection
selPoint: TEditDocument.SelectionPoint = IF sel.insertion=before THEN sel.start ELSE sel.end;
clipped: BOOL = selPoint.clipped;
IF ~clipped AND onScreen THEN
{TEditTouchup.UnlockAfterRefresh[tdd]; RETURN}; -- visible on screen
goal ← TEditSelection.InsertionPoint[sel];
IF TextNode.Root[goal.node] # tdd.text THEN -- make sure that selection didn't leave
{TEditTouchup.UnlockAfterRefresh[tdd]; RETURN};
IF sel.insertion=before AND goal.where>0 THEN goal.where ← goal.where-1;
IF ~clipped AND tryToGlitch AND selPoint.line>0 AND TryToGlitch[] THEN RETURN;
END;
TEditTouchup.UnlockAfterRefresh[tdd];
-- glitch failed; use general scroll
ScrollToPosition[viewer, goal, TRUE];
END;
END.