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! requestMachineList, requestUserList: requestList; Directions: TYPE ~ {forwards, backwards}; requestList: TYPE ~ RECORD [ list: LIST OF Rope.ROPE _ NIL, presentValue: LIST OF Rope.ROPE _ NIL ]; 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, name, host, lastchange, actions: SelectionData _ NEW[SelectionRecord], fingerInfo: LIST OF SelectionData, pictureViewer: ViewerClasses.Viewer]; SelectionData: TYPE = REF SelectionRecord; SelectionRecord: TYPE = RECORD [parentHandle: FingerHandle, prop: ATOM _ NIL, 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] ~ { RETURN [Rope.Find[s1: value, s2: "*", pos1: 0, case: FALSE] # -1] }; -- Append: PROC [list1, list2: LIST OF ATOM] RETURNS [LIST OF ATOM] ~ { 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] ~ { 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] ~ { 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] ~ { 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] ~ { buttonName _ Atom.GetPName[atom: buttonAtom]; }; -- ProduceButtonName InvalidateInfo: PROC [buttonSet: LIST OF ATOM, fingerHandle: FingerHandle] ~ { 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] ~ { 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 ANY _ NIL; alphaSet: LIST OF ATOM _ NIL; 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 DisplayMachineInfo[fingerHandle: fingerHandle, machine: ThisMachine.Name[$Pup]]; DisplayUserInfo[fingerHandle: fingerHandle, user: UserCredentials.Get[].name, mouseButton: red] END; 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: BOOL _ FALSE, height: CARDINAL _ entryHeight, edit: BOOL _ FALSE, 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] ~ { 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]; 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]; 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: REAL _ MIN[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] ~ { 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 { RETURN } ELSE { full: BOOLEAN _ FALSE; 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: BOOL _ FALSE] ~ { 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; 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: BOOL _ FALSE] ~ { lastChange: FingerOps.StateChange; time: BasicTime.GMT; now: BasicTime.GMT = BasicTime.Now[]; twoDaysInSeconds: INT = LONG[48] * 60 * 60; muser: Rope.ROPE; action: Rope.ROPE _ " "; FOR m: LIST OF Rope.ROPE _ FingerOps.GetUserData[user], m.rest UNTIL m=NIL DO [lastChange, time, muser] _ FingerOps.GetMachineData[m.first]; 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] ~ { 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; 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] ~ { lastChange: FingerOps.StateChange; time: BasicTime.GMT; lastUser: Rope.ROPE _ " "; [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] ~ { 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 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] ~ { 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 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] ~ { IF Rope.Length[user] = 0 THEN { user _ UserCredentials.Get[].name; ViewerTools.SetContents[fingerHandle.name.result, user]; }; requestUserList.list _ NIL; requestUserList.presentValue _ NIL; 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; }; NextUser[fingerHandle: fingerHandle, direction: forwards, picture: mouseButton = blue]; 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] ~ { IF Rope.Length[machine]=0 THEN { machine _ ThisMachine.Name[$Pup]; ViewerTools.SetContents[fingerHandle.host.result, machine]; }; 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; 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; }; NextMachine[fingerHandle: fingerHandle, direction: forwards] ; 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 ~ { 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 ~ { 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; Buttons.Destroy[fingerHandle.nextMachineButton]; [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]; 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; Buttons.Destroy[fingerHandle.nextUserButton]; [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 => 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.ROPE _ NIL; 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: BOOL _ TRUE; 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 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]; }; 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 ~ { 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]; 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 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. òFingerTool.mesa Copyright c 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 These global lists store the other answers to a query with more than one answer and allow traversing in both directions. 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.... ...while these items come directly from the database ... returns TRUE iff value is has a "*" in it. I need my own version of Append 'cos I couldn't easily use exsisting stuff. ... returns TRUE iff atom is in atomList Sets up the fingerInfo part of fingerHandle with all the attributes the database knows about. Returns the relevant part of fingerInfo corresponding to button. 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. This will clear the entries of the buttons listed in buttonSet in the finger tool. ...returns atomSet ordered alphabetically by PName. set up the user and machine fields with default values For uniformity, some standard distances between entries in the tool are defined. 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. -- 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]; host part of the screen ...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. something there so return default ... displays the next user on the requestUserList list (going in the right direction). find the one before presentValue ...displays all the information about user in the fingertool. First check the actions entry If the entry is real old, don't bother displaying it ... displays the next machine on the requestMachineList list (going in the right direction). find the one before presentValue ...displays all the information about machine in the fingertool. First check for any changes in the lastuser ...looks up the machine relation for the relshipSet that matches requestedValue on the requestedProp attribute. if there is more than one match, put up the NEXT button. ...looks up the user relation for the relshipSet that matches requestedValue on the requestedProp attribute. if there is more than one match, put up the NEXT button. ... displays all the info for user. 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... display all the properties of the first machine if there is more than one match, put up the NEXT button. ... displays all the info for machine. see if host exists see if machine is really a pattern and do the right thing display all the properties of the first machine if there is more than one match, put up the NEXT button. ...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? ...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? Remove the "Next" button. 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. now set editable bit (for now, let anybody edit everything!) fingerHandle.editable _ Rope.Equal[user, self, FALSE]; Remove the "Next" button. 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. 0 => oneBPPInverted, 1 => oneBPPPositive, GetSep: PROC [fileStem, fmt: Rope.ROPE] RETURNS [fileName: Rope.ROPE] = { PerCandidate: PROC [rope: Rope.ROPE] RETURNS [tryNext: BOOL _ TRUE] = { exists: BOOL _ TRUE; [] _ 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]]]; }; -- 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; force the selection into the user input field 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). Register commands with the Exec to perform finger and create an instance of the Finger Tool. 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. Ê#ô˜šœ™Icodešœ Ïmœ1™—J˜"J˜Jšœ-˜-˜%J˜˜Jšœ8 ˜SJ˜J˜J˜ J˜J˜Jšœžœ˜——Jšžœžœžœ,˜žœžœ˜]Kšœ"¡œ¡ œ™VKšœ žœ˜K˜šžœžœ˜šžœ žœž˜*Kšœ4˜4—šžœ˜šžœFžœž˜PKšœ4˜4——Kšœ*˜*KšœK˜KK˜—šžœ˜Kšœ žœžœžœ˜3K˜Kšœ¡ œ™!šžœ5ž˜;šžœžœž˜Kšœ˜Kšž˜——šžœ˜šžœ.ž˜5Kšœ˜Kšžœ˜——Kšœ(˜(Kšœ*˜*KšœK˜KK˜—K˜K˜—Kš Ÿœžœ žœ'žœžœ˜[Kšœ&¡œ™=šœ˜K˜"Jšœžœ˜Kšœžœ˜%Kšœžœžœ˜+Jšœ žœ˜Jšœ žœ˜J˜K™šžœžœžœžœ&˜>Jšžœžœž˜J˜>Jšœ4™4Jšžœ:žœžœ˜Fšœ*žœ)žœžœ˜ˆJ˜(—Jšžœ˜—J˜=K˜Kšœ9 ˜Tšžœžœžœ:˜HJšžœžœž˜šžœ,žœ˜4Jš œ'žœ žœ žœžœ˜MJ˜—Jšœz˜zJ˜Jšžœ˜—Kšœ  ˜K˜—šŸ œžœ8˜IKšœ%¡œ¡ œ™\Kšœžœ˜K˜šžœžœ˜Kšžœ#žœžœ:˜gšžœ˜KšžœLžœžœ;˜‘—K˜Kšœ0˜0KšœB˜BK˜—šžœ˜Kšœ žœžœžœ˜6K˜Kšœ¡ œ™!šžœ;ž˜Ašžœžœž˜Kšœ˜Kšž˜——šžœ˜šžœ1ž˜8Kšœ˜Kšžœ˜——Kšœ+˜+Kšœ0˜0KšœB˜BK˜—Kšœ ˜—J˜KšŸœžœžœ˜JKšœ&¡œ™@šœ˜K˜"Jšœžœ˜Jšœžœ˜J˜K™+J˜AJ˜šžœžœž˜$˜7Jšœ žœ+žœ žœ˜IJšœ,˜,——Kšžœ9žœ˜CK˜Kšœ< ˜Wšžœžœžœ@˜NJšžœžœž˜Jšœz˜zJšžœ˜—Kšœ ˜K˜—šŸœžœ-žœžœ˜hKšœA¡œ¡œ ™oK˜K–[base: ROPE]šžœ'žœžœ˜5K˜Kšœžœ 6˜UKšœBžœI˜ŽKšœ"žœ˜&K˜šžœžœžœ˜'K˜J˜TJšœ8žœ˜=Jšžœ˜J˜J˜—K˜=K˜K™8šžœ žœžœ˜+šœ0˜0˜J˜JšœS˜SJ˜Jšœ 4˜FJ˜Jšœžœ˜—Jšœ )˜DJšœ˜——Kšœ ˜K˜—šŸœžœ-žœžœ˜eKšœ>¡œ¡œ ™lK˜K–[base: ROPE]šžœ'žœžœ˜5K˜Kšœžœ˜Kšœ<žœI˜ˆKšœžœ˜#K˜šžœžœžœ˜$K˜Jšœ'žœ˜,J˜QKšœ5žœ˜:Jšžœ˜J˜J˜—Kšœ:˜:K˜K™8šžœžœžœ˜(šœ-˜-˜J˜JšœY˜YJ˜Jšœ 4˜FJ˜Jšœžœ˜—Jšœ )˜DJšœ˜——Kšœ ˜—K˜šŸœžœ)žœ+˜mKšœ¡œ™#K˜šžœžœ˜J˜"J˜8J˜—J˜Kšœžœ˜šœžœ˜#J˜—J™¾K–[pattern: ROPE]š œ=žœžœžœ$žœ˜‡K˜šžœžœžœ˜&Kšžœžœžœ;˜kJšœ'žœ˜,J˜QKšœ5žœ˜:Jšžœ˜J˜K˜—Jšœ/™/KšœW˜WK™K™8šžœžœžœ˜(šœ-˜-˜J˜JšœY˜YJ˜Jšœ 4˜FJ˜Jšœžœ˜—Jšœ )˜DJšœ˜——K˜Kšœ ˜K˜—šŸœžœ,žœ˜MKšœ¡™&K˜šžœžœ˜ J˜!J˜;J˜J˜—K˜Jšœ™š žœžœ"žœžœžœ˜IJ˜3J˜TJšœ8žœ˜=Jšžœ˜J˜—J™Jšœžœ 6˜UKšœ"žœ˜&K˜K™9šžœž˜K–[pattern: ROPE]šœI˜I—šžœ˜Kšœžœ ˜(—J™šžœžœžœžœ˜@J˜J˜TJšœ8žœ˜=Jšžœ˜—J˜J˜Jšœ/™/Kšœ>˜>K˜K™8šžœ žœžœ˜+šœ0˜0˜J˜JšœS˜SJ˜Jšœ 4˜FJ˜Jšœžœ˜—Jšœ )˜DJšœ˜——K˜Kšœ ˜K˜—J˜"J˜šŸœ˜'Kšœ4¡œT™—K™~šž˜Jšœ4žœ˜IJ˜—Kšœžœ ˜0Kšžœžœžœ˜$K˜Kšžœžœ<žœ;˜•J˜šžœ˜˜Jšœ žœž˜$J˜?J˜˜>Kšœ;žœ ˜HKšœ žœžœ˜šžœžœžœž˜2Kšžœžœžœžœ˜@šžœžœžœž˜2Jšœžœžœ˜K˜7Jš œžœžœžœžœ˜CJšžœžœžœ˜Jšžœ˜—Jšžœ˜—šžœžœžœž˜2Kšžœžœžœžœ˜@Jšžœ˜—šžœžœ˜&Kšœ˜Kšœ˜Kšœ ˜ Kšœ ˜ Kšœ˜—Kšœ  ˜ —J˜š Ÿœžœžœžœžœ™Iš Ÿ œžœ žœžœ žœžœ™GJšœžœžœ™Jš œžœžœžœžœ™?Jšžœžœ™Jšœ žœ™J™—Jšœ žœ™Jšœ,žœ™?Jšžœ žœžœžœ7™PJ™—J˜JšœY˜YJšœY˜YJ˜šŸ œžœžœ˜Qšžœžœž˜J˜J˜Jšžœžœ˜—J˜—J˜šŸ œžœžœ˜Qšžœžœž˜J˜J˜Jšžœžœ˜—J˜—J˜J˜J™=™šž™Jšœžœ ™JJšœžœ5™OJšœžœ™:Jšœ žœ™Jšœ žœ™šžœ!™#šž™J™n—šž™šž™JšœQžœ™WJ™—Jšžœ™———Jšžœ™J™—šŸœ˜,Kšžœ6žœ˜QKšœ˜Kšœ$˜$šœžœ˜šžœ˜Kšžœž˜Kšœ?˜?Kšœ<˜