<<>> <> <> <> <> <> <> <<>> DIRECTORY Ascii, Char, CodeTimer, NodeStyle, NodeStyleOps, RopeReader, Scaled, TEditDisplay, TEditDocument, TEditFormat, TEditInput, TEditLocks, TEditOps, TEditProfile, TEditScrolling, TEditSelection, TEditTouchup, TextEdit, TextNode, ViewerClasses, ViewerForkers, ViewerLocks, ViewerOps; TEditScrollingImpl: CEDAR PROGRAM IMPORTS Char, CodeTimer, NodeStyle, NodeStyleOps, RopeReader, Scaled, TEditDisplay, TEditFormat, TEditInput, TEditLocks, TEditOps, TEditProfile, TEditSelection, TEditTouchup, TextEdit, TextNode, ViewerForkers, ViewerLocks, ViewerOps EXPORTS TEditScrolling = BEGIN TEditDocumentData: TYPE = TEditDocument.TEditDocumentData; Viewer: TYPE = ViewerClasses.Viewer; forkPaints: BOOL ¬ FALSE; <> BackUp: PROC [viewer: Viewer, tdd: TEditDocumentData, pos: TextNode.Location, goal: INTEGER] RETURNS [newPos: TextNode.Location, lines, totalLeading, topIndent: INTEGER] = { <> <> <> <> <> <> <> 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 <> 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 { <> 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 SELECT RopeReader.Backwards[rdr] FROM Ascii.CR, Ascii.LF => {where ¬ where+1; EXIT}; ENDCASE; ENDLOOP }; IF styleNode # textNode THEN NodeStyleOps.ApplyAll[style, styleNode ¬ textNode, kind]; leading ¬ NodeStyle.GetInt[style, leading]; topLeading ¬ IF where=0 THEN NodeStyle.GetInt[style, topLeading] ELSE leading; bottomLeading ¬ totalLeading ¬ IF pos.node = textNode THEN leading ELSE MAX[prevTopLeading, NodeStyle.GetInt[style, bottomLeading]]; topIndent ¬ NodeStyle.GetInt[style, topIndent]; -- in case this line appears at top of viewer IF where=size THEN { <> lines ¬ 1; prev ¬ [textNode, where]; RETURN }; tPos ¬ [textNode, where]; lines ¬ 0; lineInfo ¬ TEditFormat.Allocate[]; DO <> lastBreak ¬ tPos.where; TEditFormat.FormatLine[lineInfo: lineInfo, node: tPos.node, startOffset: tPos.where, nodeStyle: style, lineWidth: Scaled.FromInt[viewer.cw]]; 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; <<>> <> <> <> <> IF totalLeading+topIndent >= goal AND leading > 0 THEN { <> 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 => { <> 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: INTEGER ¬ LAST[INTEGER]; topLeading: INTEGER ¬ 0; IF pos.where=0 THEN { <> NodeStyleOps.ApplyAll[style, pos.node, kind]; topLeading ¬ NodeStyle.GetInt[style, topLeading]; <> }; 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: BOOL ¬ FALSE; 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; CodeTimer.StartInt[$ScrollUp, $PTioga]; IF amount > 0 AND lines.lastLine = 0 THEN { <> IF lines[0].end = eon THEN { <> 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 { <> 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, <> control => TEditDocument.maxClip, <> shift => MIN[TEditDocument.maxClip, TEditInput.MaxLevelShown[tdd]+1], <> ENDCASE => tdd.clipLevel; -- no change IF newLevel # tdd.clipLevel THEN { tdd.clipLevel ¬ newLevel; levelChange ¬ TRUE; }; CodeTimer.StopInt[$ScrollUp, $PTioga]; }; down => { numLines, totalLeading, topIndent: INTEGER; CodeTimer.StartInt[$ScrollDown, $PTioga]; [topLine, numLines, totalLeading, topIndent] ¬ BackUp[self, tdd, topLine, MAX[amount, lines[0].yOffset]]; IF topLine # lines[0].pos THEN { <> op: REF TEditTouchup.PreScrollDownRec ¬ NEW[TEditTouchup.PreScrollDownRec]; op.lines ¬ numLines; op.distance ¬ topIndent+totalLeading-lines[0].yOffset; paintOp ¬ op }; CodeTimer.StopInt[$ScrollDown, $PTioga]; }; thumb => { loc1: TextNode.Location ~ [TextNode.FirstChild[tdd.text], 0]; CodeTimer.StartInt[$Thumb, $PTioga]; TEditOps.RememberCurrentPosition[self]; IF amount < 5 THEN ScrollToPosition[self, loc1, FALSE] ELSE { loc2: TextNode.Location ~ TextNode.LastLocWithin[tdd.text]; totalChars: INT ~ TextNode.LocOffset[loc1, loc2]; pos: TextNode.Location ¬ TextNode.LocRelative[loc1, (totalChars*amount)/100]; IF tdd.clipLevel < TEditDocument.maxClip THEN { <> delta: INTEGER ~ TextNode.Level[pos.node]-tdd.clipLevel; THROUGH [0..delta) DO -- only do this if pos is too deep pos ¬ [TextNode.Parent[pos.node],0]; ENDLOOP; }; ScrollToPosition[self, pos, FALSE]; }; CodeTimer.StopInt[$Thumb, $PTioga]; 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}; <> 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 { CodeTimer.StartInt[$ScrollDoPaint, $PTioga]; TEditDisplay.EstablishLine[tdd, topLine, 0]; DoPaint[self, IF levelChange THEN NIL ELSE paintOp]; CodeTimer.StopInt[$ScrollDoPaint, $PTioga]; }; }; <> 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: INT ¬ MAX[0, MIN[pos.where, TextEdit.Size[node]-1]]; start: INT ¬ where; lines: TEditDocument.LineTable ¬ tdd.lineTable; repaint: BOOL ¬ FALSE; topLine: TextNode.Location; style: NodeStyle.Ref ¬ NodeStyleOps.Alloc[]; lineInfo: TEditFormat.LineInfo; backStop: INT ¬ MAX[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 DO SELECT TextEdit.FetchChar[node, start-1] FROM Char.Widen[Ascii.CR], Char.Widen[Ascii.LF] => EXIT; ENDCASE => start ¬ start - 1; ENDLOOP; topLine ¬ [node, start]; NodeStyleOps.ApplyAll[style, topLine.node, kind]; lineInfo ¬ TEditFormat.Allocate[]; DO <> TEditFormat.FormatLine[lineInfo: lineInfo, node: topLine.node, startOffset: topLine.where, nodeStyle: style, lineWidth: Scaled.FromInt[viewer.cw]]; 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 { <> goal: INTEGER = NodeStyle.GetInt[style, leading] * 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] = { <> 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 => { <> lines: TEditDocument.LineTable ¬ tdd.lineTable; found: BOOL ¬ FALSE; 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: BOOL ¬ FALSE; TryToGlitch: PROC RETURNS [success: BOOL ¬ FALSE] = { glitchLines: INTEGER = MIN[MAX[lines.lastLine/2, 1], TEditProfile.scrollBottomOffset]; <> tryLines: INTEGER = MAX[lines.lastLine/2-glitchLines, 2]; -- how far to search for caret <> 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 { <> 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]]; 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 => { <> 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; <> goal ¬ TEditSelection.InsertionPoint[sel]; IF TextNode.Root[goal.node] # tdd.text THEN RETURN; <> 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 <> 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: BOOL ¬ TRUE] = { 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: REF ¬ NIL] = { 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.