IMPLEMENTATION MODULE Menus; (****************************************************************) (* *) (* Displays menus on screen, allows terminal user to *) (* select from them. *) (* *) (* Programmer: P. Moylan *) (* Last edited: 21 February 1995 *) (* Status: OK *) (* *) (****************************************************************) FROM Storage1 IMPORT (* proc *) ALLOCATE, DEALLOCATE; FROM TaskControl IMPORT (* type *) Lock, (* proc *) CreateLock, Obtain, Release; FROM Keyboard IMPORT (* proc *) PutBack, StuffKeyboardBuffer; FROM Windows IMPORT (* const*) MaxColumnNumber, (* type *) Window, FrameType, DividerType, (* proc *) OpenWindow, CloseWindow, ChangeScrollingRegion, GetKey, WriteChar, WriteString, SetColours, Blink, SetCursor, SaveCursor, ScrollUp, ScrollDown, EraseLine, NewScrollingRegion, ResetScrollingRegion; FROM TextLines IMPORT (* type *) LineType, (* proc *) Box, HLine; FROM Mouse IMPORT (* type *) Buttons, ButtonSet, (* proc *) MouseAvailable, HideMouseCursor, ShowMouseCursor; FROM UserInterface IMPORT (* type *) UIWindow, Capability, CapabilitySet, (* proc *) AllowMouseControl, AddActiveRegion; (************************************************************************) CONST gap = 1; (* space between menu columns *) ClickIndicator = CHR(1); (* special code to indicate mouse click *) LeftOnly = ButtonSet{LeftButton}; Esc = CHR(01BH); (* keyboard Escape character *) NilWindow = Window(NIL); TYPE (* An ItemBuffer record holds the text for the screen display of *) (* one menu item. The "selpos" field shows which character acts *) (* as the selection character. *) ItemBuffer = RECORD selpos: CARDINAL; text: ItemText; END (*RECORD*); CONST MaxItems = MAX(CARDINAL) DIV SIZE(ItemBuffer); TYPE ItemNo = [0..MaxItems]; (* The following declaration uses a large upper subscript bound *) (* because Modula-2 provides no way of declaring a variable-length *) (* array (except as a procedure parameter). The correct array size *) (* will be established on a call to ALLOCATE. *) TextPointer = POINTER TO ARRAY [1..MaxItems] OF ItemBuffer; Menu = POINTER TO MenuDetails; (********************************************************************) (* *) (* The fields in a MenuDetails record have the following meaning: *) (* *) (* win The window used to display this menu *) (* on the screen *) (* foreground, The colours to use when displaying the menu *) (* background, *) (* selchar, *) (* highforeground, *) (* highbackground *) (* ScreenPosition The row and column numbers which will be *) (* occupied by the menu on the screen. This *) (* includes space for the border and title, if *) (* present. The space taken by the menu itself, *) (* not including border and title, is given by *) (* the LocationInWindow record - see below. *) (* heading The text to display in the menu header *) (* NoOfItems Number of menu items *) (* ItemsPerColumn Number of items allocated to each column *) (* NoOfColumns Number of columns of menu items *) (* hstep The increment in item number resulting from *) (* one "cursor right" operation. *) (* vstep The increment in item number resulting from *) (* one "cursor down" operation. Note that hstep *) (* and vstep depend on whether the items are *) (* numbered horizontally or vertically, and that *) (* in turn depends on whether we have a short fat *) (* display or a tall thin one. *) (* LocationInWindow: The location of the menu itself, relative to *) (* the window in which it is displayed, and not *) (* counting the space taken by borders or a *) (* header. The top left character is at location *) (* (firstrow,firstcol), the menu takes 'height' *) (* screen lines and is 'width' characters wide. *) (* To fit the text exactly into this space, we *) (* would need *) (* NoOfItems = height*NoOfColumns *) (* If NoOfItems is smaller than this, the menu *) (* will be displayed with some blank areas. If *) (* it is larger, the menu scrolls. *) (* ColumnWidth The number of characters of TextPtr^[j] which *) (* will be written on the screen. Approximately *) (* equal to LocationInWindow.width/NoOfColumns. *) (* ShowTitle Says whether to display a menu title. *) (* ShowBorder Says whether to put a border around the menu. *) (* CloseAfterSelection If TRUE, specifies that the menu window *) (* will be closed on return from SelectFromMenu. *) (* PutBackExitKey If TRUE, the keystroke that caused a return *) (* from SelectFromMenu remains available to the *) (* caller. If FALSE, that key is consumed. *) (* FastSelect If TRUE, user can select a menu item without *) (* having to confirm with Enter or Space. *) (* MouseControl If TRUE, user can move the menu with the mouse. *) (* offL, offR, What to do when the user tries to run off the *) (* offT, offB left/right/top/bottom edge of the menu. *) (* CurrentItemNo The item number currently selected. *) (* row The current menu row selected. Note that row=1 *) (* means the top row of items displayed; this is *) (* not necessarily the top row of the entire menu *) (* since some items may have scrolled off the top.*) (* column The current menu column selected. *) (* ExtraAtTop The number of rows which have disappeared off *) (* the top of the display. *) (* ExtraAtBottom The number of rows which have disappeared off *) (* the bottom of the display. *) (* RanOffEdge Set if a cursor movement ran us off the edge *) (* of the menu. *) (* TextPtr^[j] The text to display for menu item j. *) (* *) (********************************************************************) MenuDetails = RECORD (* The screen window. *) win: Window; foreground, background, selchar, highforeground, highbackground: Colour; ScreenPosition: RECORD firstrow, lastrow: RowRange; firstcol, lastcol: ColumnRange; END (*RECORD*); heading: ItemText; (* The layout of the menu within its window, determined *) (* at the time that the menu is created. *) NoOfItems: ItemNo; ItemsPerColumn: ItemNo; NoOfColumns: MenuColumn; (* Further layout details, determined at the time we *) (* discover how much space the menu will be given on *) (* the screen. *) hstep, vstep: CARDINAL; LocationInWindow: RECORD firstrow, firstcol: CARDINAL; height, width: CARDINAL; END (*RECORD*); ColumnWidth: ColumnRange; (* Options. *) ShowTitle, ShowBorder, CloseAfterSelection, PutBackExitKey, FastSelect, MouseControl: BOOLEAN; offL, offR, offT, offB: OffEdgeOption; (* Information about the current state of the menu. *) CurrentItemNo: ItemNo; row: RowRange; column: MenuColumn; ExtraAtTop, ExtraAtBottom: CARDINAL; RanOffEdge: BOOLEAN; (* Pointer to the text of the menu items. *) TextPtr: TextPointer; END (*RECORD*); (************************************************************************) VAR (* The following record contains the details of the last mouse *) (* click. Because users can't move a mouse very fast, we don't *) (* bother to keep a queue of clicks, we just record the last seen. *) (* In the event that a click arrives before the last has been *) (* consumed, the earlier click is lost. I haven't yet seen an *) (* application where that has been a major problem. *) LastMouseClick: RECORD access: Lock; win: Window; X: ColumnRange; Y: RowRange; valid: BOOLEAN; END (*RECORD*); (************************************************************************) (* MISCELLANEOUS UTILITIES *) (************************************************************************) PROCEDURE Setselpos (VAR (*INOUT*) item: ItemBuffer); (* Looks for a "\" in the item text, adjusts item.selpos if found. *) VAR j, k, high: CARDINAL; BEGIN j := 0; high := HIGH(item.text); LOOP IF (j > high) OR (ORD(item.text[j]) = 0) THEN EXIT (*LOOP*); ELSIF item.text[j] = "\" THEN item.selpos := j; FOR k := j TO high-1 DO item.text[k] := item.text[k+1]; END (*FOR*); item.text[high] := CHR(0); EXIT (*LOOP*); ELSE INC (j); END (*IF*); END (*LOOP*); END Setselpos; (************************************************************************) PROCEDURE resize (VAR (*INOUT*) text: ItemText; size: CARDINAL); (* Makes text equal to the given size, by space filling on the *) (* right and inserting a Nul to terminate the text. *) VAR j: CARDINAL; BEGIN j := 0; WHILE (j < size) AND (ORD(text[j]) <> 0) DO INC(j); END (*WHILE*); WHILE j < size DO text[j] := " "; INC(j) END (*WHILE*); IF j <= MaxColumnNumber THEN text[j] := CHR(0) END (*IF*); END resize; (************************************************************************) (* CREATING A MENU *) (************************************************************************) PROCEDURE CreateMenu (VAR (*OUT*) M: Menu; columns: MenuColumn; VAR (*IN*) Messages: ARRAY OF ItemText; NumberOfItems: CARDINAL); (* Introduces a menu into the system, but does not display it yet. *) VAR j, count: CARDINAL; BEGIN NEW (M); WITH M^ DO win := NilWindow; NoOfColumns := columns; (* Store the header text, with space fill. *) heading := Messages[0]; resize (heading, MaxColumnNumber); (* Work out how many menu items there are. *) count := NumberOfItems; IF (count = 0) OR (count > HIGH (Messages)) THEN count := HIGH (Messages); END (*IF*); NoOfItems := count; ItemsPerColumn := (count + NoOfColumns - 1) DIV NoOfColumns; CurrentItemNo := 1; (* Store the item text. *) ALLOCATE (TextPtr, NoOfItems*SIZE(ItemBuffer)); FOR j := 1 TO NoOfItems DO WITH TextPtr^[j] DO selpos := 0; text := Messages[j]; END (*WITH*); Setselpos (TextPtr^[j]); END (*FOR*); (* Set default options. *) ShowTitle := TRUE; ShowBorder := TRUE; CloseAfterSelection := TRUE; PutBackExitKey := FALSE; FastSelect := FALSE; MouseControl := MouseAvailable(); offL := stick; offR := stick; offT := stick; offB := stick; END (*WITH*); (* Give the menu a default initial position, size, and colour. *) PositionMenu (M, 0, 10, 0, MaxColumnNumber); MenuColours (M, blue, white, black, cyan, red); END CreateMenu; (************************************************************************) PROCEDURE MenuColours (M: Menu; fore, back, hfore, hback, select: Colour); (* Set the colours for the screen display of the menu. The colours *) (* fore and back are used as the normal foreground and background *) (* colours, and the highlighted menu item is displayed in colours *) (* hfore, hback. The "select" colour is for highlighting the *) (* selection character. *) BEGIN WITH M^ DO foreground := fore; background := back; highforeground := hfore; highbackground := hback; selchar := select; END (*WITH*); END MenuColours; (************************************************************************) PROCEDURE SetOptions (M: Menu; options: MO); (* See the MenuOption declaration for the possible options. *) BEGIN WITH M^ DO IF MTitle IN options THEN ShowTitle := TRUE END (*IF*); IF MNoTitle IN options THEN ShowTitle := FALSE END (*IF*); IF MBorder IN options THEN ShowBorder := TRUE END (*IF*); IF MNoBorder IN options THEN ShowBorder := FALSE END (*IF*); IF MClose IN options THEN CloseAfterSelection := TRUE END (*IF*); IF MNoClose IN options THEN CloseAfterSelection := FALSE END (*IF*); IF MKeyBack IN options THEN PutBackExitKey := TRUE END (*IF*); IF MNoKeyBack IN options THEN PutBackExitKey := FALSE END (*IF*); IF MFastSelect IN options THEN FastSelect := TRUE END (*IF*); IF MNoFastSelect IN options THEN FastSelect := FALSE END (*IF*); IF MMouse IN options THEN MouseControl := MouseAvailable() END (*IF*); IF MNoMouse IN options THEN MouseControl := FALSE END (*IF*); END (*WITH*); END SetOptions; (************************************************************************) PROCEDURE OffEdge (M: Menu; top, bottom, left, right: OffEdgeOption); (* Sets the menu behaviour when the user runs the cursor off the *) (* edge of the menu. There is one parameter for each edge of the *) (* menu. *) (* See the OffEdgeOption type declaration for the possible options. *) BEGIN WITH M^ DO offT := top; offB := bottom; offL := left; offR := right; END (*WITH*); END OffEdge; (************************************************************************) (* POSITIONING A MENU *) (************************************************************************) PROCEDURE SetRelativeLocation (M: Menu; row1, col1, rows, columns: CARDINAL); (* Gives initial values to M^.LocationInWindow and M^.ColumnWidth, *) (* and resizes the item text to the space available. Also sets *) (* M^.hstep and M^.vstep, based on the following criterion: if the *) (* display will be wider than it is tall then we use row major *) (* ordering (hstep = 1), whereas for tall narrow menus we use *) (* column major ordering (vstep = 1). This distinction is actually *) (* irrelevant to the caller, but it affects the appearance of the *) (* menu, and the decision taken here seems to give a result which *) (* someone reading the screen would consider intuitively logical. *) VAR j: ItemNo; BEGIN WITH M^ DO WITH LocationInWindow DO firstrow := row1; firstcol := col1; height := rows; width := columns; IF ItemsPerColumn <= height THEN height := ItemsPerColumn; END (*IF*); IF NoOfColumns > height THEN hstep := 1; vstep := NoOfColumns; ELSE hstep := ItemsPerColumn; vstep := 1; END (*IF*); END (*WITH*); resize (heading, columns); ColumnWidth := (columns - (NoOfColumns-1)*gap) DIV NoOfColumns; FOR j := 1 TO NoOfItems DO resize (TextPtr^[j].text, ColumnWidth); END (*FOR*); END (*WITH*); END SetRelativeLocation; (************************************************************************) PROCEDURE PositionMenu (M: Menu; startline, endline: RowRange; leftcol, rightcol: ColumnRange); (* Sets the screen location of the window which will hold the menu. *) VAR row1, col1, height, width: CARDINAL; BEGIN WITH M^ DO (* Work out the space available on the screen. *) WITH ScreenPosition DO firstrow := startline; lastrow := endline; firstcol := leftcol; lastcol := rightcol; END (*WITH*); (* How much of this space is used for the actual menu? *) row1 := 0; col1 := 0; height := endline - startline + 1; width := rightcol - leftcol + 1; IF ShowBorder THEN INC (row1); INC(col1); DEC (height, 2); DEC (width, 2); END(*IF*); IF ShowTitle THEN INC (row1,2); DEC (height, 2); END(*IF*); SetRelativeLocation (M, row1, col1, height, width); END (*WITH*); END PositionMenu; (************************************************************************) (* CLOSING A MENU *) (************************************************************************) PROCEDURE DestroyMenu (M: Menu); (* Removes a menu from the system, freeing up the space it used. *) BEGIN WITH M^ DO IF (win <> NilWindow) AND CloseAfterSelection THEN CloseWindow (win); END (*IF*); DEALLOCATE (TextPtr, NoOfItems*SIZE(ItemBuffer)); END (*WITH*); DISPOSE (M); END DestroyMenu; (************************************************************************) (* SCREEN DISPLAY *) (************************************************************************) PROCEDURE NewColours (M: Menu; fore, back, select: Colour); (* Changes the foreground and background colours of the current *) (* menu item. The "select" colour is for highlighting the *) (* selection character. *) BEGIN IF MouseAvailable() THEN HideMouseCursor END (*IF*); WITH M^ DO SetColours (win, LocationInWindow.firstrow+row-1, (column-1)*(ColumnWidth+gap) + LocationInWindow.firstcol, ColumnWidth, fore, back); IF CurrentItemNo <= NoOfItems THEN SetColours (win, LocationInWindow.firstrow+row-1, (column-1)*(ColumnWidth+gap) + LocationInWindow.firstcol + TextPtr^[CurrentItemNo].selpos, 1, select, back); END (*IF*); END (*WITH*); IF MouseAvailable() THEN ShowMouseCursor END (*IF*); END NewColours; (************************************************************************) PROCEDURE Highlight (M: Menu); (* Highlights the current menu item. *) BEGIN NewColours (M, M^.highforeground, M^.highbackground, M^.selchar); END Highlight; (************************************************************************) PROCEDURE Unhighlight (M: Menu); (* Removes any highlighting from the current menu item. *) BEGIN NewColours (M, M^.foreground, M^.background, M^.selchar); END Unhighlight; (************************************************************************) PROCEDURE RefreshRow (M: Menu); (* Refreshes the current menu row. *) VAR screenrow: RowRange; j: ColumnRange; savecurrent: ItemNo; savecolumn: CARDINAL; BEGIN WITH M^ DO savecurrent := CurrentItemNo; savecolumn := column; WITH LocationInWindow DO screenrow := firstrow + row - 1; j := LocationInWindow.firstcol; END (*WITH*); SetCursor (win, screenrow, j); EraseLine (win, 1); column := 1; CurrentItemNo := 1 + vstep*(row + ExtraAtTop - 1); LOOP WriteString (win, TextPtr^[CurrentItemNo].text); Unhighlight (M); IF (column = NoOfColumns) OR (CurrentItemNo+hstep > NoOfItems) THEN EXIT (*LOOP*); END (*IF*); INC (column); INC (CurrentItemNo, hstep); INC (j, ColumnWidth + gap); SetCursor (win, screenrow, j); END (*LOOP*); column := savecolumn; CurrentItemNo := savecurrent; END (*WITH*); END RefreshRow; (************************************************************************) PROCEDURE DisplayMOREatTop (M: Menu); BEGIN WITH M^ DO IF ShowTitle OR (ShowBorder AND NOT MouseControl) THEN WITH LocationInWindow DO SetCursor (win, firstrow-1, firstcol+width-6); END (*WITH*); WriteString (win, "*MORE*"); END (*IF*); END (*WITH*); END DisplayMOREatTop; (************************************************************************) PROCEDURE RemoveMOREatTop (M: Menu); CONST DoubleBar = '═'; SingleBar = '─'; VAR j: [1..6]; bar: CHAR; BEGIN WITH M^ DO IF ShowTitle OR (ShowBorder AND NOT MouseControl) THEN WITH LocationInWindow DO SetCursor (win, firstrow-1, firstcol+width-6); END (*WITH*); IF ShowTitle THEN bar := DoubleBar ELSE bar := SingleBar END (*IF*); FOR j := 1 TO 6 DO WriteChar (win, bar); END (*FOR*); END (*IF*); END (*WITH*); END RemoveMOREatTop; (************************************************************************) PROCEDURE DisplayMOREatBottom (M: Menu); BEGIN WITH M^ DO IF ShowBorder THEN WITH LocationInWindow DO SetCursor (win, firstrow+height, firstcol+width-6); END (*WITH*); WriteString (win, "*MORE*"); END (*IF*); END (*WITH*); END DisplayMOREatBottom; (************************************************************************) PROCEDURE RemoveMOREatBottom (M: Menu); CONST HorizontalBar = '─'; VAR j: [1..6]; BEGIN WITH M^ DO IF ShowBorder THEN WITH LocationInWindow DO SetCursor (win, firstrow+height, firstcol+width-6); END (*WITH*); FOR j := 1 TO 6 DO WriteChar (win, HorizontalBar); END (*FOR*); END (*IF*); END (*WITH*); END RemoveMOREatBottom; (************************************************************************) PROCEDURE BlinkCurrent (M: Menu); (* Toggles the blinking state of the current menu item. *) BEGIN WITH M^ DO Blink (win, LocationInWindow.firstrow+row-1, (column-1)*(ColumnWidth+gap) + LocationInWindow.firstcol, ColumnWidth); END (*WITH*); END BlinkCurrent; (************************************************************************) (* CURSOR MOVEMENTS *) (************************************************************************) PROCEDURE DownARow (M: Menu); FORWARD; (************************************************************************) PROCEDURE UpARow (M: Menu); (* Moves to the next item up, if present. Scrolls if necessary. *) BEGIN WITH M^ DO IF row > 1 THEN DEC (row); DEC (CurrentItemNo, vstep); ELSIF ExtraAtTop > 0 THEN DEC (CurrentItemNo, vstep); ScrollDown (win); DEC (ExtraAtTop); INC (ExtraAtBottom); RefreshRow (M); IF MouseAvailable() THEN HideMouseCursor END (*IF*); IF ExtraAtTop = 0 THEN RemoveMOREatTop(M) END (*IF*); IF ExtraAtBottom = 1 THEN DisplayMOREatBottom(M) END(*IF*); IF MouseAvailable() THEN ShowMouseCursor END (*IF*); ELSE CASE offT OF | stick: (* no action needed*) ; | wrap: WHILE (row < LocationInWindow.height) OR (ExtraAtBottom > 0) DO DownARow(M); END (*WHILE*); | escape: CurrentItemNo := 0; RanOffEdge := TRUE; | return: RanOffEdge := TRUE; END (*CASE*); END (*IF*); END (*WITH*); END UpARow; (************************************************************************) PROCEDURE DownARow (M: Menu); (* Moves to the next item down, if present. Scrolls if necessary. *) BEGIN WITH M^ DO IF row < LocationInWindow.height THEN INC (row); INC (CurrentItemNo, vstep); ELSIF ExtraAtBottom > 0 THEN INC (CurrentItemNo, vstep); ScrollUp (win); INC (ExtraAtTop); DEC (ExtraAtBottom); RefreshRow (M); IF MouseAvailable() THEN HideMouseCursor END (*IF*); IF ExtraAtTop = 1 THEN DisplayMOREatTop(M) END (*IF*); IF ExtraAtBottom = 0 THEN RemoveMOREatBottom(M) END (*IF*); IF MouseAvailable() THEN ShowMouseCursor END (*IF*); ELSE CASE offB OF | stick: (* no action needed*) ; | wrap: WHILE (row > 1) OR (ExtraAtTop > 0) DO UpARow (M); END (*WHILE*); | escape: CurrentItemNo := 0; RanOffEdge := TRUE; | return: RanOffEdge := TRUE; END (*CASE*); END (*IF*); END (*WITH*); END DownARow; (************************************************************************) PROCEDURE MoveRight (M: Menu); (* Moves to the next item right, if present. *) BEGIN WITH M^ DO IF column < NoOfColumns THEN INC (column); INC (CurrentItemNo, hstep); ELSE CASE offR OF | stick: (* no action needed*) ; | wrap: DEC (CurrentItemNo, hstep*(column-1)); column := 1; | escape: CurrentItemNo := 0; RanOffEdge := TRUE; | return: RanOffEdge := TRUE; END (*CASE*); END (*IF*); END (*WITH*); END MoveRight; (************************************************************************) PROCEDURE MoveLeft (M: Menu); (* Moves to the next item left, if present. *) BEGIN WITH M^ DO IF column > 1 THEN DEC (column); DEC (CurrentItemNo, hstep); ELSE CASE offL OF | stick: (* no action needed*) ; | wrap: INC (CurrentItemNo, hstep*(NoOfColumns-1)); column := NoOfColumns; | escape: CurrentItemNo := 0; RanOffEdge := TRUE; | return: RanOffEdge := TRUE; END (*CASE*); END (*IF*); END (*WITH*); END MoveLeft; (************************************************************************) PROCEDURE GotoItem (M: Menu; newitem: ItemNo); (* Moves to the menu item whose number is specified. We move a row *) (* at a time, rather than taking one big leap, since this is less *) (* disconcerting to the user. *) BEGIN WITH M^ DO IF newitem <> CurrentItemNo THEN IF vstep = 1 THEN (* we are using column major order *) column := ((newitem-1) DIV hstep) + 1; ELSE (* we are using row major order *) column := ((newitem-1) MOD vstep) + 1; END (*IF*); CurrentItemNo := (column-1)*hstep + (ExtraAtTop+row-1)*vstep + 1; WHILE CurrentItemNo > newitem DO UpARow(M) END (*WHILE*); WHILE CurrentItemNo < newitem DO DownARow(M) END (*WHILE*); END (*IF*); END (*WITH*); END GotoItem; (************************************************************************) PROCEDURE RepositionTo (M: Menu; searchchar: CHAR): BOOLEAN; (* Finds the next menu item whose selection character matches *) (* selchar, and adjusts the display appropriately. Returns TRUE if *) (* searchchar was actually found; otherwise the current menu item *) (* doesn't change and the function result is FALSE. *) VAR j: ItemNo; found: BOOLEAN; BEGIN WITH M^ DO j := CurrentItemNo; REPEAT IF j >= NoOfItems THEN j := 1 ELSE INC (j) END (*IF*); WITH TextPtr^[j] DO found := CAP(text[selpos]) = searchchar; END (*WITH*); UNTIL found OR (j = CurrentItemNo); END (*WITH*); GotoItem (M, j); RETURN found; END RepositionTo; (************************************************************************) PROCEDURE HandleFunctionKey (M: Menu; VAR (*INOUT*) option: CHAR); (* Deals with the case where the user typed a function key - i.e. *) (* any key which produces a two-code sequence where the first code *) (* is CHR(0). On entry, the CHR(0) has already been read. *) VAR count: CARDINAL; BEGIN WITH M^ DO option := GetKey(win); IF option = "H" THEN UpARow(M) (* cursor up *) ELSIF option = "P" THEN DownARow(M) (* cursor down *) ELSIF option = "M" THEN MoveRight(M) (* cursor right *) ELSIF option = "K" THEN MoveLeft(M) (* cursor left *) ELSIF option = "G" THEN (* home *) GotoItem (M, 1); ELSIF option = "O" THEN (* end *) GotoItem (M, NoOfColumns*ItemsPerColumn); GotoItem (M, NoOfItems); ELSIF option = "I" THEN (* page up *) IF row = 1 THEN IF ExtraAtTop > 0 THEN count := LocationInWindow.height; REPEAT UpARow(M); DEC (count); UNTIL (count=0) OR (ExtraAtTop=0); END (*IF*) ELSE WHILE row > 1 DO UpARow(M) END (*WHILE*) END (*IF*) ELSIF option = "Q" THEN (* page down *) IF row = LocationInWindow.height THEN IF ExtraAtBottom > 0 THEN count := LocationInWindow.height; REPEAT DownARow(M); DEC (count); UNTIL (count=0) OR (ExtraAtBottom=0); END (*IF*) ELSE WHILE row < LocationInWindow.height DO DownARow(M); END (*WHILE*) END (*IF*) END (*IF*); END (*WITH*); END HandleFunctionKey; (************************************************************************) (* DEALING WITH MOUSE CLICKS *) (************************************************************************) PROCEDURE SelectItemAt (M: Menu; r: RowRange; c: ColumnRange); VAR NewItemNo: ItemNo; OnAnItem: BOOLEAN; BEGIN WITH M^ DO WITH LocationInWindow DO DEC (r, firstrow); DEC (c, firstcol); END (*WITH*); OnAnItem := c MOD (ColumnWidth+gap) < ColumnWidth; c := c DIV (ColumnWidth+gap); IF c >= NoOfColumns THEN OnAnItem := FALSE END(*IF*); IF OnAnItem THEN (* We have now reduced (r,c) to be the coordinates of *) (* an item in the visible part of the array, with (0,0) *) (* corresponding to the top left position. *) NewItemNo := c*hstep + (ExtraAtTop+r)*vstep + 1; (* The first click on an item simply means that we *) (* should go to that item; a second click on the same *) (* item means that we should accept it as the result. *) (* If the FastSelect option is enabled, the first click *) (* will select the item as the result. *) IF FastSelect OR (NewItemNo = CurrentItemNo) THEN PutBack (" "); END (*IF*); HideMouseCursor; GotoItem (M, NewItemNo); ShowMouseCursor; END (*IF*); END (*WITH*); END SelectItemAt; (************************************************************************) PROCEDURE InterpretMouseClick (M: Menu); (* This procedure is called when we know that a mouse click has *) (* been detected and its details stored in LastMouseClick. This *) (* procedure checks whether the click is relevant to menu M, and *) (* takes the appropriate action if so. *) VAR OK: BOOLEAN; row: RowRange; column: ColumnRange; BEGIN WITH LastMouseClick DO Obtain (access); OK := valid AND (win = M^.win); IF OK THEN column := X; row := Y; END (*IF*); valid := FALSE; Release (access); END (*WITH*); IF OK THEN SelectItemAt (M, row, column); END (*IF*); END InterpretMouseClick; (************************************************************************) PROCEDURE RecordClick (w: Window; row: RowRange; col: ColumnRange); (* This procedure is called asynchronously as the result of a mouse *) (* click. The parameters tell us which window was clicked on, and *) (* where in that window the click occurred, but they don't tell us *) (* which menu is involved. Rather than work that out here, we *) (* stuff a special character into the keyboard. Procedure *) (* MakeTheSelection will pick up that special character and from *) (* that deduce that it needs to look at the LastMouseClick data. *) BEGIN WITH LastMouseClick DO Obtain (access); win := w; X := col; Y := row; valid := TRUE; Release (access); StuffKeyboardBuffer (ClickIndicator); END (*WITH*); END RecordClick; (************************************************************************) PROCEDURE ClickOnTop (w: Window; row: RowRange; col: ColumnRange); (* This procedure is triggered by a mouse click on the window *) (* divider. We turn this into a "cursor up" command. *) BEGIN StuffKeyboardBuffer (CHR(0)); StuffKeyboardBuffer ("H"); END ClickOnTop; (************************************************************************) PROCEDURE ClickOnTopMORE (w: Window; row: RowRange; col: ColumnRange); (* This procedure is triggered by a mouse click on the top *MORE* *) (* indicator. We turn this into a "page up" command. *) BEGIN StuffKeyboardBuffer (CHR(0)); StuffKeyboardBuffer ("I"); END ClickOnTopMORE; (************************************************************************) PROCEDURE ClickOnBottom (w: Window; row: RowRange; col: ColumnRange); (* This procedure is triggered by a mouse click on the bottom *) (* of the window frame. We turn this into a "cursor down" command. *) BEGIN StuffKeyboardBuffer (CHR(0)); StuffKeyboardBuffer ("P"); END ClickOnBottom; (************************************************************************) PROCEDURE ClickOnBottomMORE (w: Window; row: RowRange; col: ColumnRange); (* This procedure is triggered by a mouse click on the bottom *) (* *MORE* indicator. We turn this into a "page down" command. *) BEGIN StuffKeyboardBuffer (CHR(0)); StuffKeyboardBuffer ("Q"); END ClickOnBottomMORE; (************************************************************************) (* SCREEN DISPLAY *) (************************************************************************) PROCEDURE OpeningDisplay (M: Menu; initialvalue: CARDINAL); (* Sets up the initial state of the display of M. It is assumed *) (* that window M^.win is already open and that the position of the *) (* menu has already been set. It is also assumed that the border *) (* and title, if needed, have already been displayed. *) VAR j: RowRange; BEGIN WITH M^ DO IF (initialvalue = 0) OR (initialvalue > NoOfItems) THEN initialvalue := 1; END (*IF*); RanOffEdge := FALSE; CurrentItemNo := 1; column := 1; ExtraAtTop := 0; ExtraAtBottom := ItemsPerColumn - LocationInWindow.height; FOR j := 1 TO LocationInWindow.height DO row := j; RefreshRow (M); END (*FOR*); row := 1; IF (ExtraAtBottom > 0) THEN IF MouseAvailable() THEN HideMouseCursor END (*IF*); DisplayMOREatBottom (M); IF MouseAvailable() THEN ShowMouseCursor END (*IF*); END (*IF*); END (*WITH*); GotoItem (M, initialvalue); END OpeningDisplay; (************************************************************************) PROCEDURE DisplayMenu (w: Window; M: Menu; rows, columns, initialvalue: CARDINAL); (* Displays menu M at the current cursor position in window w, *) (* with initialvalue specifying a field to highlight. *) VAR row1, col1: CARDINAL; BEGIN SaveCursor (w, row1, col1); WITH M^ DO win := w; IF ShowBorder THEN Box (w, row1, col1, columns-1, rows-1, single); INC (row1); INC (col1); DEC (rows, 2); DEC (columns, 2); END (*IF*); IF ShowTitle THEN INC (row1, 2); DEC (rows, 2); IF ShowBorder THEN HLine (w, row1-1, col1-1, col1+columns, double); ELSE HLine (w, row1-1, col1, col1+columns-1, double); END (*IF*); END (*IF*); SetRelativeLocation (M, row1, col1, rows, columns); IF ShowTitle THEN SetCursor (w, row1-2, col1); WriteString (w, heading); END (*IF*); END (*WITH*); NewScrollingRegion (w, row1, row1+rows-1, col1, col1+columns-1); OpeningDisplay (M, initialvalue); Highlight (M); ResetScrollingRegion (w); END DisplayMenu; (************************************************************************) (* MAKING A SELECTION FROM A MENU *) (************************************************************************) PROCEDURE MakeTheSelection (M: Menu); (* Allows the keyboard user to alter the state of menu M by use of *) (* the cursor control keys, or by typing the initial letter of a *) (* menu item. Returns when Space or Enter is typed, also returns *) (* with M^.CurrentItem=0 if Esc is typed. In Special Mode only, *) (* also returns if a cursor movement key would run us off the edge *) (* of the menu. (In Normal Mode, any attempt to run off the edge *) (* is ignored.) In addition, in Special Mode the final key typed *) (* remains available (e.g. by InKey()) to the caller. *) TYPE CHARSET = SET OF CHAR; CONST CR = CHR(0DH); VAR option: CHAR; BEGIN WITH M^ DO RanOffEdge := FALSE; LOOP Highlight (M); option := GetKey (M^.win); Unhighlight (M); IF option = ClickIndicator THEN InterpretMouseClick(M); ELSIF option = " " THEN EXIT(*LOOP*) ELSIF option = CR THEN EXIT (*LOOP*) ELSIF option = Esc THEN CurrentItemNo := 0; EXIT (*LOOP*) ELSIF option IN CHARSET{"0".."9", "A".."Z", "a".."z"} THEN IF RepositionTo(M, CAP(option)) AND FastSelect THEN EXIT (*LOOP*); END (*IF*); ELSIF option = CHR(0) THEN HandleFunctionKey (M, option); IF RanOffEdge THEN IF PutBackExitKey THEN PutBack (option); option := CHR(0); END (*IF*); EXIT (*LOOP*); END (*IF*); END (*IF*); END (*LOOP*); IF PutBackExitKey THEN PutBack (option); END (*IF*); IF CurrentItemNo > NoOfItems THEN CurrentItemNo := 0; END (*IF*); END (*WITH*); END MakeTheSelection; (************************************************************************) PROCEDURE SelectFromMenu (M: Menu): CARDINAL; (* Displays menu M on the screen, allows terminal user to use *) (* cursor keys to move about the menu and the ENTER key to select *) (* an item. (The space bar is also accepted, as an alternative to *) (* the ENTER key, to select an item). An item may also be selected *) (* by typing its initial letter, followed by space or ENTER. *) (* Returns the number of the item which was selected. *) (* (Item numbers start from 1). An answer of 0 indicates that the *) (* user typed the ESC key to return without selecting anything. *) (* Remark: it is possible with the cursor keys to "select" a *) (* nonexistent item at the bottom of the last column. The result *) (* will be 0 in this case. Although this might appear to be a bug, *) (* it is deliberate. I found by experiment that the more "logical" *) (* approach of stopping the user from moving the cursor into a *) (* blank region was a nuisance for the user. *) VAR UIW: UIWindow; frame: FrameType; capabilities: CapabilitySet; framesize: CARDINAL; BEGIN WITH M^ DO (* Open the window, unless it's already open. *) IF win = NilWindow THEN framesize := 0; IF MouseAvailable() THEN HideMouseCursor END(*IF*); WITH ScreenPosition DO IF ShowBorder THEN frame := simpleframe; INC(framesize); ELSE frame := noframe END (*IF*); OpenWindow (win, foreground, background, firstrow, lastrow, firstcol, lastcol, frame, doubledivider); IF ShowTitle THEN WriteString (win, heading); ChangeScrollingRegion (win, 2+framesize, lastrow-firstrow-framesize); END (*IF*); END (*WITH*); IF MouseAvailable() THEN IF ShowBorder AND MouseControl THEN capabilities := CapabilitySet {wshow, wmove, wescape}; ELSE capabilities := CapabilitySet {wshow}; END (*IF*); UIW := AllowMouseControl (win, heading, capabilities); WITH LocationInWindow DO AddActiveRegion (UIW, firstrow, firstrow+height-1, firstcol, firstcol+width-1, LeftOnly, RecordClick); IF ShowTitle OR (ShowBorder AND NOT MouseControl) THEN AddActiveRegion (UIW, firstrow-1, firstrow-1, firstcol, firstcol+width-7, LeftOnly, ClickOnTop); AddActiveRegion (UIW, firstrow-1, firstrow-1, firstcol+width-6, firstcol+width-1, LeftOnly, ClickOnTopMORE); END (*IF*); IF ShowBorder THEN AddActiveRegion (UIW, firstrow+height, firstrow+height, firstcol, firstcol+width-7, LeftOnly, ClickOnBottom); AddActiveRegion (UIW, firstrow+height, firstrow+height, firstcol+width-6, firstcol+width-1, LeftOnly, ClickOnBottomMORE); END (*IF*); END (*WITH*); ShowMouseCursor; END (*IF*); OpeningDisplay (M, 1); END (*IF*); WITH LocationInWindow DO NewScrollingRegion (win, firstrow, firstrow+height-1, firstcol, firstcol+width-1); END (*WITH*); (* Window is now open, let the user make a selection. *) MakeTheSelection (M); IF CurrentItemNo > 0 THEN Highlight (M); END (*IF*); ResetScrollingRegion (win); (* Close the window, if appropriate. *) IF CloseAfterSelection THEN IF MouseAvailable() THEN HideMouseCursor END(*IF*); CloseWindow (win); win := NilWindow; IF MouseAvailable() THEN ShowMouseCursor END(*IF*); END (*IF*); RETURN CurrentItemNo; END (*WITH*); END SelectFromMenu; (************************************************************************) BEGIN WITH LastMouseClick DO CreateLock (access); valid := FALSE; END (*WITH*); END Menus.