MenusImpl.mesa; Written by S. McGregor
Edited by McGregor on August 4, 1983 11:22 am
Last Edited by: Maxwell, May 24, 1983 7:48 am
Last Edited by: Pausch, August 25, 1983 10:29 am
Last Edited by: Wyatt, November 14, 1983 1:49 pm
DIRECTORY
Imager USING [black, Color, Context, IntegerMaskRectangle, IntegerSetXY, MakeStipple, SetColor, ShowCharacters, white],
Menus,
MenusPrivate,
MessageWindow USING [Append],
Process USING [Detach, Milliseconds, MsecToTicks, priorityNormal, SetPriority, SetTimeout],
Real USING [RoundC],
Rope USING [ROPE],
TIPUser USING [TIPScreenCoords],
UserProfile USING [Boolean, CallWhenProfileChanges, Number, ProfileChangedProc],
VFonts USING [defaultFont, FONT, FontAscent, FontHeight, RopeWidth],
ViewerBLT USING [ChangeHeader],
ViewerClasses,
ViewerOps USING [HelpViewer, InvertForMenus, NotifyViewer, PaintViewer],
ViewerSpecs USING [captionHeight, guardOffset, guardHeight, menuBarHeight, menuHeight, windowBorderSize],
WindowManager USING [RestoreCursor],
WindowManagerPrivate USING [DrawCaptionMenu, ProcessWindowResults, windowMenu];
MenusImpl: CEDAR MONITOR LOCKS entry USING entry: EntryInfo
IMPORTS Imager, MessageWindow, Process, Real, UserProfile, VFonts, ViewerBLT, ViewerOps, WindowManager, WindowManagerPrivate
EXPORTS Menus, MenusPrivate
SHARES ViewerOps
= BEGIN OPEN Menus, MenusPrivate;
Viewer: TYPE = ViewerClasses.Viewer;
ROPE: TYPE = Rope.ROPE;
menuFont: VFonts.FONT ← VFonts.defaultFont;
fontHeight: INTEGER;
baselineOffset: INTEGER;
SetMenu:
PUBLIC
PROC[viewer: Viewer, menu: Menu, paint:
BOOL ←
TRUE] = {
viewer.menu ← MakeMenu[menu];
};
MakeEntry:
PUBLIC
PROC[entry: Entry]
RETURNS[EntryInfo] = {
RETURN[
NEW[MenusPrivate.EntryInfoRec ← [name: entry.name, actions: entry.actions,
guarded: entry.guarded, state: IF entry.guarded THEN guarded ELSE armed]]];
};
MakeMenu:
PROC[menu: Menu]
RETURNS[MenuInfo] = {
CopyEntries:
PROC[list:
LIST
OF Entry]
RETURNS[
LIST
OF EntryInfo] = {
RETURN[IF list=NIL THEN NIL ELSE CONS[MakeEntry[list.first], CopyEntries[list.rest]]];
};
MakeLine:
PROC[line: Line]
RETURNS[LineInfo] = {
RETURN[
NEW[LineInfoRec ← [name: line.name,
entries: CopyEntries[line.entries], active: line.active]]];
};
CopyLines:
PROC[list:
LIST
OF Line]
RETURNS[
LIST
OF LineInfo] = {
RETURN[IF list=NIL THEN NIL ELSE CONS[MakeLine[list.first], CopyLines[list.rest]]];
};
RETURN[NEW[MenuInfoRec ← [lines: CopyLines[menu]]]];
};
widthFudge: INTEGER = 3;
MarkMenu:
PUBLIC
PROC[menu: MenuInfo, parent: Viewer,
mousePos: TIPUser.TIPScreenCoords] = {
entry: EntryInfo = ResolveEntry[menu, mousePos];
IF menu.inverted#entry
THEN {
IF menu.inverted#NIL THEN InvertEntry[menu, parent];
menu.inverted ← entry;
IF menu.inverted#NIL THEN InvertEntry[menu, parent];
};
};
HitMenu:
PUBLIC
PROC[menu: MenuInfo, parent: Viewer,
mousePos: TIPUser.TIPScreenCoords, trigger: Trigger] = {
entry: EntryInfo = ResolveEntry[menu, mousePos];
hit: BOOL = (menu.inverted#NIL AND entry=menu.inverted);
IF menu.inverted#NIL THEN InvertEntry[menu, parent]; -- take down
menu.inverted ← NIL;
IF hit THEN ProcessMenuHit[entry, menu, parent, trigger];
};
ProcessMenuHit:
ENTRY
PROC[entry: EntryInfo,
menus: MenuInfo, parent: Viewer, trigger: Trigger] = {
SELECT entry.state
FROM
guarded => {
entry.state ← arming;
TRUSTED {Process.Detach[FORK ArmMenuProc[entry, menus, parent]]};
TRUSTED {Process.Detach[FORK GuardResponse[parent, ChooseAction[entry, trigger]]]};
};
arming=> NULL; -- no action
armed => {
IF entry.guarded THEN entry.state ← guarded;
MenuPusher[entry, menus, parent, trigger, FALSE];
};
ENDCASE;
};
ArmMenuProc:
ENTRY
PROC[entry: EntryInfo, menus: MenuInfo, parent: Viewer] = {
menuWaitCondition: CONDITION;
assert: state=arming
TRUSTED {Process.SetTimeout[@menuWaitCondition, Process.MsecToTicks[armingTime]]};
WAIT menuWaitCondition;
IF entry.state = arming
THEN {
entry.state ← armed;
RedrawMenu[parent, menus, entry];
TRUSTED {Process.SetTimeout[@menuWaitCondition, Process.MsecToTicks[armedTime]]};
WAIT menuWaitCondition;
};
IF entry.state#guarded
THEN {
entry.state ← guarded;
RedrawMenu[parent, menus, entry];
};
};
GuardResponse:
PUBLIC
PROC[viewer: Viewer, action: Action] = {
IF ViewerOps.HelpViewer[viewer, action.input] THEN NULL
ELSE MessageWindow.Append[action.doc, TRUE];
};
MenuPusher:
PROC[entry: EntryInfo, menu: MenuInfo, parent: Viewer, trigger: Trigger,
normalPriority: BOOL ← TRUE] = {
action: Action = ChooseAction[entry, trigger];
entry.greyCount ← entry.greyCount+1;
IF entry.greyCount=1 THEN RedrawMenu[parent, menu, entry];
IF normalPriority THEN TRUSTED {Process.SetPriority[Process.priorityNormal]};
IF menu=WindowManagerPrivate.windowMenu
THEN
[] ← WindowManagerPrivate.ProcessWindowResults[parent, action.input]
ELSE ViewerOps.NotifyViewer[parent, action.input];
entry.greyCount ← entry.greyCount-1;
IF entry.greyCount=0 THEN RedrawMenu[parent, menu, entry];
WindowManager.RestoreCursor[];
};
RedrawMenu:
PROC[viewer: Viewer, menu: MenuInfo, entry: EntryInfo] = {
IF menu=WindowManagerPrivate.windowMenu
THEN
WindowManagerPrivate.DrawCaptionMenu[viewer, FALSE]
ELSE ViewerOps.PaintViewer[viewer: viewer, hint: menu, whatChanged: entry];
};
ClearMenu:
PUBLIC
PROC[menu: MenuInfo, parent: Viewer, paint:
BOOL ←
TRUE] = {
IF menu.inverted#
NIL
THEN {
IF paint THEN InvertEntry[menu, parent];
menu.inverted ← NIL;
};
};
myGrey: Imager.Color = Imager.MakeStipple[001010B];
DrawSingleEntry:
PROC[viewer: Viewer, context: Imager.Context, entry: EntryInfo,
clearFirst: BOOLEAN ← TRUE] = {
x: INTEGER = viewer.wx + entry.x;
y: INTEGER = viewer.wy + entry.y;
w: INTEGER = entry.w;
IF clearFirst
THEN {
Imager.SetColor[context, Imager.white];
Imager.IntegerMaskRectangle[context, x-widthFudge, y, w+(2*widthFudge), fontHeight-1];
};
IF entry.greyCount>0
THEN {
Imager.SetColor[context, myGrey];
Imager.IntegerMaskRectangle[context, x-widthFudge, y, w+(2*widthFudge), fontHeight-1];
};
Imager.SetColor[context, Imager.black];
Imager.IntegerSetXY[context, x, y+baselineOffset];
Imager.ShowCharacters[context, entry.name, VFonts.defaultFont];
IF entry.guarded
AND entry.state#armed
THEN {
OPEN ViewerSpecs;
Imager.IntegerMaskRectangle[context, x-1, y+baselineOffset+guardOffset, w+2, guardHeight];
};
};
DrawMenu:
PUBLIC
PROC[v: Viewer, menu: MenuInfo,
context: Imager.Context, whatChanged: REF ANY ← NIL] = {
Note: the menu parameter might be the windowManager caption menu rather than v.menu.
WITH whatChanged
SELECT
FROM
entry: EntryInfo => { DrawSingleEntry[v, context, entry]; RETURN };
ENDCASE;
FOR lines:
LIST
OF LineInfo ← menu.lines, lines.rest
UNTIL lines=
NIL
DO
line: LineInfo = lines.first;
IF NOT line.active THEN LOOP;
FOR entries:
LIST
OF EntryInfo ← line.entries, entries.rest
UNTIL entries=
NIL
DO
DrawSingleEntry[v, context, entries.first, FALSE];
ENDLOOP;
ENDLOOP;
menu.inverted ← NIL;
};
ResolveEntry has to go FAST, so we cache the last entry. In order for this cache to be legit, we have to save a pointer to the cached entry, the line that contains the entry, and the menu that contains the line. If we just tested against the entry, we could get fooled by an entry in a menu that had been made inactive.
cacheMenu: MenuInfo ← NIL;
cacheLine: LineInfo ← NIL;
cacheEntry: EntryInfo ← NIL;
ResolveEntry:
PROC[menu: MenuInfo, mousePos: TIPUser.TIPScreenCoords]
RETURNS[EntryInfo] = {
x: INTEGER = mousePos.mouseX;
y: INTEGER = mousePos.mouseY;
Cache test:
IF cacheMenu=menu
AND cacheLine.active
AND x IN[cacheEntry.x..cacheEntry.x+cacheEntry.w)
AND y
IN[cacheEntry.y..cacheEntry.y+fontHeight)
THEN RETURN[cacheEntry];
if cache fails, we test everybody.
FOR lines:
LIST
OF LineInfo ← menu.lines, lines.rest
UNTIL lines=
NIL
DO
line: LineInfo = lines.first;
IF NOT line.active THEN LOOP;
FOR entries:
LIST
OF EntryInfo ← line.entries, entries.rest
UNTIL entries=
NIL
DO
entry: EntryInfo = entries.first;
IF x
IN[entry.x..entry.x+entry.w)
AND y
IN[entry.y..entry.y+fontHeight)
THEN {
cacheMenu ← menu; cacheLine ← line; cacheEntry ← entry;
RETURN[entry] };
ENDLOOP;
ENDLOOP;
RETURN[NIL];
};
InvertEntry:
PROC[menus: MenuInfo, parent: Viewer] = {
IF
NOT(parent.destroyed
OR parent.iconic)
THEN {
entry: EntryInfo ← menus.inverted;
ViewerOps.InvertForMenus[parent, (parent.wx+entry.x)-widthFudge, parent.wy+entry.y, entry.w+(2*widthFudge), fontHeight-1];
};
};
ComputeFontInfo:
PROC = {
ascent: INTEGER = VFonts.FontAscent[menuFont];
fontHeight ← VFonts.FontHeight[menuFont];
baselineOffset ← Real.RoundC[fontHeight-ascent];
};
ChooseAction:
PROC[entry: EntryInfo, trigger: Trigger]
RETURNS[Action] = {
list: LIST OF Action ← entry.actions;
count: NAT ← 0;
IF list=NIL THEN RETURN[[NIL, NIL, NIL]];
WHILE count<trigger AND list.rest#NIL DO list ← list.rest; count ← count+1 ENDLOOP;
RETURN[list.first];
};
ReRegisterMenu: PUBLIC PROC [menu: Menu, paint: BOOL ← TRUE] = {
newMenu: ViewerMenuRec;
MenuDefinitonChanged: ViewerOps.EnumProc = {
-- [v: Viewer] RETURNS [BOOL ← TRUE] --
menus: MenuInfo = NARROW[v.menus];
FOR m: LIST OF LineInfo ← menus.list, m.rest UNTIL m = NIL DO
IF menu.name=m.first.commonData.name THEN m.first ← NEW[ViewerMenuRec ← newMenu];
ENDLOOP;
IF paint THEN RePaintBecauseOfMenuChange[v];
};
ValidateMenu[menu];
FOR m: LIST OF Menu ← registeredMenus, m.rest UNTIL m = NIL DO
IF m.first.name=menu.name THEN {
m.first ← menu;
newMenu ← MakeNewViewerMenuRec[m.first];
ViewerOps.EnumerateViewers[MenuDefinitonChanged];
RETURN;
};
ENDLOOP;
ERROR targetNotFound;
};
GetRegisteredMenu: PUBLIC PROC[name: ATOM] RETURNS [Menu] = {
FOR m: LIST OF Menu ← registeredMenus, m.rest UNTIL m = NIL DO
menu: Menu = m.first;
IF menu.name=name THEN RETURN[menu];
ENDLOOP;
ERROR targetNotFound;
};
ClearMenus: PUBLIC PROC [viewer: Viewer, paint: BOOL ← TRUE] = {
viewer.menus ← NIL;
IF paint THEN RePaintBecauseOfMenuChange[viewer];
};
Addline: PUBLIC PROC[viewer: Viewer, name: ATOM, paint: BOOL ← TRUE, addBefore: ATOM ← NIL] = {
menus: MenuInfo = NARROW[viewer.menus];
IF menus = NIL THEN {
IF addBefore # NIL THEN SIGNAL targetNotFound;
viewer.menus ← NEW[ViewerMenusRec ← [list: LIST[MakeNewViewerMenu[name]]]];
}
ELSE IF addBefore # NIL THEN {
place in the slot before 'addBefore'
prev: LIST OF LineInfo ← NIL;
foundIt: BOOLEAN ← FALSE;
FOR m: LIST OF LineInfo ← menus.list, m.rest UNTIL m = NIL DO
IF m.first.commonData.name=name THEN {
prev.rest ← CONS[MakeNewViewerMenu[name], m];
foundIt ← TRUE;
};
prev ← m;
ENDLOOP;
IF NOT foundIt THEN SIGNAL targetNotFound;
}
ELSE IF GetRegisteredMenu[name].breakBefore THEN {
place at the end of the list if it wants its own separate line.
prev: LIST OF LineInfo ← NIL;
FOR m: LIST OF LineInfo ← menus.list, m.rest UNTIL m = NIL DO
prev ← m;
ENDLOOP;
prev.rest ← LIST[MakeNewViewerMenu[name]];
}
ELSE {
place it at the first place without a 'breakAfter' that has a 'breakBefore' following it
prev: LIST OF LineInfo ← NIL;
FOR m: LIST OF LineInfo ← menus.list, m.rest UNTIL m = NIL OR ((prev # NIL AND prev.first.commonData.breakAfter = FALSE) AND m.first.commonData.breakBefore) DO
prev ← m;
ENDLOOP;
prev.rest ← CONS[MakeNewViewerMenu[name],prev.rest];
};
IF paint THEN RePaintBecauseOfMenuChange[viewer];
};
ViewerlessAddMenu: PUBLIC PROC [name: ATOM, addBefore: ATOM ← NIL, paint: BOOL ← TRUE] = {
provided for convenience of BuildWindowMenus in WindowManagerImpl
IF WindowManagerPrivate.windowMenu = NIL THEN {
IF addBefore # NIL THEN SIGNAL targetNotFound;
WindowManagerPrivate.windowMenu ← NEW[ViewerMenusRec];
WindowManagerPrivate.windowMenu.list ← LIST[MakeNewViewerMenu[name]];
}
ELSE IF addBefore # NIL THEN {
place in the slot before 'addBefore'
prev: LIST OF LineInfo ← NIL;
foundIt: BOOLEAN ← FALSE;
FOR m: LIST OF LineInfo ← WindowManagerPrivate.windowMenu.list, m.rest UNTIL m = NIL DO
IF m.first.commonData.name=name THEN {
prev.rest ← CONS[MakeNewViewerMenu[name], m];
foundIt ← TRUE;
};
prev ← m;
ENDLOOP;
IF NOT foundIt THEN SIGNAL targetNotFound;
}
ELSE IF GetRegisteredMenu[name].breakBefore THEN {
place at the end of the list if it wants its own separate line.
prev: LIST OF LineInfo ← NIL;
FOR m: LIST OF LineInfo ← WindowManagerPrivate.windowMenu.list, m.rest UNTIL m = NIL DO
prev ← m;
ENDLOOP;
prev.rest ← LIST[MakeNewViewerMenu[name]];
}
ELSE {
place it at the first place without a 'breakAfter' that has a 'breakBefore' following it
prev: LIST OF LineInfo ← NIL;
FOR m: LIST OF LineInfo ← WindowManagerPrivate.windowMenu.list, m.rest UNTIL m = NIL OR ((prev # NIL AND prev.first.commonData.breakAfter = FALSE) AND m.first.commonData.breakBefore) DO
prev ← m;
ENDLOOP;
prev.rest ← CONS[MakeNewViewerMenu[name],prev.rest];
};
};
ChangeLine:
PUBLIC
PROC[viewer: Viewer, name:
ATOM, change: Change] = {
menu: MenuInfo = NARROW[viewer.menu];
IF menu=NIL THEN RETURN;
FOR lines:
LIST
OF LineInfo ← menu.lines, lines.rest
UNTIL lines=
NIL
DO
line: LineInfo = lines.first;
wasActive: BOOL = line.active;
IF line.name=name
THEN {
SELECT change
FROM
show => line.active ← TRUE;
hide => line.active ← FALSE;
toggle => line.active ← NOT line.active;
ENDCASE;
IF line.active#wasActive THEN ViewerBLT.ChangeHeader[viewer];
RETURN;
};
ENDLOOP;
};
LineInViewer:
PUBLIC
PROC[viewer: Viewer, name:
ATOM]
RETURNS[exists:
BOOLEAN] = {
WITH viewer.menu
SELECT
FROM
menu: MenuInfo => {
FOR lines:
LIST
OF LineInfo ← menu.lines, lines.rest
UNTIL lines=
NIL
DO
line: LineInfo = lines.first;
IF line.name=name THEN RETURN[TRUE];
ENDLOOP;
};
ENDCASE => IF viewer.menu#NIL THEN ERROR;
RETURN[FALSE];
};
The user-profilable menu options:
wrap: BOOLEAN ← TRUE; -- TRUE iff user wants menu entries ALWAYS to be displayed
wrapIndent: INTEGER ← (menuHLeading*4); -- indentation on non-first lines of wrapped menu
tooNarrowToWrapMenus: INTEGER ← 150; -- don't wrap if the window is ridiculously narrow
This routine is called whenever the user profile changes:
ReEstablishUserProfileParameters:
PUBLIC UserProfile.ProfileChangedProc = {
wrap ← UserProfile.Boolean[key: "ViewerMenusWrap", default: TRUE];
wrapIndent ← UserProfile.Number[key: "ViewerMenusWrapIndent", default: (menuHLeading*4)];
tooNarrowToWrapMenus ← UserProfile.Number["ViewerMenusTooNarrowToWrap", 150];
};
EstablishHeader:
PUBLIC
PROC[v: Viewer]
RETURNS[y:
INTEGER] = {
OPEN ViewerSpecs;
vbs: INTEGER = IF v.border THEN windowBorderSize ELSE 0;
ch: INTEGER = IF v.parent=NIL AND v.column#static THEN captionHeight ELSE vbs;
WITH v.menu
SELECT
FROM
menu: MenuInfo => {
RecomputeMenu[menu: menu, xmin: vbs, xmax: v.ww-vbs, ymax: v.wh-ch];
RETURN[menu.y-menuBarHeight];
};
ENDCASE => IF v.menu=NIL THEN RETURN[v.wh-ch] ELSE ERROR;
};
RecomputeMenu:
PROC[menu: MenuInfo, xmin, xmax, ymax:
INTEGER] = {
OPEN ViewerSpecs;
Note: all coordinates here are relative to the viewer's lower left corner!
wrappingOK: BOOL ← (wrap AND (xmax-xmin)>tooNarrowToWrapMenus);
y: INTEGER ← ymax;
FOR lines:
LIST
OF LineInfo ← menu.lines, lines.rest
UNTIL lines=
NIL
DO
line: LineInfo = lines.first;
x: INTEGER ← xmin + menuHLeading;
IF NOT line.active THEN LOOP;
y ← y - menuHeight; -- begin new line
FOR e:
LIST
OF EntryInfo ← line.entries, e.rest
UNTIL e =
NIL
DO
entry: EntryInfo = e.first;
w: INTEGER = VFonts.RopeWidth[entry.name];
IF (x+w)>xmax
AND wrappingOK
THEN {
x ← xmin+wrapIndent; y ← y-menuHeight; -- move down a line and indent
};
entry.x ← x; entry.y ← y; entry.w ← w;
x ← x + w + menuHSpace;
ENDLOOP;
ENDLOOP;
menu.x ← xmin; menu.y ← y; menu.w ← xmax-xmin; menu.h ← ymax-y;
};
ReComputeWindowMenus: PUBLIC PROC [v: Viewer, guard: BOOL, color: BOOL] = {
OPEN ViewerSpecs;
menus coordinates are computed and stored RELATIVE to the viewer
CurrentX: INTEGER ← menuHLeading + windowBorderSize;
CurrentY: INTEGER ~ v.wh - captionHeight; -- all have same Y coordinate
WindowManagerPrivate.windowMenu.x ← windowBorderSize;
WindowManagerPrivate.windowMenu.y ← v.wh - captionHeight;
WindowManagerPrivate.windowMenu.w ← v.ww - (2 * windowBorderSize);
WindowManagerPrivate.windowMenu.h ← captionHeight;
IF guard THEN {
SetDisplay[$windowDestroyMenu, FALSE];
SetDisplay[$windowGuardedDestroyMenu, TRUE];
}
ELSE {
SetDisplay[$windowDestroyMenu, TRUE];
SetDisplay[$windowGuardedDestroyMenu, FALSE];
};
IF color THEN SetDisplay[$windowColorMenu, TRUE]
ELSE SetDisplay[$windowColorMenu, FALSE];
FOR m: LIST OF LineInfo ← WindowManagerPrivate.windowMenu.list, m.rest UNTIL m = NIL DO
IF NOT m.first.active THEN LOOP;
FOR e: LIST OF EntryInfo ← m.first.entries, e.rest UNTIL e = NIL DO
e.first.w ← WITH e.first.commonData.displayData SELECT FROM
r: ROPE => VFonts.RopeWidth[r],
user: REF DrawingRec => user.proc[op: query, clientData: user.data]
ENDCASE => ERROR;
e.first.x ← CurrentX;
e.first.y ← CurrentY;
hack for special clicking GROW and CLOSE
IF Rope.Equal[e.first.commonData.name, "Grow"] THEN {
WindowManagerPrivate.growPos.mouseX ← e.first.x+1;
WindowManagerPrivate.growPos.color ← FALSE;
WindowManagerPrivate.growPos.mouseY ← e.first.y+1;
};
IF Rope.Equal[e.first.commonData.name, "Close"] THEN {
WindowManagerPrivate.closePos.mouseX ← e.first.x+1;
WindowManagerPrivate.closePos.color ← FALSE;
WindowManagerPrivate.closePos.mouseY ← e.first.y+1;
};
CurrentX ← CurrentX + e.first.w + menuHSpace;
ENDLOOP;
ENDLOOP;
};
ComputeFontInfo[];
UserProfile.CallWhenProfileChanges[ReEstablishUserProfileParameters];
END.