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: BOOLTRUE] = {
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: BOOLTRUE] = {
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: BOOLTRUE] = {
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: BOOLEANTRUE] = {
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 ANYNIL] = {
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: BOOLTRUE] = {
newMenu: ViewerMenuRec;
MenuDefinitonChanged: ViewerOps.EnumProc = {
-- [v: Viewer] RETURNS [BOOLTRUE] --
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: BOOLTRUE] = {
viewer.menus ← NIL;
IF paint THEN RePaintBecauseOfMenuChange[viewer];
};
Addline: PUBLIC PROC[viewer: Viewer, name: ATOM, paint: BOOLTRUE, addBefore: ATOMNIL] = {
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: BOOLEANFALSE;
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: ATOMNIL, paint: BOOLTRUE] = {
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: BOOLEANFALSE;
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: BOOLEANTRUE; -- 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.