PipalTextEditorImpl:
CEDAR
PROGRAM
IMPORTS BiScrollers, Imager, ImagerBackdoor, ImagerFont, ImagerSample, InputFocus, IO, MessageWindow, Pipal, PipalEdit, PipalInt, PipalInteractiveEdit, PipalPaint, PipalReal, Real, Rope, TerminalIO, VFonts, ViewerOps
EXPORTS PipalTextEditor =
BEGIN OPEN PipalTextEditor;
Text Class
pipalTextClass:
PUBLIC Pipal.Class ← Pipal.RegisterClass[name: "PipalText", type:
CODE[PipalTextRec]];
TextDescribe: Pipal.DescribeProc = {
text: PipalText ← NARROW [object];
Pipal.PutIndent[out, indent, cr];
IO.PutF[out, "Text [%g]", IO.rope[text.rope]];
};
TextSize: PipalReal.SizeProc ~ {
size ← NARROW[object, PipalText].size;
};
fixedFont: Imager.Font ← VFonts.defaultFont;
feedbackHeight: REAL ← 4.0;
fontHeight: REAL ← ImagerFont.FontBoundingBox[fixedFont].descent + ImagerFont.FontBoundingBox[fixedFont].ascent;
lineHeight:
REAL ← fontHeight + feedbackHeight;
TextPaint: PipalPaint.PaintProc ~ {
EachRow: RowProc = {
Imager.SetXY[context, [0.0, y]];
Imager.ShowRope[context, text.rope, interval.base, interval.size];
};
text: PipalText ← NARROW[object];
Imager.SetColor[context, Imager.black];
Imager.SetFont[context, fixedFont];
[] ← EnumerateRows[text, EachRow];
};
emptySize: PipalReal.Size ← PipalReal.emptySize;
CreatePipalText:
PUBLIC
PROC [rope: Pipal.
ROPE, size: PipalReal.Size ← PipalReal.emptySize]
RETURNS [text: PipalText] = {
text ← NEW[PipalTextRec ← [rope, size]];
IF size=PipalReal.emptySize
THEN {
extents: ImagerFont.Extents ← ImagerFont.RopeBoundingBox[font: fixedFont, rope: rope];
text.size ← [extents.leftExtent+extents.rightExtent, lineHeight];
};
};
GetSelectionInterval:
PUBLIC
PROC [text: PipalText, position: PipalReal.Position, grain: SelectionGrain]
RETURNS [interval: PipalInt.Interval ← [0, 0], closerRight:
BOOL ←
FALSE] = {
ropeSize: INT ← Rope.Size[text.rope];
IF ropeSize>0
THEN {
leftEdge, rightEdge: REAL ← 0.0;
location: Location;
bbox: PipalReal.Rectangle;
[location, bbox] ← CharLocation[text, position];
leftEdge ← bbox.base.x;
rightEdge ← bbox.base.x+bbox.size.x;
SELECT grain
FROM
point => {
interval.base ← location;
interval.size ← 0;
};
char => {
interval.base ← location;
interval.size ← 1;
};
word => {
start: Location ← location;
char: CHAR ← Rope.Fetch[text.rope, location];
IF CharSeparator[char] THEN interval.base ← location
ELSE {
UNTIL location=0
OR CharSeparator[(char ← Rope.Fetch[text.rope, location-1])]
DO
location ← location-1;
leftEdge ← leftEdge-CharSize[char].x;
ENDLOOP;
interval.base ← location;
location ← start;
UNTIL location=ropeSize-1
OR CharSeparator[(char ← Rope.Fetch[text.rope, location+1])]
DO
location ← location+1;
rightEdge ← rightEdge+CharSize[char].x;
ENDLOOP;
};
interval.size ← location-interval.base+1;
};
ENDCASE => ERROR;
closerRight ← ABS[rightEdge-position.x]<ABS[position.x-leftEdge];
};
};
CharLocation:
PROC [text: PipalText, position: PipalReal.Position]
RETURNS [location: Location, bbox: PipalReal.Rectangle] = {
EachChar: CharProc = {
distance: REAL ← RectanglePointDistance[bbox, position];
IF (quit ← PipalReal.IsInsidePoint[bbox, position])
OR distance<closest
THEN {
loc ← location;
bb ← bbox;
closest ← distance;
};
};
loc: Location ← 0;
bb: PipalReal.Rectangle ← PipalReal.emptyRectangle;
closest: REAL ← PipalReal.infinity;
[] ← EnumerateChars[text, EachChar];
location ← loc;
bbox ← bb;
};
CharProc: TYPE = PROC [char: CHAR, location: Location, bbox: PipalReal.Rectangle] RETURNS [quit: BOOL ← FALSE];
EnumerateChars:
PROC [text: PipalText, eachChar: CharProc]
RETURNS [quit:
BOOL ←
FALSE] = {
EachRow: RowProc = {
quit ← EnumerateCharsInRow[text, interval, y, eachChar];
};
quit ← EnumerateRows[text, EachRow];
};
EnumerateCharsInRow:
PROC [text: PipalText, interval: PipalInt.Interval, y:
REAL, eachChar: CharProc]
RETURNS [quit:
BOOL ←
FALSE] = {
x: REAL ← 0.0;
FOR loc: Location
IN [interval.base..interval.base+interval.size)
DO
char: CHAR ← Rope.Fetch[text.rope, loc];
cx: REAL ← CharSize[char].x;
IF eachChar[char, loc, [[x, y-feedbackHeight], [cx, lineHeight]]] THEN {quit ← TRUE; EXIT};
x ← x + cx;
ENDLOOP;
};
CharSize:
PROC [char:
CHAR]
RETURNS [size: PipalReal.Size] = {
size ← [ImagerFont.Escapement[fixedFont, [0, LOOPHOLE[char]]].x, lineHeight]
};
RowProc: TYPE = PROC [interval: PipalInt.Interval, y: REAL] RETURNS [quit: BOOL ← FALSE];
EnumerateRows:
PROC [text: PipalText, eachRow: RowProc]
RETURNS [quit:
BOOL ←
FALSE] = {
start: Location ← 0;
y: REAL ← text.size.y-lineHeight+feedbackHeight;
length: INT ← Rope.Length[text.rope];
IF length>0
THEN
DO
rightEdge: REAL ← 0.0;
firstUnpaintedChar: Location ← start;
lastSeparator: Location ← start;
DO
char: CHAR ← Rope.Fetch[text.rope, firstUnpaintedChar];
rightEdge ← rightEdge + CharSize[char].x;
IF rightEdge>=text.size.x THEN EXIT;
IF CharSeparator[char] THEN lastSeparator ← firstUnpaintedChar;
firstUnpaintedChar ← firstUnpaintedChar + 1;
IF firstUnpaintedChar=length THEN EXIT;
ENDLOOP;
IF firstUnpaintedChar=length THEN lastSeparator ← length-1;
IF eachRow[[start, lastSeparator-start+1], y] THEN {quit ← TRUE; EXIT};
IF lastSeparator+1=length THEN EXIT;
start ← lastSeparator+1;
y ← y-lineHeight;
ENDLOOP;
};
CharSeparator:
PROC [char:
CHAR]
RETURNS [separator:
BOOL] = {
separator ← NOT (char IN ['a..'z] OR char IN ['A..'Z] OR char IN ['0..'9]);
};
Text Editor Class
textEditorClass:
PUBLIC Pipal.Class ← Pipal.RegisterClass[name: "TextEditor", type:
CODE[TextEditorRec]];
TextEditorDescribe: Pipal.DescribeProc = {
textEditor: TextEditor ← NARROW[object];
Pipal.PutIndent[out, indent, cr];
IO.PutF[out, "Text Editor"];
};
TextEditorSize: PipalReal.SizeProc ~ {
size ← PipalReal.emptySize;
};
black: Imager.Color ← ImagerBackdoor.MakeStipple[stipple: 0FFFFH, xor: TRUE];
lightGrey: Imager.Color ← ImagerBackdoor.MakeStipple[stipple: 0208H, xor: TRUE];
darkGrey: Imager.Color ← ImagerBackdoor.MakeStipple[stipple: 0A5A5H, xor:
TRUE];
caretH: INTEGER = 6;
CaretBits: TYPE ~ REF CaretBitsRep;
CaretBitsRep:
TYPE =
ARRAY [0..caretH)
OF
WORD;
caretBits: CaretBits
← NEW[CaretBitsRep ← [
000400B, -- 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0
001600B, -- 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
003700B, -- 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0
003300B, -- 0 0 0 0 0 1 1 0 1 1 0 0 0 0 0 0
006140B, -- 0 0 0 0 1 1 0 0 0 1 1 0 0 0 0 0
004040B -- 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0
]];
caretMap: ImagerSample.SampleMap ← SampleMapFromCaretBits[caretBits];
SampleMapFromCaretBits:
PROC [bits: CaretBits]
RETURNS [ImagerSample.SampleMap] ~
TRUSTED {
RETURN [ImagerSample.UnsafeNewSampleMap[
box: [min: [s: 0, f: 0], max: [s: caretH, f: caretW]],
bitsPerSample: 1, bitsPerLine: BITS[WORD], base: [word: LOOPHOLE[bits], bit: 0],
ref: bits, words: SIZE[CaretBitsRep]
]];
};
TextEditorPaint: PipalPaint.PaintProc = {
EachRow: RowProc = {
y ← y-feedbackHeight;
FOR selection: Selections
IN Selections
DO
IF te.selections[selection].valid
AND PipalInt.IntersectInterval[interval, te.selections[selection].interval]
THEN {
sel: Selection ← te.selections[selection];
selectInterval: PipalInt.Interval ← IntersectIntervals[interval, sel.interval];
start: REAL ← ImagerFont.RopeEscapement[fixedFont, text.rope, interval.base, selectInterval.base-interval.base].x;
SELECT
TRUE
FROM
selection=primary => Imager.SetColor[context, black];
sel.pendingDelete => Imager.SetColor[context, lightGrey];
ENDCASE => Imager.SetColor[context, darkGrey];
IF selectInterval.size>0
THEN {
length: REAL ← ImagerFont.RopeEscapement[fixedFont, text.rope, selectInterval.base, selectInterval.size].x;
IF sel.pendingDelete THEN Imager.MaskRectangle[context, [start, y, length, fontHeight]]
ELSE Imager.MaskRectangle[context, [start, y, length, feedbackHeight]];
IF (sel.caretAfter AND PipalInt.IsInsideIntervalNumber[selectInterval, sel.interval.base+sel.interval.size]) OR (NOT sel.caretAfter AND PipalInt.IsInsideIntervalNumber[selectInterval, sel.interval.base]) THEN Imager.MaskBitmap[context, caretMap, [0, 7], Imager.defaultScanMode, [IF sel.caretAfter THEN start+length ELSE start, y+feedbackHeight]];
}
ELSE Imager.MaskBitmap[context, caretMap, [0, 7], Imager.defaultScanMode, [start, y+feedbackHeight]];
};
ENDLOOP;
};
editor: PipalEdit.Editor ← NARROW[object];
text: PipalText ← NARROW[editor.object];
te: TextEditor ← NARROW[editor.state];
PipalPaint.Paint[text, context];
Imager.SetStrokeWidth[context, feedbackHeight];
[] ← EnumerateRows[text, EachRow];
};
Manipulation
InsertRope:
PUBLIC
PROC [editor: PipalEdit.Editor, characters: Pipal.
ROPE] = {
text: PipalText ← NARROW[editor.object];
te: TextEditor ← NARROW[editor.state];
[text, te] ← BasicInsert[text, te, characters];
PipalEdit.Do[editor, "insert rope", text, te];
};
InsertChar:
PUBLIC
PROC [editor: PipalEdit.Editor, character:
CHAR] = {
text: PipalText ← NARROW[editor.object];
te: TextEditor ← NARROW[editor.state];
[text, te] ← BasicInsert[text, te, Rope.FromChar[character]];
PipalEdit.Do[editor, "insert char", text, te];
};
Copy:
PUBLIC
PROC [editor: PipalEdit.Editor] = {
text: PipalText ← NARROW[editor.object];
te: TextEditor ← NARROW[editor.state];
IF te.selections[secondary].valid
THEN {
[text, te] ← BasicInsert[text, te, Rope.Substr[text.rope, te.selections[secondary].interval.base, te.selections[secondary].interval.size]];
IF te.selections[secondary].pendingDelete THEN [text, te] ← BasicDelete[text, te, te.selections[secondary].interval];
}
ELSE ERROR Pipal.Error[$noSecondarySelection];
PipalEdit.Do[editor, "copy", text, te];
};
Transpose:
PUBLIC
PROC [editor: PipalEdit.Editor] = {
text: PipalText ← NARROW[editor.object];
te: TextEditor ← NEW[TextEditorRec ← NARROW[editor.state, TextEditor]^];
IF NOT te.selections[primary].valid THEN ERROR Pipal.Error[$noPrimarySelection];
IF NOT te.selections[secondary].valid THEN ERROR Pipal.Error[$noSecondarySelection];
{
i1: PipalInt.Interval;
i2: PipalInt.Interval;
switched: BOOL;
[i1, i2, switched] ← DecomposeIntervals[te.selections[primary].interval, te.selections[secondary].interval];
text ← CreatePipalText[Rope.Cat[
Rope.Substr[text.rope, 0, i1.base],
Rope.Substr[text.rope, i2.base, i2.size],
Rope.Substr[text.rope, i1.base+i1.size, i2.base-(i1.base+i1.size)],
Rope.Substr[text.rope, i1.base, i1.size],
Rope.Substr[text.rope, i2.base+i2.size, Rope.Length[text.rope]-i2.base+i2.size]], text.size];
{
difference: INT ← te.selections[primary].interval.size - te.selections[secondary].interval.size;
t: Location ← te.selections[primary].interval.base;
te.selections[primary].interval.base ← te.selections[secondary].interval.base;
te.selections[secondary].interval.base ← t;
IF switched THEN te.selections[secondary].interval.base ← te.selections[secondary].interval.base + difference
ELSE te.selections[primary].interval.base ← te.selections[primary].interval.base - difference;
};
};
PipalEdit.Do[editor, "transpose", text, te];
};
Delete:
PUBLIC
PROC [editor: PipalEdit.Editor] = {
text: PipalText ← NARROW[editor.object];
te: TextEditor ← NEW[TextEditorRec ← NARROW[editor.state, TextEditor]^];
IF NOT te.selections[primary].valid THEN ERROR Pipal.Error[$noPrimarySelection];
te.paste ← Rope.Substr[text.rope, te.selections[primary].interval.base, te.selections[primary].interval.size];
[text, te] ← BasicDelete[text, te, te.selections[primary].interval];
PipalEdit.Do[editor, "delete", text, te];
};
Paste:
PUBLIC
PROC [editor: PipalEdit.Editor] = {
text: PipalText ← NARROW[editor.object];
te: TextEditor ← NARROW[editor.state];
[text, te] ← BasicInsert[text, te, te.paste];
PipalEdit.Do[editor, "paste", text, te];
};
Erase:
PUBLIC
PROC [editor: PipalEdit.Editor] = {
text: PipalText ← NARROW[editor.object];
te: TextEditor ← NARROW[editor.state];
sel: Selection ← te.selections[primary];
loc: Location;
IF NOT sel.valid THEN ERROR Pipal.Error[$noPrimarySelection];
loc ← IF sel.caretAfter THEN sel.interval.base+sel.interval.size ELSE sel.interval.base;
IF loc=0 THEN ERROR Pipal.Error[$noCharacterToErase];
[text, te] ← BasicDelete[text, te, [loc-1, 1]];
PipalEdit.Do[editor, "erase", text, te];
};
BasicInsert:
PROC [oldText: PipalText, oldte: TextEditor, characters: Pipal.
ROPE]
RETURNS [newText: PipalText, newte: TextEditor] = {
size: Location ← Rope.Size[characters];
loc: Location;
IF NOT oldte.selections[primary].valid THEN ERROR Pipal.Error[$noPrimarySelection];
IF oldte.selections[primary].pendingDelete
THEN {
paste: Rope.ROPE ← Rope.Substr[oldText.rope, oldte.selections[primary].interval.base, oldte.selections[primary].interval.size];
[newText, newte] ← BasicDelete[oldText, oldte, oldte.selections[primary].interval];
newte.paste ← paste;
}
ELSE {
newText ← oldText;
newte ← NEW[TextEditorRec ← oldte^];
};
loc ← IF newte.selections[primary].caretAfter THEN newte.selections[primary].interval.base + newte.selections[primary].interval.size ELSE newte.selections[primary].interval.base;
newText ← CreatePipalText[Rope.Cat[
Rope.Substr[newText.rope, 0, loc],
characters,
Rope.Substr[newText.rope, loc, Rope.Size[newText.rope]-loc]], newText.size];
FOR selection: Selections
IN Selections
DO
IF newte.selections[selection].valid AND loc<=newte.selections[selection].interval.base THEN newte.selections[selection].interval.base ← newte.selections[selection].interval.base + size;
ENDLOOP;
};
BasicDelete:
PROC [oldText: PipalText, oldte: TextEditor, interval: PipalInt.Interval]
RETURNS [newText: PipalText, newte: TextEditor] = {
base: Location ← interval.base;
size: INT ← interval.size;
newText ← CreatePipalText[Rope.Cat[Rope.Substr[oldText.rope, 0, base], Rope.Substr[oldText.rope, base+size, Rope.Size[oldText.rope]-(base+size)]], oldText.size];
newte ← NEW[TextEditorRec ← oldte^];
FOR sel: Selections
IN Selections
DO
IF oldte.selections[sel].valid
THEN {
IF base<oldte.selections[sel].interval.base THEN newte.selections[sel].interval.base ← IF base+size<oldte.selections[sel].interval.base THEN oldte.selections[sel].interval.base - size ELSE base;
newte.selections[sel].interval.size ← oldte.selections[sel].interval.size - IntersectIntervals[interval, oldte.selections[sel].interval].size;
};
ENDLOOP;
};
Selection
SetSelection:
PUBLIC
PROC [editor: PipalEdit.Editor, interval: PipalInt.Interval, selection: Selections ← primary, pendingDelete:
BOOL ←
FALSE, caretAfter:
BOOL ←
FALSE, granularity: SelectionGrain ← char] = {
text: PipalText ← NARROW[editor.object];
te: TextEditor ← NEW[TextEditorRec ← NARROW[editor.state, TextEditor]^];
te.selections[selection].valid ← TRUE;
te.selections[selection].interval ← interval;
te.selections[selection].pendingDelete ← pendingDelete;
te.selections[selection].caretAfter ← caretAfter;
te.selections[selection].granularity ← granularity;
IF (te.selections[selection].interval.size#0 AND te.selections[selection].granularity=point) OR (caretAfter AND interval.base>=Rope.Size[text.rope]) THEN ERROR;
PipalEdit.Do[editor, "select", text, te];
};
CancelSelection:
PUBLIC
PROC [editor: PipalEdit.Editor, selection: Selections ← primary] = {
text: PipalText ← NARROW[editor.object];
te: TextEditor ← NEW[TextEditorRec ← NARROW[editor.state, TextEditor]^];
te.selections[selection].valid ← FALSE;
PipalEdit.Do[editor, "cancel selection", text, te];
};
PipalInt
IntersectIntervals:
PROC [i1, i2: PipalInt.Interval]
RETURNS [i3: PipalInt.Interval] = {
i3.base ← MAX[i1.base, i2.base];
i3.size ← MIN[i1.base+i1.size, i2.base+i2.size]-i3.base;
IF i3.size<0 THEN i3.size ← 0;
};
DecomposeIntervals:
PROC [i1, i2: PipalInt.Interval]
RETURNS [i3, i4: PipalInt.Interval, switched:
BOOL ←
FALSE] = {
Orders i1 and i2 so that i3.base<i4.base and then computes intervals which cover the nonintersecting portions of i3 and i4. If i1 and i2 are exchanged then switched is TRUE.
IF i1.base>i2.base
THEN {
t: PipalInt.Interval ← i1; i1 ← i2; i2 ← t;
switched ← TRUE;
};
i3.base ← i1.base;
i3.size ← MIN[i1.size, i2.base-i1.base];
i4.base ← MAX[i2.base, i1.base+i1.size];
i4.size ← MIN[i2.size, ABS[(i2.base+i2.size)-(i1.base+i1.size)]];
};
Interactive Editor
InteractiveEditor:
PUBLIC
PROC [size: PipalReal.Size]
RETURNS [viewerData: PipalInteractiveEdit.ViewerData] = {
viewerData ← PipalInteractiveEdit.Create[Create[CreatePipalText[NIL, size]], "PipalTextEditor.TIP", EditorNotify, NEW[EditorNotifyDataRec]];
};
EditorNotifyData: TYPE = REF EditorNotifyDataRec;
EditorNotifyDataRec:
TYPE =
RECORD [
coords: PipalReal.Vector ← PipalReal.zeroVector, -- mx, my
editState: EditState ← reset,
sel: Selections ← primary,
selState: SelState ← reset,
pdelState: PDelState ← reset,
pDel: BOOL ← FALSE,
prevPSel: Selection,
initialSelect: PipalInt.Interval,
mouseColor: MouseColor ← red,
editMessage: Rope.ROPE ← NIL];
EditState:
TYPE = {
reset, -- no edit specified
abort, -- edit aborted
tolimbo, -- delete primary
toprimary, -- copy/move secondary selection to/onto primary
tosecondary, -- copy/move primary to/onto secondary
toboth -- transpose primary and secondary
};
SelState:
TYPE = {
reset, -- not specified yet
primary, -- making a primary selection
secondary
};
PDelState:
TYPE = {
reset, -- not specified yet
pending, -- making a pending delete selection
not
};
LevelChange:
TYPE = {reduce, expand, same};
MouseColor:
TYPE = {red, yellow, blue, dead};
MessageArray: TYPE = ARRAY BOOL OF ARRAY BOOL OF Rope.ROPE;
toPrimaryMessages: REF MessageArray ← NEW [MessageArray];
toSecondaryMessages:
REF MessageArray ←
NEW [MessageArray];
EditorNotify: ViewerClasses.NotifyProc = {
Complain:
PROC [atom:
ATOM] = {
IO.PutF[TerminalIO.TOS[], "Unimplemented tip table atom %g\n", IO.atom[atom]];
someComplaint ← TRUE;
};
AbortSecondary:
PROC = {
MessageWindow.Append["Make a primary selection first.",TRUE];
MessageWindow.Blink[];
d.editState ← abort; d.mouseColor ← dead
};
EditMessage:
PROC = {
msg: Rope.
ROPE ←
SELECT d.editState
FROM
tolimbo => "Select for delete",
toprimary => toPrimaryMessages [textEditor.selections[primary].pendingDelete] [textEditor.selections[secondary].pendingDelete],
tosecondary => toSecondaryMessages [textEditor.selections[primary].pendingDelete] [textEditor.selections[secondary].pendingDelete],
toboth => "Select for transpose",
ENDCASE => NIL;
IF msg = NIL OR msg = d.editMessage THEN RETURN;
MessageWindow.Append[msg,TRUE];
d.editMessage ← msg
};
Extend:
PROC [saveEnds:
BOOL] = {
IF d.editState=abort THEN RETURN;
IF d.mouseColor # blue
THEN {
IF d.mouseColor = dead THEN SIGNAL BadMouse;
d.mouseColor ← blue;
d.prevPSel ← textEditor.selections[primary];
saveEnds ← TRUE;
};
{
grain: SelectionGrain ← textEditor.selections[d.sel].granularity;
new: PipalInt.Interval;
closerRight: BOOL;
set: PipalInt.Interval;
IF saveEnds THEN d.initialSelect ← textEditor.selections[d.sel].interval;
[new, closerRight] ← GetSelectionInterval[text, d.coords, grain];
IF d.initialSelect.base<=new.base
THEN {
set.base ← d.initialSelect.base;
set.size ← new.base+new.size-d.initialSelect.base;
}
ELSE {
set.base ← new.base;
set.size ← d.initialSelect.base+d.initialSelect.size-new.base;
};
SetSelection[editor, set, d.sel, d.pDel, closerRight, grain];
IF d.sel=secondary THEN EditMessage[];
};
};
viewerData: PipalInteractiveEdit.ViewerData ← NARROW [BiScrollers.ClientDataOfViewer[self]];
d: EditorNotifyData ← NARROW[viewerData.data];
editor: PipalEdit.Editor ← viewerData.editor;
text: PipalText ← NARROW[editor.object];
textEditor: TextEditor ← NARROW[editor.state];
someComplaint: BOOL ← FALSE;
FOR input ← input, input.rest
UNTIL input=
NIL
DO
WITH input.first
SELECT
FROM
coords: BiScrollers.ClientCoords => d.coords ← coords^;
char:
REF
CHAR =>
SELECT char^
FROM
IO.BS => Erase[editor ! Pipal.Error => IF reason=$noCharacterToErase THEN {TerminalIO.PutRope["No character to erase\n"]; CONTINUE} ELSE REJECT];
ENDCASE => {
newBase: Location ← textEditor.selections[primary].interval.base;
IF textEditor.selections[primary].caretAfter THEN newBase ← newBase + textEditor.selections[primary].interval.size;
InsertChar[editor, char^];
SetSelection[editor: editor, interval: [newBase+1, 0], selection: primary, pendingDelete: FALSE, caretAfter: FALSE, granularity: point];
};
atom:
ATOM => {
InputFocus.SetInputFocus[self];
SELECT atom
FROM
$BlueMouse => {
IF d.mouseColor = dead THEN SIGNAL BadMouse;
d.mouseColor ← blue;
d.prevPSel ← textEditor.selections[primary];
};
$DoEdit => {
SELECT d.editState
FROM
tolimbo => Delete[editor];
reset, abort => NULL;
toprimary => {Copy[editor]; CancelSelection[editor, secondary]};
tosecondary => Complain[$DoEditToSecondary];
toboth => Transpose[editor];
ENDCASE => ERROR;
d.editState ← reset;
d.sel ← primary;
d.selState ← reset;
d.pdelState ← reset;
d.pDel ← FALSE;
d.mouseColor ← red;
IF d.editMessage # NIL THEN MessageWindow.Clear[];
d.editMessage ← NIL;
};
$ForceSelNotPendDel => {
d.pdelState ← reset;
d.pdelState ← not;
d.pDel ← FALSE;
};
$ForceSelPendDel => {
d.pdelState ← reset;
d.pdelState ← pending;
d.pDel ← TRUE;
};
$RedMouse => {
IF d.mouseColor = dead THEN SIGNAL BadMouse;
d.mouseColor ← red;
d.prevPSel ← textEditor.selections[primary];
};
$SelChar => {
IF
NOT d.editState=abort
THEN {
IF d.sel=secondary AND NOT textEditor.selections[primary].valid THEN AbortSecondary[]
ELSE {
interval: PipalInt.Interval;
closerRight: BOOL;
[interval, closerRight] ← GetSelectionInterval[text, d.coords, char];
SetSelection[editor, interval, d.sel, d.pDel, closerRight, char];
IF d.editState=tolimbo OR d.sel=secondary THEN EditMessage[];
};
};
};
$SelExpand => textEditor.selections[d.sel].granularity ←
SELECT textEditor.selections[d.sel].granularity
FROM
point => char,
char => word,
ENDCASE => word;
$SelExtend => Extend[FALSE];
$SelNotPendDel =>
IF d.pdelState=reset
THEN {
d.pdelState ← not;
d.pDel ← FALSE;
};
$SelPendDel =>
IF d.pdelState=reset
THEN {
d.pdelState ← pending;
d.pDel ← TRUE;
};
$SelReduce => textEditor.selections[d.sel].granularity ← char;
$SelSecondary =>
IF d.selState=reset
THEN {
d.selState ← secondary;
d.sel ← secondary;
};
$SelStartExtend => Extend[TRUE];
$SelUpdate => {
IF
NOT d.editState=abort
THEN {
IF d.sel=secondary AND NOT textEditor.selections[primary].valid THEN AbortSecondary[]
ELSE {
grain: SelectionGrain ← textEditor.selections[d.sel].granularity;
interval: PipalInt.Interval;
closerRight: BOOL;
[interval, closerRight] ← GetSelectionInterval[text, d.coords, grain];
SetSelection[editor, interval, d.sel, d.pDel, closerRight, grain];
IF d.sel=secondary THEN EditMessage[];
};
};
};
$SelWord => {
IF
NOT d.editState=abort
THEN {
IF d.sel=secondary AND NOT textEditor.selections[primary].valid THEN AbortSecondary[]
ELSE {
interval: PipalInt.Interval;
closerRight: BOOL;
[interval, closerRight] ← GetSelectionInterval[text, d.coords, word];
SetSelection[editor, interval, d.sel, d.pDel, closerRight, word];
IF d.editState=tolimbo OR d.sel=secondary THEN EditMessage[];
};
};
};
$ToLimbo => IF d.editState=reset THEN d.editState ← tolimbo;
$ToPrimary => IF d.editState=reset OR d.editState=tolimbo THEN d.editState ← toprimary;
$YellowMouse => {
IF d.mouseColor = dead THEN SIGNAL BadMouse;
d.mouseColor ← yellow;
d.prevPSel ← textEditor.selections[primary];
};
ENDCASE => {
IO.PutF[TerminalIO.TOS[], "Unknown tip table atom %g\n", IO.atom[atom]];
someComplaint ← TRUE;
};
};
ENDCASE; ENDLOOP;
IF someComplaint THEN TerminalIO.PutRope["\n"];
ViewerOps.PaintViewer[self, all];
};
Initialization
Pipal.PutClassMethod[pipalTextClass, Pipal.describeMethod, NEW [Pipal.DescribeProc ← TextDescribe]];
Pipal.PutClassMethod[pipalTextClass, PipalReal.sizeMethod, NEW [PipalReal.SizeProc ← TextSize]];
Pipal.PutClassMethod[pipalTextClass, PipalPaint.paintMethod, NEW [PipalPaint.PaintProc ← TextPaint]];
Pipal.PutClassMethod[textEditorClass, Pipal.describeMethod, NEW [Pipal.DescribeProc ← TextEditorDescribe]];
Pipal.PutClassMethod[textEditorClass, PipalReal.sizeMethod, NEW [PipalReal.SizeProc ← TextEditorSize]];
Pipal.PutClassMethod[textEditorClass, PipalPaint.paintMethod, NEW [PipalPaint.PaintProc ← TextEditorPaint]];
toPrimaryMessages[FALSE][FALSE] ← "Select for copy to caret";
toPrimaryMessages[FALSE][TRUE] ← "Select for move to caret";
toPrimaryMessages[TRUE][FALSE] ← "Select replacement";
toPrimaryMessages[TRUE][TRUE] ← "Select for move onto";
toSecondaryMessages[FALSE][FALSE] ← "Select destination for copy";
toSecondaryMessages[FALSE][TRUE] ← "Select for replacement";
toSecondaryMessages[TRUE][FALSE] ← "Select destination for move";
toSecondaryMessages[TRUE][TRUE] ← "Select destination for move onto";