FingerTool.mesa
Copyright © 1986 by Xerox Corporation. All rights reserved.
Last edited by: Donahue, February 5, 1986 5:25:02 pm PST
Last edited by: Gifford, July 26, 1985 4:25:45 pm PDT
Spreitzer, December 2, 1985 2:18:12 pm PST
Ewan Tempero September 11, 1986 5:35:08 pm PDT
Carl Hauser, September 30, 1986 3:16:37 pm PDT
DIRECTORY
AIS,
Atom USING [GetPName, MakeAtom],
BasicTime USING [nullGMT, GMT, Now, Period],
Buttons USING [Button, ButtonProc, Create, SetDisplayStyle, Destroy],
Commander USING [CommandProc, Handle, Register],
Containers USING [Container, Create],
Convert USING [RopeFromTime],
FingerOps,
FS USING [Error, ExpandName, FileInfo],
Icons USING [IconFlavor, NewIconFromFile],
Imager,
ImagerBackdoor,
ImagerColorOperator,
ImagerTransformation,
ImagerPixelArray,
ImagerPixelArrayDefs,
ImagerFont,
IO,
List USING [Sort, Comparison, CompareProc],
MBQueue,
Menus USING [MouseButton],
MessageWindow,
Process USING[Detach],
ThisMachine,
Rope,
UserCredentials USING [Get],
UserProfile,
VFonts,
ViewerClasses,
ViewerEvents,
ViewerOps,
ViewerTools,
WalnutRegistry;
FingerTool: CEDAR PROGRAM
IMPORTS
AIS, Atom, BasicTime, Buttons, Commander, Containers, Convert, FingerOps, FS, Icons, Imager, ImagerBackdoor, ImagerColorOperator, ImagerPixelArray, ImagerTransformation, IO, List, MBQueue, MessageWindow, Process, ThisMachine, Rope, UserCredentials, UserProfile, ViewerEvents, VFonts, ViewerOps, ViewerTools, WalnutRegistry =
BEGIN
FingerImpl: ERROR ~ CODE; -- BugCatchers!
These global lists store the other answers to a query with more than one answer and allow traversing in both directions.
requestMachineList, requestUserList: requestList;
Directions: TYPE ~ {forwards, backwards};
requestList: TYPE ~ RECORD [
list: LIST OF Rope.ROPENIL,
presentValue: LIST OF Rope.ROPENIL
];
FingerHandle: TYPE = REF FingerToolRec; -- a REF to the data for a particular instance of the finger tool; multiple instances can be created.
FingerToolRec: TYPE =
RECORD -- the data for a particular tool instance
[outer: Containers.Container ← NIL, -- handle for the enclosing container
height: CARDINAL ← 0, -- height measured from the top of the container
performFingerButton: Buttons.Button, -- button clicked to perform finger operation
storeChangesButton: Buttons.Button, -- button clicked to make changes to finger data
talkButton: Buttons.Button, -- button to connect to talk program
nextUserButton: Buttons.Button, -- transient button for multiple matches
nextMachineButton: Buttons.Button, -- transient button for multiple matches
performHostButton, storeHostButton: Buttons.Button,
the following hold all the data pertinent to selections for each fingered object
These items are either user supplied or calculated from other info in the database....
name, host, lastchange, actions: SelectionData ← NEW[SelectionRecord],
...while these items come directly from the database
fingerInfo: LIST OF SelectionData,
pictureViewer: ViewerClasses.Viewer];
SelectionData: TYPE = REF SelectionRecord;
SelectionRecord: TYPE =
RECORD
[parentHandle: FingerHandle,
prop: ATOMNIL,
selectable: BOOL,
button: Buttons.Button,
result: ViewerClasses.Viewer,
registration: ViewerEvents.EventRegistration ← NIL];
PictureState: TYPE = REF PictureStateRecord;
PictureStateRecord: TYPE =
RECORD
[rectangle: ImagerTransformation.Rectangle,
pa: ImagerPixelArrayDefs.PixelArray,
colorOperator: Imager.ColorOperator];
fingerIcon: Icons.IconFlavor = Icons.NewIconFromFile["Finger.icon", 0];
fingerQueue: MBQueue.Queue = MBQueue.Create[];
mailProp: FingerOps.PropPair ← NEW[FingerOps.PropPairObject ← [prop:$MailRead, val: NIL]];
IsPattern: PROC [value: Rope.ROPE] RETURNS [ItsAPattern:BOOL] ~ {
... returns TRUE iff value is has a "*" in it.
RETURN [Rope.Find[s1: value, s2: "*", pos1: 0, case: FALSE] # -1]
}; --
Append: PROC [list1, list2: LIST OF ATOM] RETURNS [LIST OF ATOM] ~ {
I need my own version of Append 'cos I couldn't easily use exsisting stuff.
newList: LIST OF ATOM ← list1;
FOR list: LIST OF ATOM ← list2, list.rest UNTIL list = NIL DO
newList ← CONS[list.first, newList]
ENDLOOP;
RETURN[newList]
};
InAtomList: PROC [atom: ATOM, atomList: LIST OF ATOM] RETURNS [inList: BOOLEAN] ~ {
... returns TRUE iff atom is in atomList
FOR list: LIST OF ATOM ← atomList, list.rest UNTIL list = NIL DO
IF atom = list.first THEN RETURN[inList ← TRUE]
ENDLOOP;
RETURN[inList ← FALSE]
}; -- InAtomList
RecordMailRead: WalnutRegistry.EventProc ~ {
IF event = mailRead THEN {
mailProp.val ← TimeRope[BasicTime.Now[]];
FingerOps.SetUserProps[user: UserCredentials.Get[].name, props: LIST[ mailProp] ! FingerOps.FingerError => CONTINUE ] } };
InitFingerInfo: PROC [fingerHandle: FingerHandle]
Sets up the fingerInfo part of fingerHandle with all the attributes the database knows about.
~ {
FOR info: LIST OF ATOM ← Append[FingerOps.ListMachineProps[], FingerOps.ListUserProps[]], info.rest UNTIL info = NIL DO
fingerHandle.fingerInfo ← CONS[NEW[SelectionRecord], fingerHandle.fingerInfo];
fingerHandle.fingerInfo.first.prop ← info.first
ENDLOOP;
}; -- InitFingerInfo
FindDataForButton: PROC [button: ATOM, fingerInfo: LIST OF SelectionData] RETURNS [SelectionData ← NIL] ~ {
Returns the relevant part of fingerInfo corresponding to button.
FOR infoList: LIST OF SelectionData ← fingerInfo, infoList.rest UNTIL infoList = NIL DO
IF button = infoList.first.prop THEN RETURN[infoList.first]
ENDLOOP;
ERROR FingerImpl; -- should never get to here
}; -- FindDataForButton
ProduceButtonName: PROC [buttonAtom: ATOM] RETURNS [buttonName: Rope.ROPE]
This returns a text version of the atom. It originally was going to do some tricky stuff with the pname (hence the separate routine) but that was later changed.
~ {
buttonName ← Atom.GetPName[atom: buttonAtom];
}; -- ProduceButtonName
InvalidateInfo: PROC [buttonSet: LIST OF ATOM, fingerHandle: FingerHandle] ~ {
This will clear the entries of the buttons listed in buttonSet in the finger tool.
FOR button: LIST OF ATOM ← buttonSet, button.rest UNTIL button = NIL DO
ViewerTools.SetContents[FindDataForButton[button: button.first, fingerInfo: fingerHandle.fingerInfo].result, NIL];
ENDLOOP;
};
Alphabetisize: PROC [atomSet: LIST OF ATOM] RETURNS [LIST OF ATOM]
...returns atomSet ordered alphabetically by PName.
~ {
Compare: List.CompareProc = {
c:List.Comparison;
WITH ref1 SELECT FROM
rope: Rope.ROPE => c ← Rope.Compare[s1: rope, s2: NARROW[ref2], case: TRUE];
ENDCASE => IF ref1 = NIL
THEN c ← Rope.Compare[s1: NARROW[ref1], s2: NARROW[ref2], case: TRUE]
ELSE ERROR;
RETURN [IF c=less THEN greater ELSE IF c=greater THEN less ELSE equal]
};
pNameSet: LIST OF REF ANYNIL;
alphaSet: LIST OF ATOMNIL;
FOR atom: LIST OF ATOM ← atomSet, atom.rest UNTIL atom = NIL DO
pNameSet ← CONS[Atom.GetPName[atom: atom.first], pNameSet]
ENDLOOP;
FOR pName: LIST OF REF ANY ← List.Sort[list: pNameSet, compareProc: Compare], pName.rest UNTIL pName = NIL DO
alphaSet ← CONS[Atom.MakeAtom[pName: NARROW[pName.first, Rope.ROPE]], alphaSet]
ENDLOOP;
RETURN[alphaSet]
};
MakeFingerTool: Commander.CommandProc =
BEGIN
fingerHandle: FingerHandle = NEW[FingerToolRec];
InitFingerInfo[fingerHandle: fingerHandle];
fingerHandle.outer ← Containers.Create -- construct the outer container
[[name: "Finger Tool", -- name displayed in the caption
icon: fingerIcon,
iconic: TRUE, -- so tool will be iconic when first created
column: left, -- initially in the left column
scrollable: FALSE]]; -- don't allow user to scroll contents
fingerHandle.height ← 0;
SetupViewer[fingerHandle];
ViewerOps.SetOpenHeight[fingerHandle.outer, fingerHandle.height]; -- hint our desired height
ViewerOps.PaintViewer[fingerHandle.outer, all]; -- reflect above change
set up the user and machine fields with default values
DisplayMachineInfo[fingerHandle: fingerHandle, machine: ThisMachine.Name[$Pup]];
DisplayUserInfo[fingerHandle: fingerHandle, user: UserCredentials.Get[].name, mouseButton: red]
END;
For uniformity, some standard distances between entries in the tool are defined.
pictureColumnWidth: CARDINAL = 192;
pictureHeight: CARDINAL = 192;
entryVSpace: CARDINAL = 8; -- vertical leading space between sections
entryHeight: CARDINAL = 15; -- how tall to make each line of items
horizSpace: CARDINAL = 10; -- space between items on the same line
firstColumnIndent: CARDINAL = 1; -- measured from left margin
firstColumnWidth: CARDINAL = 110;
secondColumnWidth: CARDINAL = 240;
hostButtonsLevel: CARDINAL ← 0; -- set to the vertical level of the host buttons.
userButtonsLevel: CARDINAL ← 0; -- set to the vertical level of the user buttons.
pictureFont: ImagerFont.Font =
VFonts.EstablishFont["Helvetica", 12, TRUE];
SetupViewer: PROC [fingerHandle: FingerHandle] =
BEGIN
SetupButtons: PROC [name: Rope.ROPE, which: SelectionData, scroll: BOOLFALSE, height: CARDINAL ← entryHeight, edit: BOOLFALSE, style: ATOM ← $WhiteOnBlack] =
BEGIN
which.button ← Buttons.Create
[info:
[name: name,
wx: firstColumnIndent, -- keep the buttons aligned
wy: fingerHandle.height,
ww: firstColumnWidth,
wh: entryHeight, -- specify rather than defaulting so line is uniform
parent: fingerHandle.outer,
border: FALSE ],
proc: SelectionButtonClicked,
clientData: which]; -- this will be passed to our button proc
which.parentHandle ← fingerHandle;
which.selectable ← edit;
Buttons.SetDisplayStyle[which.button, style];
which.result ← ViewerOps.CreateViewer
[flavor: $Text,
info:
[wx: firstColumnIndent + firstColumnWidth + horizSpace, -- keep the buttons aligned
wy: fingerHandle.height,
ww: secondColumnWidth,
wh: height,
scrollable: scroll,
parent: fingerHandle.outer,
border: FALSE]];
IF NOT edit THEN ViewerTools.InhibitUserEdits[which.result];
fingerHandle.height ← fingerHandle.height + height; -- maintain total height
END; -- SetupButtons
SetupButtonSet: PROC [buttonSet: LIST OF ATOM, fingerInfo: LIST OF SelectionData]
Takes a list of atoms and produces a vertical set of buttons in the fingertool based on those atoms and related to fingerInfo in fingerHandle.
~ {
data: SelectionData;
FOR button: LIST OF ATOM ← Alphabetisize[buttonSet], button.rest UNTIL button = NIL DO
data ← FindDataForButton[button: button.first, fingerInfo: fingerInfo];
SetupButtons[
name: ProduceButtonName[button.first],
which: data,
scroll: TRUE,
edit: TRUE];
data.registration ← ViewerEvents.RegisterEventProc[SetNew, edit, data.result]
ENDLOOP;
}; -- SetupButtonSet
fingerHandle.height ← fingerHandle.height + entryVSpace; -- space down from the top of the viewer
userButtonsLevel ← fingerHandle.height;
fingerHandle.performFingerButton ← Buttons.Create -- button for performing finger
[info:
[name: "perform finger",
wx: 0,
wy: fingerHandle.height,
wh: entryHeight, -- specify rather than defaulting so line is uniform
parent: fingerHandle.outer,
border: TRUE],
clientData: fingerHandle, -- this will be passed to our button proc
proc: PerformFingerAtTool];
fingerHandle.storeChangesButton ← Buttons.Create -- button for making changes to finger data
[info:
[name: "store changes",
wx: fingerHandle.performFingerButton.wx + fingerHandle.performFingerButton.ww + horizSpace,
wy: fingerHandle.height,
wh: entryHeight, -- specify rather than defaulting so line is uniform
parent: fingerHandle.outer,
border: TRUE],
clientData: fingerHandle, -- this will be passed to our button proc
proc: StoreChanges];
-- Can't use talker yet, because it hasn't been carried over
fingerHandle.talkButton ← Buttons.Create -- button to connect to talk program
[info:
[name: "talk to",
wx: fingerHandle.storeChangesButton.wx + fingerHandle.storeChangesButton.ww + horizSpace,
wy: fingerHandle.height,
wh: entryHeight, -- specify rather than defaulting so line is uniform
parent: fingerHandle.outer,
border: TRUE],
clientData: fingerHandle, -- this will be passed to our button proc
proc: TalkTo];
fingerHandle.height ← fingerHandle.height + entryHeight + entryVSpace; -- space down from the action buttons
SetupButtons[name: "User Name", which: fingerHandle.name, scroll: TRUE, edit: TRUE];
fingerHandle.name.registration ← ViewerEvents.RegisterEventProc[SetNew, edit, fingerHandle.name.result];
SetupButtonSet[buttonSet: FingerOps.ListUserProps[], fingerInfo: fingerHandle.fingerInfo];
SetupButtons[name: "Actions", which: fingerHandle.actions, scroll: TRUE, height: 3*entryHeight, edit: FALSE, style: $BlackOnGrey];
fingerHandle.pictureViewer ← ViewerOps.CreateViewer[flavor: $FingerPicture,
info: [wx: firstColumnIndent + firstColumnWidth + horizSpace + secondColumnWidth + horizSpace, wy: fingerHandle.name.result.wy, ww: pictureColumnWidth, wh: pictureHeight, parent: fingerHandle.outer, border: FALSE, scrollable: FALSE],
paint: TRUE];
host part of the screen
fingerHandle.height ← fingerHandle.height + entryVSpace; -- space down a line
hostButtonsLevel ← fingerHandle.height;
fingerHandle.performHostButton ← Buttons.Create
[info:
[name: "Get Host Information",
wx: 0,
wy: fingerHandle.height,
wh: entryHeight, -- specify rather than defaulting so line is uniform
parent: fingerHandle.outer,
border: TRUE],
clientData: fingerHandle, -- this will be passed to our button proc
proc: PerformHost];
fingerHandle.storeHostButton ← Buttons.Create -- button for making changes to finger data
[info:
[name: "Store Host Data",
wx: fingerHandle.performFingerButton.wx + fingerHandle.performHostButton.ww + horizSpace,
wy: fingerHandle.height,
wh: entryHeight, -- specify rather than defaulting so line is uniform
parent: fingerHandle.outer,
border: TRUE],
clientData: fingerHandle, -- this will be passed to our button proc
proc: StoreHost];
fingerHandle.height ← fingerHandle.height + entryHeight + entryVSpace; -- space down from the action buttons
SetupButtons[name: "Host", which: fingerHandle.host, scroll: TRUE, edit: TRUE];
fingerHandle.host.registration ← ViewerEvents.RegisterEventProc[SetNew, edit, fingerHandle.host.result];
SetupButtonSet[buttonSet: FingerOps.ListMachineProps[], fingerInfo: fingerHandle.fingerInfo];
SetupButtons[name: "Last User", which: fingerHandle.lastchange, scroll: TRUE, height: entryHeight, edit: FALSE, style: $BlackOnGrey];
fingerHandle.height ← fingerHandle.height + entryVSpace; -- space at bottom of finger viewer
END;
SetNew: ViewerEvents.EventProc = {
WHILE viewer.parent # NIL DO viewer ← viewer.parent ENDLOOP;
TRUSTED { Process.Detach[FORK ViewerOps.PaintViewer[viewer: viewer, hint: caption]] }
};
PaintPicture: ViewerClasses.PaintProc = {
state: PictureState = NARROW[self.data];
rect: Imager.Rectangle = ImagerBackdoor.GetBounds[context];
IF state = NIL THEN {
Imager.SetColor[context, Imager.white];
Imager.MaskRectangle[context, rect];
Imager.SetColor[context, Imager.black];
Imager.SetXY[context, [3.0, pictureHeight*0.5]];
Imager.SetFont[context, pictureFont];
Imager.ShowRope[context, "No Picture"];
RETURN
};
BEGIN
scaleX: REAL ← self.ww / state.rectangle.w;
scaleY: REAL ← self.wh / state.rectangle.h;
scale: REALMIN[scaleX, scaleY];
Imager.ScaleT[context, scale];
Imager.SetSampledColor[context: context, pa: state.pa, m: NIL, colorOperator: state.colorOperator];
Imager.MaskRectangle[context, state.rectangle];
END
};
Message: PROC [r: Rope.ROPE] = {
MessageWindow.Clear[];
MessageWindow.Append[r];
MessageWindow.Blink[];
};
DetermineRequest: PROC [fingerHandle: FingerHandle, relation: ATOM] RETURNS [requestedProp: ATOM, requestedValue: Rope.ROPE]
...looks in fingerHandle and makes a wild guess as to which field the query is being done on. relation specifies which relation to look in ($User for the user relation and $Machine for the machine relation). It also acts as the default. If more than one field has something in it, default. If the defalut field is empty then use the local machine/logged user as the default.
~ {
requestedProp ← relation;
requestedValue ← NIL; -- since the default case pays no attention to this value
IF Rope.Length[ViewerTools.GetContents[IF relation = $User THEN fingerHandle.name.result ELSE fingerHandle.host.result]] # 0 THEN {
something there so return default
RETURN
}
ELSE {
full: BOOLEANFALSE;
FOR prop: LIST OF ATOM ← (IF relation = $User THEN FingerOps.ListUserProps[] ELSE FingerOps.ListMachineProps[]), prop.rest UNTIL prop = NIL DO
IF Rope.Length[ViewerTools.GetContents[FindDataForButton[button: prop.first, fingerInfo: fingerHandle.fingerInfo].result]] # 0 THEN
IF full THEN {
requestedProp ← relation;
requestedValue ← NIL -- since the default case pays no attention to this value
}
ELSE {
full ← TRUE;
requestedProp ← prop.first;
requestedValue ← ViewerTools.GetContents[FindDataForButton[button: prop.first, fingerInfo: fingerHandle.fingerInfo].result]
}
ENDLOOP
}
}; -- DetermineRequest
NextUser: PROC [fingerHandle: FingerHandle, direction: Directions, picture: BOOLFALSE] ~ {
... displays the next user on the requestUserList list (going in the right direction).
user: Rope.ROPE;
IF direction = forwards THEN {
IF requestUserList.presentValue = NIL THEN
requestUserList.presentValue ← requestUserList.list
ELSE
IF (requestUserList.presentValue ← requestUserList.presentValue.rest) = NIL THEN
requestUserList.presentValue ← requestUserList.list;
user ← requestUserList.presentValue.first;
DisplayUserProps[user: user, fingerHandle: fingerHandle, picture: picture];
}
ELSE {
tempList: LIST OF Rope.ROPE ← requestUserList.list;
find the one before presentValue
IF requestUserList.presentValue = requestUserList.list THEN
WHILE tempList.rest # NIL DO
tempList ← tempList.rest
ENDLOOP
ELSE
WHILE tempList.rest # requestUserList.presentValue DO
tempList ← tempList.rest
ENDLOOP;
requestUserList.presentValue ← tempList;
user ← requestUserList.presentValue.first;
DisplayUserProps[user: user, fingerHandle: fingerHandle, picture: picture];
}
};
DisplayUserProps: PROC [user: Rope.ROPE, fingerHandle: FingerHandle, picture: BOOLFALSE]
...displays all the information about user in the fingertool.
~ {
lastChange: FingerOps.StateChange;
time: BasicTime.GMT;
now: BasicTime.GMT = BasicTime.Now[];
twoDaysInSeconds: INT = LONG[48] * 60 * 60;
muser: Rope.ROPE;
action: Rope.ROPE ← " ";
First check the actions entry
FOR m: LIST OF Rope.ROPE ← FingerOps.GetUserData[user], m.rest
UNTIL m=NIL DO
[lastChange, time, muser] ← FingerOps.GetMachineData[m.first];
If the entry is real old, don't bother displaying it
IF BasicTime.Period[from: time, to: now] > twoDaysInSeconds THEN LOOP;
action ← Rope.Cat[action, muser, Rope.Cat[IF lastChange=FingerOps.StateChange[login] THEN " logged in " ELSE " logged out ", m.first],
Rope.Cat[" at\n ",TimeRope[time],"\n"]];
ENDLOOP;
ViewerTools.SetContents[fingerHandle.actions.result, action];
ViewerTools.SetContents[fingerHandle.name.result, user]; -- do it again if necessary
FOR p: LIST OF FingerOps.PropPair ← FingerOps.GetUserProps[user], p.rest
UNTIL p=NIL DO
IF p.first.prop=Atom.MakeAtom["Picture File"] THEN {
SetPicture[fingerHandle.pictureViewer, IF picture THEN p.first.val ELSE NIL];
};
ViewerTools.SetContents[FindDataForButton[button: p.first.prop, fingerInfo: fingerHandle.fingerInfo].result, p.first.val];
ENDLOOP;
}; -- NextUser
NextMachine: PROC [fingerHandle: FingerHandle, direction: Directions] ~ {
... displays the next machine on the requestMachineList list (going in the right direction).
machine: Rope.ROPE;
IF direction = forwards THEN {
IF requestMachineList.presentValue = NIL THEN requestMachineList.presentValue ← requestMachineList.list
ELSE
IF (requestMachineList.presentValue ← requestMachineList.presentValue.rest) = NIL THEN requestMachineList.presentValue ← requestMachineList.list;
machine ← requestMachineList.presentValue.first;
DisplayMachineProps[machine: machine, fingerHandle: fingerHandle];
}
ELSE {
tempList: LIST OF Rope.ROPE ← requestMachineList.list;
find the one before presentValue
IF requestMachineList.presentValue = requestMachineList.list THEN
WHILE tempList.rest # NIL DO
tempList ← tempList.rest
ENDLOOP
ELSE
WHILE tempList.rest # requestMachineList.presentValue DO
tempList ← tempList.rest
ENDLOOP;
requestMachineList.presentValue ← tempList;
machine ← requestMachineList.presentValue.first;
DisplayMachineProps[machine: machine, fingerHandle: fingerHandle];
}
}; -- NextMachine
DisplayMachineProps: PROC [machine: Rope.ROPE, fingerHandle: FingerHandle]
...displays all the information about machine in the fingertool.
~ {
lastChange: FingerOps.StateChange;
time: BasicTime.GMT;
lastUser: Rope.ROPE ← " ";
First check for any changes in the lastuser
[lastChange, time, lastUser] ← FingerOps.GetMachineData[machine];
IF NOT Rope.Equal[lastUser, ""] THEN
ViewerTools.SetContents[fingerHandle.lastchange.result,
Rope.Cat[IF lastChange = FingerOps.StateChange[login] THEN "Login " ELSE
"Logout ", lastUser, " at ",TimeRope[time]]]
ELSE ViewerTools.SetContents[fingerHandle.lastchange.result, NIL];
ViewerTools.SetContents[fingerHandle.host.result, machine]; -- do it again if necessary
FOR p: LIST OF FingerOps.PropPair ← FingerOps.GetMachineProps[machine], p.rest
UNTIL p=NIL DO
ViewerTools.SetContents[FindDataForButton[button: p.first.prop, fingerInfo: fingerHandle.fingerInfo].result, p.first.val];
ENDLOOP;
}; -- DisplayMachineProps
LookUpMachineProp: PROC [fingerHandle: FingerHandle, requestedProp: ATOM, requestedValue: Rope.ROPE] ~ {
...looks up the machine relation for the relshipSet that matches requestedValue on the requestedProp attribute.
IF Rope.Length[base: requestedValue] = 0 THEN RETURN;
requestMachineList.list ← NIL; -- make sure there is nothing left over fro last time.
requestMachineList.list ← FingerOps.MatchMachineProperty[propVal: NEW[FingerOps.PropPairObject ← [prop: requestedProp, val: requestedValue]]];
requestMachineList.presentValue ← NIL;
IF requestMachineList.list = NIL THEN {
Message["Not valid!"];
InvalidateInfo[buttonSet: FingerOps.ListMachineProps[], fingerHandle: fingerHandle];
ViewerTools.SetContents[fingerHandle.lastchange.result, NIL];
RETURN;
};
NextMachine[fingerHandle: fingerHandle, direction: forwards];
if there is more than one match, put up the NEXT button.
IF requestMachineList.list.rest # NIL THEN
fingerHandle.nextMachineButton ← Buttons.Create
[info:
[name: "Next",
wx: fingerHandle.storeHostButton.wx + fingerHandle.storeHostButton.ww + horizSpace,
wy: hostButtonsLevel,
wh: entryHeight, -- specify rather than defaulting so line is uniform
parent: fingerHandle.outer,
border: TRUE],
clientData: fingerHandle, -- this will be passed to our button proc
proc: PerformNextMachine];
}; -- LookUpMachineProp
LookUpUserProp: PROC [fingerHandle: FingerHandle, requestedProp: ATOM, requestedValue: Rope.ROPE] ~ {
...looks up the user relation for the relshipSet that matches requestedValue on the requestedProp attribute.
IF Rope.Length[base: requestedValue] = 0 THEN RETURN;
requestUserList.list ← NIL;
requestUserList.list ← FingerOps.MatchUserProperty[propVal: NEW[FingerOps.PropPairObject ← [prop: requestedProp, val: requestedValue]]];
requestUserList.presentValue ← NIL;
IF requestUserList.list = NIL THEN {
Message["Not a valid user!"];
SetPicture[fingerHandle.pictureViewer, NIL];
InvalidateInfo[buttonSet: FingerOps.ListUserProps[], fingerHandle: fingerHandle];
ViewerTools.SetContents[fingerHandle.actions.result, NIL];
RETURN;
};
NextUser[fingerHandle: fingerHandle, direction: forwards];
if there is more than one match, put up the NEXT button.
IF requestUserList.list.rest # NIL THEN
fingerHandle.nextUserButton ← Buttons.Create
[info:
[name: "Next",
wx: fingerHandle.storeChangesButton.wx + fingerHandle.storeChangesButton.ww + horizSpace,
wy: userButtonsLevel,
wh: entryHeight, -- specify rather than defaulting so line is uniform
parent: fingerHandle.outer,
border: TRUE],
clientData: fingerHandle, -- this will be passed to our button proc
proc: PerformNextUser]
}; -- LookUpUserProp
DisplayUserInfo: PROC [fingerHandle: FingerHandle, user: Rope.ROPE, mouseButton: Menus.MouseButton ← red] ~ {
... displays all the info for user.
IF Rope.Length[user] = 0 THEN {
user ← UserCredentials.Get[].name;
ViewerTools.SetContents[fingerHandle.name.result, user];
};
requestUserList.list ← NIL;
requestUserList.presentValue ← NIL;
Because noone ever bothers to put the registration, append a "*" (assuming there isn't already one there) and always look up the pattern. It's pretty quite so noone will no the difference...
requestUserList.list ← FingerOps.GetMatchingPersons[pattern: IF NOT IsPattern[user] THEN Rope.Concat[base: user, rest: "*"] ELSE user];
IF (requestUserList.list = NIL) THEN {
IF IsPattern[user] THEN Message["No Match"] ELSE Message[Rope.Cat[user, " not found in Finger database!"]];
SetPicture[fingerHandle.pictureViewer, NIL];
InvalidateInfo[buttonSet: FingerOps.ListUserProps[], fingerHandle: fingerHandle];
ViewerTools.SetContents[fingerHandle.actions.result, NIL];
RETURN;
};
display all the properties of the first machine
NextUser[fingerHandle: fingerHandle, direction: forwards, picture: mouseButton = blue];
if there is more than one match, put up the NEXT button.
IF requestUserList.list.rest # NIL THEN
fingerHandle.nextUserButton ← Buttons.Create
[info:
[name: "Next",
wx: fingerHandle.storeChangesButton.wx + fingerHandle.storeChangesButton.ww + horizSpace,
wy: userButtonsLevel,
wh: entryHeight, -- specify rather than defaulting so line is uniform
parent: fingerHandle.outer,
border: TRUE],
clientData: fingerHandle, -- this will be passed to our button proc
proc: PerformNextUser]
}; -- DisplayUserInfo
DisplayMachineInfo: PROC [fingerHandle: FingerHandle, machine: Rope.ROPE] ~ {
... displays all the info for machine.
IF Rope.Length[machine]=0 THEN {
machine ← ThisMachine.Name[$Pup];
ViewerTools.SetContents[fingerHandle.host.result, machine];
};
see if host exists
IF NOT FingerOps.MachineExists[machine] AND NOT IsPattern[machine] THEN {
Message[Rope.Cat[machine," is not a valid host!"]];
InvalidateInfo[buttonSet: FingerOps.ListMachineProps[], fingerHandle: fingerHandle];
ViewerTools.SetContents[fingerHandle.lastchange.result, NIL];
RETURN;
};
requestMachineList.list ← NIL; -- make sure there is nothing left over fro last time.
requestMachineList.presentValue ← NIL;
see if machine is really a pattern and do the right thing
IF IsPattern[machine] THEN
requestMachineList.list ← FingerOps.GetMatchingMachines[pattern: machine]
ELSE
requestMachineList.list ← LIST[machine];
IF (requestMachineList.list = NIL) AND IsPattern[machine] THEN {
Message["No Match"];
InvalidateInfo[buttonSet: FingerOps.ListMachineProps[], fingerHandle: fingerHandle];
ViewerTools.SetContents[fingerHandle.lastchange.result, NIL];
RETURN;
};
display all the properties of the first machine
NextMachine[fingerHandle: fingerHandle, direction: forwards] ;
if there is more than one match, put up the NEXT button.
IF requestMachineList.list.rest # NIL THEN
fingerHandle.nextMachineButton ← Buttons.Create
[info:
[name: "Next",
wx: fingerHandle.storeHostButton.wx + fingerHandle.storeHostButton.ww + horizSpace,
wy: hostButtonsLevel,
wh: entryHeight, -- specify rather than defaulting so line is uniform
parent: fingerHandle.outer,
border: TRUE],
clientData: fingerHandle, -- this will be passed to our button proc
proc: PerformNextMachine];
}; -- DisplayMachineInfo
fingerErrorCode: FingerOps.Reason;
PerformNextUser: Buttons.ButtonProc ~ {
...displays the information about the first user in requestUserList in fingertool and the removes that user from the list except from a button request.
Note that the only time this can be called is when the button exists so there shouldn't be any problems destroying it...right?
ENABLE
FingerOps.FingerError => {fingerErrorCode ← reason; GOTO FingerProblem };
fingerHandle: FingerHandle ← NARROW[clientData];
IF mouseButton = yellow THEN RETURN;
IF mouseButton = blue THEN NextUser[fingerHandle: fingerHandle, direction: backwards] ELSE NextUser[fingerHandle: fingerHandle, direction: forwards];
EXITS FingerProblem => {
MessageWindow.Append[
message: SELECT fingerErrorCode FROM
Aborted => "\n... Transaction Aborted; Retry Finger Operation",
Error => "\n... Internal Finger Error; Contact Implementor",
Failure => "\n... Connection with server broken; try again later",
ENDCASE => NIL,
clearFirst: TRUE ];
MessageWindow.Blink[] };
}; -- PerformNextUser
PerformNextMachine: Buttons.ButtonProc
...displays the information about the first machine in requestMachineList in fingertool and the removes that machine from the list except from a button request.
Note that the only time this can be called is when the button exists so there shouldn't be any problems destroying it...right?
~ {
ENABLE
FingerOps.FingerError => {fingerErrorCode ← reason; GOTO FingerProblem };
fingerHandle: FingerHandle ← NARROW[clientData];
IF mouseButton = yellow THEN RETURN;
NextMachine[fingerHandle: fingerHandle, direction: IF mouseButton = blue THEN backwards ELSE forwards]
EXITS FingerProblem => {
MessageWindow.Append[
message: SELECT fingerErrorCode FROM
Aborted => "\n... Transaction Aborted; Retry Finger Operation",
Error => "\n... Internal Finger Error; Contact Implementor",
Failure => "\n... Connection with server broken; try again later",
ENDCASE => NIL,
clearFirst: TRUE ];
MessageWindow.Blink[] };
}; -- PerformNextMachine
PerformHost: Buttons.ButtonProc = BEGIN
ENABLE
FingerOps.FingerError => {fingerErrorCode ← reason; GOTO FingerProblem };
fingerHandle: FingerHandle ← NARROW[clientData];
machine: Rope.ROPE ← ViewerTools.GetContents[fingerHandle.host.result];
requestedProp: ATOM;
requestedValue: Rope.ROPE;
Remove the "Next" button.
Buttons.Destroy[fingerHandle.nextMachineButton];
Decide which field to do the lookup on. If more than one field has something in it, default to the "Host" field. I the "Host" field is empty then default to the host machine.
[requestedProp, requestedValue] ← DetermineRequest[fingerHandle, $Machine];
IF requestedProp = $Machine -- ie default -- THEN DisplayMachineInfo[fingerHandle: fingerHandle, machine: machine]
ELSE LookUpMachineProp[fingerHandle: fingerHandle, requestedProp: requestedProp, requestedValue: requestedValue];
fingerHandle.outer.newVersion ← FALSE;
ViewerOps.PaintViewer[fingerHandle.outer, caption];
now set editable bit (for now, let anybody edit everything!)
fingerHandle.editable ← Rope.Equal[user, self, FALSE];
EXITS FingerProblem => {
MessageWindow.Append[
message: SELECT fingerErrorCode FROM
Aborted => "\n... Transaction Aborted; Retry Finger Operation",
Error => "\n... Internal Finger Error; Contact Implementor",
Failure => "\n... Connection with server broken; try again later",
ENDCASE => NIL,
clearFirst: TRUE ];
MessageWindow.Blink[] };
END; -- PerformHost
StoreHost: Buttons.ButtonProc =
BEGIN ENABLE
FingerOps.FingerError => {fingerErrorCode ← reason; GOTO FingerProblem };
fingerHandle: FingerHandle = NARROW[clientData]; -- get finger viewer data
newInfo: LIST OF FingerOps.PropPair ← NIL;
IF Rope.Length[ViewerTools.GetContents[fingerHandle.host.result]] = 0 THEN RETURN;
FOR prop: LIST OF ATOM ← FingerOps.ListMachineProps[], prop.rest UNTIL prop = NIL DO
newInfo ← CONS[NEW[FingerOps.PropPairObject ← [prop.first, ViewerTools.GetContents[FindDataForButton[button: prop.first, fingerInfo: fingerHandle.fingerInfo].result]]], newInfo]
ENDLOOP;
FingerOps.SetMachineProps[ViewerTools.GetContents[fingerHandle.host.result], newInfo];
fingerHandle.outer.newVersion ← FALSE;
ViewerOps.PaintViewer[fingerHandle.outer, caption];
EXITS FingerProblem => {
MessageWindow.Append[
message: SELECT fingerErrorCode FROM
Aborted => "\n... Transaction Aborted; Retry Finger Operation",
Error => "\n... Internal Finger Error; Contact Implementor",
Failure => "\n... Connection with server broken; try again later",
ENDCASE => NIL,
clearFirst: TRUE ];
MessageWindow.Blink[] };
END; -- StoreHost
PerformFingerAtTool: Buttons.ButtonProc =
BEGIN
ENABLE
FingerOps.FingerError => {fingerErrorCode ← reason; GOTO FingerProblem };
fingerHandle: FingerHandle ← NARROW[clientData];
user: Rope.ROPE ← ViewerTools.GetContents[fingerHandle.name.result];
now: BasicTime.GMT = BasicTime.Now[];
twoDaysInSeconds: INT = LONG[48] * 60 * 60;
action: Rope.ROPE ← " ";
requestedProp: ATOM;
requestedValue: Rope.ROPE;
Remove the "Next" button.
Buttons.Destroy[fingerHandle.nextUserButton];
Decide which field to do the lookup on. If more than one field has something in it, default to the User field. I the User field is empty then default to the user that is logged in.
[requestedProp, requestedValue] ← DetermineRequest[fingerHandle, $User];
IF requestedProp = $User THEN DisplayUserInfo[fingerHandle: fingerHandle, user: user, mouseButton: mouseButton]
ELSE LookUpUserProp[fingerHandle: fingerHandle, requestedProp: requestedProp, requestedValue: requestedValue];
fingerHandle.outer.newVersion ← FALSE;
ViewerOps.PaintViewer[fingerHandle.outer, caption];
EXITS FingerProblem => {
MessageWindow.Append[
message: SELECT fingerErrorCode FROM
Aborted => "\n... Transaction Aborted; Retry Finger Operation",
Error => "\n... Internal Finger Error; Contact Implementor",
Failure => "\n... Connection with server broken; try again later",
ENDCASE => NIL,
clearFirst: TRUE ];
MessageWindow.Blink[] };
END; -- PerformFingerAtTool
StoreChanges: Buttons.ButtonProc =
BEGIN ENABLE
FingerOps.FingerError => {fingerErrorCode ← reason; GOTO FingerProblem };
fingerHandle: FingerHandle = NARROW[clientData]; -- get finger viewer data
aisFile: Rope.ROPE ← ViewerTools.GetContents[FindDataForButton[button: Atom.MakeAtom["Picture File"], fingerInfo: fingerHandle.fingerInfo].result]; -- needed for SetPicture
newInfo: LIST OF FingerOps.PropPair ← NIL;
IF Rope.Length[ViewerTools.GetContents[fingerHandle.name.result]] = 0 THEN RETURN;
FOR buttonVal: LIST OF SelectionData ← fingerHandle.fingerInfo, buttonVal.rest UNTIL buttonVal = NIL DO
IF InAtomList[atom: buttonVal.first.prop, atomList: FingerOps.ListUserProps[]] THEN
newInfo ← CONS[NEW[FingerOps.PropPairObject ← [prop: buttonVal.first.prop, val: ViewerTools.GetContents[buttonVal.first.result]]], newInfo];
ENDLOOP;
FingerOps.SetUserProps[ViewerTools.GetContents[fingerHandle.name.result], newInfo];
IF mouseButton = blue THEN SetPicture[fingerHandle.pictureViewer, aisFile];
fingerHandle.outer.newVersion ← FALSE;
ViewerOps.PaintViewer[fingerHandle.outer, caption];
EXITS FingerProblem => {
MessageWindow.Append[
message: SELECT fingerErrorCode FROM
Aborted => "\n... Transaction Aborted; Retry Finger Operation",
Error => "\n... Internal Finger Error; Contact Implementor",
Failure => "\n... Connection with server broken; try again later",
ENDCASE => NIL,
clearFirst: TRUE ];
MessageWindow.Blink[] };
END;
TimeRope: PROC[time: BasicTime.GMT] RETURNS[rope: Rope.ROPE] = {
rope ← IF time = BasicTime.nullGMT THEN "unknown"
ELSE Convert.RopeFromTime[time] };
SetPicture: PROC[v: ViewerClasses.Viewer, filename: Rope.ROPE] ~ {
v.data ← NIL;
BEGIN ENABLE FS.Error => CONTINUE;
IF Rope.Length[filename] = 0 THEN NULL
ELSE IF FS.ExpandName[filename].cp.ext.length # 0 THEN {--suppose it is a single black-and-white AIS file name
pa: ImagerPixelArrayDefs.PixelArray ← ImagerPixelArray.FromAIS[filename];
maxSample: ImagerPixelArray.Sample ← ImagerPixelArray.MaxSampleValue[pa, 0];
r: ImagerTransformation.Rectangle ← ImagerTransformation.TransformRectangle[pa.m, [0, 0, pa.sSize, pa.fSize]];
aisFile: AIS.FRef ← AIS.OpenFile[filename];
colorOperator: Imager.ColorOperator ← SELECT aisFile.raster.bitsPerPixel FROM
0 => oneBPPInverted,
1 => oneBPPPositive,
0 => ImagerColorOperator.BlackColorModel[clear: FALSE],
ENDCASE => ImagerColorOperator.GrayLinearColorModel[maxSample, 0, maxSample, NIL];
AIS.CloseFile[aisFile];
v.data ← NEW[PictureStateRecord ← [rectangle: r, pa: pa, colorOperator: colorOperator]];
}
ELSE {--suppose it is the stem of the file names for the RGB seperations of a color picture
redName: Rope.ROPE ← GetSep[filename, "AISSeparationKeys.red", LIST["-red", "-r"]];
greenName: Rope.ROPE ← GetSep[filename, "AISSeparationKeys.green", LIST["-grn", "-green", "-g"]];
blueName: Rope.ROPE ← GetSep[filename, "AISSeparationKeys.blue", LIST["-blu", "-blue", "-b"]];
pa: ImagerPixelArrayDefs.PixelArray ← ImagerPixelArray.Join3AIS[redName, greenName, blueName];
maxSample: ImagerPixelArray.Sample ← ImagerPixelArray.MaxSampleValue[pa, 0];
r: ImagerTransformation.Rectangle ← ImagerTransformation.TransformRectangle[pa.m, [0, 0, pa.sSize, pa.fSize]];
colorOperator: Imager.ColorOperator ← ImagerColorOperator.RGBLinearColorModel[maxSample];
v.data ← NEW[PictureStateRecord ← [rectangle: r, pa: pa, colorOperator: colorOperator]];
};
END;
ViewerOps.PaintViewer[v, all];
}; -- SetPicture
RopeList: TYPE = LIST OF Rope.ROPE;
GetSep: PROC [fileStem, sepKey: Rope.ROPE, sepDefault: RopeList] RETURNS [fileName: Rope.ROPE] = {
seps: RopeList ← UserProfile.ListOfTokens[sepKey, sepDefault];
exts: RopeList ← UserProfile.ListOfTokens["AISExtensions", LIST["AIS"]];
sl, el: Rope.ROPENIL;
FOR ss: RopeList ← seps, ss.rest WHILE ss # NIL DO
IF sl = NIL THEN sl ← ss.first ELSE sl ← sl.Cat[", ", ss.first];
FOR es: RopeList ← exts, es.rest WHILE es # NIL DO
exists: BOOLTRUE;
fileName ← Rope.Cat[fileStem, ss.first, ".", es.first];
[] ← FS.FileInfo[fileName !FS.Error => {exists ← FALSE; CONTINUE}];
IF exists THEN RETURN;
ENDLOOP;
ENDLOOP;
FOR es: RopeList ← exts, es.rest WHILE es # NIL DO
IF el = NIL THEN el ← es.first ELSE el ← el.Cat[", ", es.first];
ENDLOOP;
FS.Error[[user, $NoSuchFile, IO.PutFR[
"%g{%g}.{%g}",
[rope[fileStem]],
[rope[sl]],
[rope[el]]
]]];
}; -- GetSep
GetSep: PROC [fileStem, fmt: Rope.ROPE] RETURNS [fileName: Rope.ROPE] = {
PerCandidate: PROC [rope: Rope.ROPE] RETURNS [tryNext: BOOLTRUE] = {
exists: BOOLTRUE;
[] ← FS.FileInfo[rope !FS.Error => {exists ← FALSE; CONTINUE}];
IF exists THEN fileName ← rope;
tryNext ← NOT exists;
};
fileName ← NIL;
UserProfileOps.Enumerate[PerCandidate, fmt, IO.rope[fileStem]];
IF fileName = NIL THEN FS.Error[[user, $NoSuchFile, fmt.Cat[" of ", fileStem]]];
};
oneBPPPositive: Imager.ColorOperator ← ImagerColorOperator.MapColorModel[1, MapPositive];
oneBPPInverted: Imager.ColorOperator ← ImagerColorOperator.MapColorModel[1, MapInverted];
MapPositive: PROC [s: ImagerPixelArray.Sample] RETURNS [Imager.ConstantColor] = {
RETURN [SELECT s FROM
0 => Imager.black,
1 => Imager.white,
ENDCASE => ERROR];
};
MapInverted: PROC [s: ImagerPixelArray.Sample] RETURNS [Imager.ConstantColor] = {
RETURN [SELECT s FROM
0 => Imager.white,
1 => Imager.black,
ENDCASE => ERROR];
};
-- Can't use talker yet, when it gets converted, we'll use it
TalkTo: Buttons.ButtonProc =
BEGIN
fingerHandle: FingerHandle = NARROW[clientData]; -- get finger viewer data
userNamePattern: Rope.ROPE = ViewerTools.GetContents[fingerHandle.name.result];
cmd: Commander.Handle ← NEW[Commander.CommandObject ← []];
cmd.out ← IO.noWhereStream;
cmd.err ← IO.noWhereStream;
IF Rope.Length[userNamePattern] # 0
THEN
[] ← CommandTool.DoCommandRope[commandLine: Rope.Cat["///Commands/Talk ", userNamePattern, " &"], parent: cmd]
ELSE
BEGIN
MessageWindow.Append[message: "Enter username for talk connection.", clearFirst: TRUE];
MessageWindow.Blink[];
END;
END;
SetUserProperties: Commander.CommandProc = {
ENABLE FingerOps.FingerError => { fingerErrorCode ← reason; GOTO FingerProblem };
FingerOps.AddUserProp["Plan"];
FingerOps.AddMachineProp["Network"];
EXITS FingerProblem => {
IO.PutRope[cmd.out,
SELECT fingerErrorCode FROM
Aborted => "\n... Transaction Aborted; Retry Finger Operation",
Error => "\n... Internal Finger Error; Contact Implementor",
Failure => "\n... Connection with server broken; try again later",
ENDCASE => NIL ] };
};
SelectionButtonClicked: Buttons.ButtonProc ~ {
force the selection into the user input field
whichButton: SelectionData ← NARROW[clientData]; -- get our data
fingerHandle: FingerHandle ← whichButton.parentHandle; -- get other data relating to finger tool
IF whichButton.selectable THEN {
ViewerTools.EnableUserEdits[whichButton.result];
ViewerTools.SetSelection[whichButton.result];
Now decide what to do. If it's the middle button that's clicked then do a lookup on whatever is in that field (whatever that field is).
IF mouseButton = yellow THEN {
IF whichButton = fingerHandle.name THEN {
Buttons.Destroy[fingerHandle.nextUserButton];
DisplayUserInfo[fingerHandle: fingerHandle, user: ViewerTools.GetContents[whichButton.result]]
}
ELSE IF whichButton = fingerHandle.host THEN {
Buttons.Destroy[fingerHandle.nextMachineButton];
DisplayMachineInfo[fingerHandle: fingerHandle, machine: ViewerTools.GetContents[whichButton.result]]
}
ELSE IF InAtomList[atom: whichButton.prop, atomList: FingerOps.ListMachineProps[]] THEN {
Buttons.Destroy[fingerHandle.nextMachineButton];
LookUpMachineProp[fingerHandle: fingerHandle, requestedProp: whichButton.prop, requestedValue: ViewerTools.GetContents[whichButton.result]]
}
ELSE {
Buttons.Destroy[fingerHandle.nextUserButton];
LookUpUserProp[fingerHandle: fingerHandle, requestedProp: whichButton.prop, requestedValue: ViewerTools.GetContents[whichButton.result]]
}
}
}
}; -- SelectionButtonClicked
Register commands with the Exec to perform finger and create an instance of the Finger Tool.
ViewerOps.RegisterViewerClass[flavor: $FingerPicture, class: NEW[ViewerClasses.ViewerClassRec ← [flavor: $FingerPicture, paint: PaintPicture]]];
Commander.Register[key: "FingerTool", proc: MakeFingerTool, doc: "Create a finger tool." ];
Commander.Register[key: "SetUserProperties", proc: SetUserProperties, doc: "Initialize all of the necessary user properties for the FingerTool"];
[] ← WalnutRegistry.Register[procSet: WalnutRegistry.ProcSet[eventProc: RecordMailRead], queue: fingerQueue];
END.
Ewan Tempero August 1, 1986 11:25:51 am PDT
Changed the way the fingertool displayed the information from the database. Before, the information to be displayed was hard-wired into the code. Now it depends on what information the database has. The makes it easy to add more attributes to the various relations.
Added: Append, InitFingerInfo, FindDataForButton, ProduceButtonName, InvalidateInfo, SetupButtonSet.
changes to: SetupViewer (SetupButtonSet and calls to it added), PerformHost, StoreHost, PerformFingerAtTool, StoreChanges, SetUserProperties.
Ewan Tempero August 25, 1986 4:06:59 pm PDT
Now allow queries by middle buttoning and of the property buttons. If multiple matches then a "NEXT" button appears and allows cycling both ways through the list.