-- file: IntTrackMain.mesa
-- last edited by Horning, April 15, 1981 3:17 PM
-- last edited by Brotz, November 13, 1981 12:26 PM
-- edited by Levin, 13-Nov-80 16:30:25
-- edited by Taft, May 13, 1983 10:25 AM

DIRECTORY
Ascii USING [DEL, ESC, NUL],
displayCommon USING [bitmapInMDS],
dsD: FROM "DisplayDefs" USING [ChangeCursor, ClearRectangle, GetCursor, invert,
InvertRectangle, paint, PaintPicture, PutStringInBitMap, ReplaceRectangle,
ScreenXCoord, ScreenYCoord, SetCursor],
Editor USING [cancelCode, ClearSourceSelection, Decode, DeUnderlineSelection,
nextBracketStringCode, shiftedSelectionFlag, UnderlineSelection, UnderlineType],
exD: FROM "ExceptionDefs" USING [ClearExceptionsRegion, SysBug],
inD: FROM "InteractorDefs" USING [BoundaryLineNbrPtr, BoundaryPadNbrPtr,
CommandNbrPtr, cursorX, cursorY, HousePtr, IdleLoop, KeyboardInputAcceptor,
MouseButton, mouseX, mouseY, NbrPtr, realTimeClock, RegionPtr, ScreenXCoord,
ScreenYCoord, SetBracketsCaretBlinking, SetCaretBlinking, StopBlinkingCaret,
TextSelectionPtr, ThumbLineNbrPtr, TrackerType],
intCommon USING [cmCommandNbr, cmTextNbr, commandMode, currentCommand,
currentSelection, editorType, keystream, mailCommandNbr, nextBracketDelay,
nextBracketTimeout, pendingDeleteSetByControl, regions, secondarySelectionEnabled,
source, tapTimeOut, target, tocCommandNbr],
KeyDefs USING [Keys, updown],
lmD: FROM "LaurelMenuDefs" USING [MenuChange],
Storage USING [Prune],
StreamDefs USING [StreamHandle],
vmD: FROM "VirtualMgrDefs" USING [InvalidateCaches];

IntTrackMain: PROGRAM
IMPORTS disC: displayCommon, dsD, Editor, exD, inD, intC: intCommon, lmD,
Storage, vmD
EXPORTS Editor, inD = PUBLIC

BEGIN
OPEN inD;

-- Purpose: handles user interactions including the display, keyboard and
-- mouse. This division gathers together commands and their arguments and is
-- responsible for the display of all error messages.

-- Global variables

acceptKeyboardInputStream: StreamDefs.StreamHandle ← intC.keystream;
kbdInputAcceptor: KeyboardInputAcceptor ← Editor.Decode;
modelessEditor: BOOLEAN = (intC.editorType = modeless);

-- Exported variables

keyboardInputAcceptorPtr: PUBLIC POINTER TO KeyboardInputAcceptor
← @kbdInputAcceptor;

-- Exported Procedures


AcceptKeyboardInput: PUBLIC PROCEDURE =
-- Called by cursor tracking routines when keyboard input is acceptable.
-- Dispatches to the proper command interpreter. The standard Laurel editor
-- operates in a mode such that the acceptKeyboardInputStream is identical to
-- intC.keystream. Thus, when input is present on the
-- acceptKeyboardInputStream it is also present on the intC.keystream. Other
-- command interpreters may introduce a filter between these two keystreams.
-- This procedure guarantees that input is present on the
-- acceptKeyboardInputStream; if operating in a nonstandard mode, the current
-- KeyboardInputAcceptor must funnel input from this keystream to whichever
-- keystream is used by subsequent procedures.
BEGIN
haveChar: BOOLEAN ←
~acceptKeyboardInputStream.endof[acceptKeyboardInputStream];
char: CHARACTER ←
IF haveChar THEN acceptKeyboardInputStream.get[acceptKeyboardInputStream]
ELSE Ascii.NUL;
cancelChar: BOOLEAN ← (char = Editor.cancelCode OR char = Ascii.DEL);
haveNonCancel: BOOLEAN = (haveChar AND ~cancelChar);
shiftUp: BOOLEAN = ShiftKey[up];
controlUp: BOOLEAN = ControlKey[up];
target: TextSelectionPtr = @intC.target;
source: TextSelectionPtr = @intC.source;

MonitorTaps[];

IF ControlTap[] THEN
BEGIN OPEN Editor;
selPtr: TextSelectionPtr ←
IF intC.currentSelection = source THEN source ELSE target;
underline: UnderlineType ← intC.currentSelection;
IF selPtr.mnp.editable AND selPtr.start # selPtr.end THEN
BEGIN
IF ~selPtr.pendingDelete THEN
BEGIN
DeUnderlineSelection[selPtr, underline];
selPtr.pendingDelete ← TRUE;
UnderlineSelection[selPtr, underline];
END;
IF intC.currentSelection = target THEN intC.pendingDeleteSetByControl ← TRUE;
END;
END;

-- hack to make shifted selection look like a key stroke
IF source.start # source.end
AND ((modelessEditor AND ((shiftUp AND controlUp) OR haveNonCancel))
OR (~modelessEditor AND ~intC.secondarySelectionEnabled
AND (shiftUp OR haveNonCancel))) THEN
BEGIN
IF haveChar THEN
acceptKeyboardInputStream.putback[acceptKeyboardInputStream, char];
haveChar ← TRUE;
cancelChar ← FALSE;
char ← Editor.shiftedSelectionFlag;
intC.currentSelection ← target;
END
ELSE IF target.pendingDelete AND intC.pendingDeleteSetByControl
AND ~haveChar AND controlUp AND shiftUp
THEN {haveChar ← TRUE; char ← Ascii.DEL};
IF haveChar THEN
IF cancelChar AND (~shiftUp OR ~controlUp) THEN
BEGIN
Editor.ClearSourceSelection[];
IF target.pendingDelete THEN
BEGIN
Editor.DeUnderlineSelection[target, target];
target.pendingDelete ← FALSE;
Editor.UnderlineSelection[target, target];
intC.currentSelection ← target;
intC.pendingDeleteSetByControl ← FALSE;
END;
END
ELSE kbdInputAcceptor[intC.cmTextNbr, char];
END; -- of AcceptKeyboardInput --


ScreenTracker: PUBLIC PROCEDURE [trackerType: TrackerType] =
-- Determines which cursor tracking routine should handle cursor tracking based
-- on the cursor’s current position. This procedure is the highest on any
-- calling chain. A return to ScreenTracker means that a lower level cursor
-- tracker has determined that the cursor has moved out of its jurisdiction and
-- a new lower level cursor tracker should be invoked.
BEGIN
y: ScreenYCoord;
yOffset: INTEGER;
rp: RegionPtr;
DO
ENABLE lmD.MenuChange => RESUME;
[ , , yOffset] ← dsD.GetCursor[];
y ← cursorY↑ + yOffset;
FOR rp ← intC.regions, rp.nextRegion UNTIL rp = NIL DO
IF y IN [rp.topY .. rp.bottomY) THEN GOTO foundRegion;
REPEAT
foundRegion => RegionTracker[rp, trackerType];
ENDLOOP;
IdleLoop[];
AcceptKeyboardInput[];
ENDLOOP;
END; -- of ScreenTracker --


RegionTracker: PROCEDURE [rp: RegionPtr, trackerType: TrackerType] =
-- Tracks cursor within a region. Changes cursor shape on entry, calls
-- appropriate neighborhood cursor tracker or returns to ScreenTracker if the
-- cursor leaves this region.
BEGIN
x: ScreenXCoord;
y: ScreenYCoord;
xOffset, yOffset: INTEGER;
np: NbrPtr;
dsD.ChangeCursor[rp.cursorShape];
[ , xOffset, yOffset] ← dsD.GetCursor[];
DO
x ← cursorX↑ + xOffset;
y ← cursorY↑ + yOffset;
IF ~(y IN [rp.topY .. rp.bottomY)) THEN RETURN;
FOR np ← rp.nbrs, np.nextNbr UNTIL np = NIL DO
IF (x IN [np.leftX..np.rightX)) AND (y IN [np.topY..np.bottomY))
THEN GOTO foundNbr;
REPEAT
foundNbr => BEGIN
WITH vnp: np SELECT FROM
command => vnp.nbrTracker[@vnp, trackerType];
tocText => vnp.nbrTracker[@vnp, trackerType];
messageText => vnp.nbrTracker[@vnp, trackerType];
boundaryLine => vnp.nbrTracker[@vnp];
boundaryPad => vnp.nbrTracker[@vnp, trackerType];
thumbLine => vnp.nbrTracker[@vnp, trackerType];
ENDCASE => exD.SysBug[];
dsD.ChangeCursor[rp.cursorShape];
END;
ENDLOOP;
IdleLoop[];
AcceptKeyboardInput[];
ENDLOOP;
END; -- of RegionTracker --


CommandTracker: PUBLIC PROC [cnp: CommandNbrPtr, trackerType: TrackerType] =
-- This is the nbrTracker for command neighborhoods. Sets cursor shape for
-- command neighborhood. Watches for button up and down, calls routines to
-- invert and restore command houses on screen. Calls routines in Command
-- Dept. as appropriate.
BEGIN
x: ScreenXCoord;
y: ScreenYCoord;
xOffset, yOffset: INTEGER;
hp: HousePtr;
dsD.ChangeCursor[cnp.cursorShape];
[ , xOffset, yOffset] ← dsD.GetCursor[];
DO
x ← cursorX↑ + xOffset;
y ← cursorY↑ + yOffset;
IF y ~IN [cnp.topY .. cnp.bottomY) THEN RETURN;
FOR hp ← cnp.houses, hp.nextHouse UNTIL hp = NIL DO
IF x IN [hp.leftX .. hp.rightX) AND y IN [hp.topY .. hp.bottomY)
THEN GOTO foundHouse;
REPEAT
foundHouse =>
{HouseTracker[hp, trackerType]; dsD.ChangeCursor[cnp.cursorShape]};
ENDLOOP;
IdleLoop[];
AcceptKeyboardInput[];
ENDLOOP;
END; -- of CommandTracker --


HouseTracker: PUBLIC PROCEDURE [hp: HousePtr, trackerType: TrackerType] =
-- Tracks cursor when within a command house. Watches for button down and
-- up, inverts house display pattern, and calls command procedure when fired.
BEGIN
x: ScreenXCoord;
y: ScreenYCoord;
xOffset, yOffset: INTEGER;
state: {neutral, cockedLeft, confirmCocked, nextBracketCocked, nextBracketContinuous, cockedRight} ← neutral;
nextBracketTime: CARDINAL;
menuChangeAbort: BOOLEAN ← FALSE;
keystream: StreamDefs.StreamHandle = intC.keystream;

CleanUp: PROCEDURE =
BEGIN
IF state = cockedLeft OR state = cockedRight THEN
dsD.InvertRectangle[hp.leftX, hp.rightX, hp.topY, hp.bottomY];
state ← neutral;
END; -- of CleanUp --

[ , xOffset, yOffset] ← dsD.GetCursor[];
DO
x ← cursorX↑ + xOffset;
y ← cursorY↑ + yOffset;
IF menuChangeAbort OR y ~IN [hp.topY .. hp.bottomY) OR x ~IN [hp.leftX .. hp.rightX)
THEN {CleanUp[]; RETURN};
SELECT TRUE FROM
(state = neutral AND hp.callable AND trackerType = normal
AND MouseButton[left, down]) =>
BEGIN
state ← cockedLeft;
dsD.InvertRectangle[hp.leftX, hp.rightX, hp.topY, hp.bottomY];
END;
(state = neutral AND hp.callable AND trackerType = normal
AND MouseButton[right, down] AND hp.needsConfirmation) =>
BEGIN
state ← cockedRight;
dsD.InvertRectangle[hp.leftX, hp.rightX, hp.topY, hp.bottomY];
END;
(state = neutral AND hp = intC.currentCommand AND trackerType = brackets) =>
IF MouseButton[left, down] THEN
{nextBracketTime ← realTimeClock↑; state ← nextBracketCocked}
ELSE IF MouseButton[middle, down] THEN state ← confirmCocked;
((state = cockedLeft AND MouseButton[left, up]) OR
(state = cockedRight AND MouseButton[right, up])) =>
BEGIN
dsD.InvertRectangle[hp.leftX, hp.rightX, hp.topY, hp.bottomY];
IndicateCommandBusy[hp];
IF CaretIsBlinking[] THEN StopBlinkingCaret[];
intC.currentCommand ← hp;
IF intC.source.start # intC.source.end THEN Editor.ClearSourceSelection[];
IF disC.bitmapInMDS THEN vmD.InvalidateCaches[]; -- free up VM buffers
hp.command[hp, state = cockedRight];
[] ← Storage.Prune[];
IF hp.trackerIndicateDone THEN
BEGIN
IndicateCommandFinished[hp];
IF CaretIsBlinking[] THEN
SetCaretBlinking[intC.target.point, intC.target.mnp];
END;
intC.currentCommand ← NIL;
RETURN; -- Command may have changed world significantly.
END;
(state = confirmCocked AND MouseButton[middle, up]) =>
{keystream.putback[keystream, Ascii.ESC]; RETURN};
(state = nextBracketCocked) =>
IF intC.nextBracketTimeout # 0
AND realTimeClock↑ - nextBracketTime >= intC.nextBracketTimeout THEN
BEGIN
state ← nextBracketContinuous;
nextBracketTime ← realTimeClock↑ - intC.nextBracketDelay;
END
ELSE IF MouseButton[left, up] THEN
{keystream.putback[keystream, Editor.nextBracketStringCode]; RETURN};
(state = nextBracketContinuous) =>
IF MouseButton[left, up] THEN RETURN
ELSE IF realTimeClock↑ - nextBracketTime >= intC.nextBracketDelay THEN
BEGIN
keystream.putback[keystream, Editor.nextBracketStringCode];
AcceptKeyboardInput
[ ! lmD.MenuChange => {CleanUp[]; menuChangeAbort ← TRUE; RESUME}];
nextBracketTime ← realTimeClock↑
END;
ENDCASE;
IdleLoop[];
AcceptKeyboardInput
[ ! lmD.MenuChange => {CleanUp[]; menuChangeAbort ← TRUE; RESUME}];
ENDLOOP;
END; -- of HouseTracker --


BoundaryLineNbrTracker: PUBLIC PROCEDURE [bnp: BoundaryLineNbrPtr] =
-- Tracks cursor within a boundary line neighborhood.
BEGIN
x: ScreenXCoord;
y: ScreenYCoord;
xOffset, yOffset: INTEGER;
dsD.ChangeCursor[bnp.cursorShape];
[ , xOffset, yOffset] ← dsD.GetCursor[];
DO
x ← cursorX↑ + xOffset;
y ← cursorY↑ + yOffset;
IF y ~IN [bnp.topY .. bnp.bottomY) OR x ~IN [bnp.leftX .. bnp.rightX)
THEN RETURN;
IdleLoop[];
AcceptKeyboardInput[];
ENDLOOP;
END; -- of BoundaryLineNbrTracker --


BoundaryPadNbrTracker: PUBLIC PROCEDURE
[bnp: BoundaryPadNbrPtr, trackerType: TrackerType]=
-- Tracks cursor within a boundary pad neighborhood.
BEGIN
state: {up, down} ← up;
x: ScreenXCoord;
y: ScreenYCoord;
xOffset, yOffset: INTEGER;
dsD.ChangeCursor[charArrow];
[ , xOffset, yOffset] ← dsD.GetCursor[];
DO
x ← cursorX↑ + xOffset;
y ← cursorY↑ + yOffset;
SELECT state FROM
up => BEGIN
IF y ~IN [bnp.topY .. bnp.bottomY) OR x ~IN [bnp.leftX .. bnp.rightX)
THEN RETURN;
IF MouseButton[middle, down] THEN
BEGIN
cursorX↑ ← mouseX↑ ← bnp.leftX;
cursorY↑ ← mouseY↑ ← bnp.topY;
dsD.SetCursor[boundaryPad];
[ , xOffset, yOffset] ← dsD.GetCursor[];
dsD.PaintPicture[bnp.leftX, bnp.topY, boundaryPad, dsD.invert];
state ← down;
END;
END;
down => BEGIN
IF x + 150 < bnp.leftX THEN
BEGIN
dsD.ChangeCursor[charArrow];
dsD.PaintPicture[bnp.leftX, bnp.topY, boundaryPad, dsD.invert];
RETURN;
END;
IF MouseButton[middle, up] THEN
BEGIN
dsD.ChangeCursor[invisibleCursor];
StopBlinkingCaret[];
bnp.command[bnp, y];
IF trackerType = normal AND CaretIsBlinking[] THEN
SetCaretBlinking[intC.target.point, intC.target.mnp];
IF trackerType = brackets THEN SetBracketsCaretBlinking[];
--##cursorX↑ ← mouseX↑ ← bnp.leftX + 8;
--##cursorY↑ ← mouseY↑ ← bnp.topY + 2;
dsD.ChangeCursor[charArrow];
RETURN;
END;
END;
ENDCASE;
IdleLoop[];
AcceptKeyboardInput[];
ENDLOOP;
END; -- of BoundaryPadNbrTracker --


ThumbLineNbrTracker: PUBLIC PROCEDURE
[tlnp: ThumbLineNbrPtr, trackerType: TrackerType] =
-- Tracks cursor within a Thumb Line neighborhood.
BEGIN
state: {up, down} ← up;
x: ScreenXCoord;
y: ScreenYCoord;
xOffset, yOffset: INTEGER;

dsD.ChangeCursor[charArrow];
[ , xOffset, yOffset] ← dsD.GetCursor[];
DO
x ← cursorX↑ + xOffset;
y ← cursorY↑ + yOffset;
IF y ~IN [tlnp.topY .. tlnp.bottomY) OR x + 5 ~IN [tlnp.leftX .. tlnp.rightX + 10)
THEN {dsD.ChangeCursor[charArrow]; RETURN};
SELECT state FROM
up =>
IF MouseButton[middle, down] THEN
BEGIN
dsD.ChangeCursor[thumbMarker];
[ , xOffset, yOffset] ← dsD.GetCursor[];
state ← down;
END;
down =>
IF MouseButton[middle, up] THEN
BEGIN
IF CaretIsBlinking[] AND trackerType = normal
THEN StopBlinkingCaret[];
tlnp.command[tlnp, tlnp.np, IF x < tlnp.leftX THEN tlnp.leftX
ELSE IF x > tlnp.rightX THEN tlnp.rightX ELSE x];
IF CaretIsBlinking[] AND trackerType = normal THEN
SetCaretBlinking[intC.target.point, intC.target.mnp];
dsD.ChangeCursor[charArrow];
RETURN;
END;
ENDCASE;
IdleLoop[];
AcceptKeyboardInput[];
ENDLOOP;
END; -- of ThumbLineNbrTracker --


CaretIsBlinking: PUBLIC PROCEDURE RETURNS [BOOLEAN] =
-- Indicates whether the editor is in a state such that the caret should be
-- blinking.
BEGIN
RETURN[intC.cmTextNbr.haveMessage
AND (modelessEditor OR ~intC.commandMode)];
END; -- of CaretIsBlinking --


IndicateCommandBusy: PUBLIC PROCEDURE [hp: HousePtr] =
-- Shows that a command is in progress, by: displaying the command house in
-- gray and changing the cursor shape.
BEGIN
dsD.ChangeCursor[hourGlass];
exD.ClearExceptionsRegion[];
dsD.ReplaceRectangle[hp.leftX, hp.rightX, hp.topY, hp.bottomY, lightGray];
IF hp.usePicture THEN dsD.PaintPicture[hp.leftX, hp.topY, hp.picture, dsD.paint]
ELSE [] ← dsD.PutStringInBitMap[hp.leftX, hp.topY, hp.text, boldFace];
END; -- of IndicateCommandBusy --


IndicateCommandFinished: PUBLIC PROCEDURE [hp: HousePtr] =
-- Shows that a command is finished, by: displaying the command house in
-- white.
BEGIN
dsD.ClearRectangle[hp.leftX, hp.rightX, hp.topY, hp.bottomY];
IF hp.usePicture THEN dsD.PaintPicture[hp.leftX, hp.topY, hp.picture, dsD.paint]
ELSE [] ← dsD.PutStringInBitMap[hp.leftX, hp.topY, hp.text, hp.typeface];
END; -- of IndicateCommandFinished --


MakeCommandsCallable: PUBLIC PROCEDURE [callable: BOOLEAN] =
-- Sets the callable property of all commands currently on screen to be the value
-- of the input parameter.
BEGIN

IterateOverHouseList: PROCEDURE [house: HousePtr] =
BEGIN
UNTIL house = NIL DO
IF house.typeface = boldFace THEN house.callable ← callable;
house ← house.nextHouse;
ENDLOOP;
END; -- of IterateOverHouseList --

IterateOverHouseList[intC.mailCommandNbr.houses];
IterateOverHouseList[intC.tocCommandNbr.houses];
IterateOverHouseList[intC.cmCommandNbr.houses];
END; -- of MakeCommandsCallable --


TapState: TYPE = {down, notPossibleTapDown, possibleTapDown, up};

controlTapState: TapState ← up;

controlTap: BOOLEAN ← FALSE;

controlDown: CARDINAL;


MonitorTaps: PROCEDURE =
-- To be called continuously. Maintains Control and key states and records
-- Control taps.
BEGIN
controlUp: BOOLEAN ← ControlKey[up];
IF ~modelessEditor THEN RETURN;
SELECT controlTapState FROM
down =>
IF controlUp THEN controlTapState ← up
ELSE IF ShiftKey[down] OR MouseButton[any, down] THEN
{controlTapState ← possibleTapDown; controlDown ← realTimeClock↑};
notPossibleTapDown => IF controlUp THEN controlTapState ← up;
possibleTapDown =>
IF controlUp THEN
BEGIN
controlTapState ← up;
IF realTimeClock↑ - controlDown < intC.tapTimeOut THEN
controlTap ← TRUE;
END;
up => IF ~controlUp THEN
IF ShiftKey[down] OR MouseButton[any, down] THEN
{controlTapState ← possibleTapDown; controlDown ← realTimeClock↑}
ELSE controlTapState ← down;
ENDCASE;
END; -- of MonitorTaps --


ResetTaps: PROCEDURE =
-- To be called when any mouse buttons go down in selection areas or on a
-- SHIFT-CANCEL.
BEGIN
controlTap ← FALSE;
controlTapState ← IF ControlKey[up] THEN up ELSE notPossibleTapDown;
END; -- of ResetTaps --


ControlTap: PUBLIC PROCEDURE RETURNS [yes: BOOLEAN] =
-- Returns TRUE iff a Control tap has occurred. Upon return of TRUE, Control
-- tap is reset.
BEGIN
IF (yes ← controlTap) THEN controlTap ← FALSE;
END; -- of ControlTap --


ShiftKey: PUBLIC PROCEDURE [state: KeyDefs.updown] RETURNS [yes: BOOLEAN] =
BEGIN OPEN KeyDefs;
SELECT state FROM
up => RETURN[Keys.LeftShift = up AND Keys.RightShift = up];
down => RETURN[Keys.LeftShift = down OR Keys.RightShift = down];
ENDCASE;
END; -- of ShiftKey --


ComKey: PUBLIC PROCEDURE [state: KeyDefs.updown] RETURNS [yes: BOOLEAN] =
BEGIN OPEN KeyDefs;
up: BOOLEAN = (Keys.Spare3 = up AND Keys.FL4 = up AND Keys.FR5 = up);
RETURN[IF state = up THEN up ELSE ~up];
END; -- of ComKey --


ControlKey: PUBLIC PROCEDURE [state: KeyDefs.updown] RETURNS [yes: BOOLEAN] =
BEGIN
RETURN[KeyDefs.Keys.Ctrl = state];
END; -- of ControlKey --


END. -- of IntTrackMain --