DIRECTORY BasicTime USING [ GMT, Now, OutOfRange, Period ], Buttons USING [ ButtonProc, Create ], Commander USING [ CommandProc, Handle, Register ], CommanderOps USING [ NextArgument ], Containers USING [ ChildXBound, ChildYBound, Container, Create ], Convert USING [ IntFromRope, RopeFromTime, RopeFromTimeRFC822 ], FinchSmarts USING [ AnswerCall, ConvDesc, CurrentConversations, CurrentRName, DisconnectCall, Feep, FetchAttribute, FinchIsRunning, IdentifyVisitor, Join, Poke, ReleaseVisitor, InitFinchSmarts, UninitFinchSmarts ], FinchSynthesizer USING [ InitializeFinchSynthesizer, StopSpeech, TextToSpeech ], FinchTool, FS USING [ ComponentPositions, Error, ExpandName, StreamOpen ], Icons USING [ IconFlavor, NewIconFromFile ], IO, Labels USING [Create], MBQueue USING [ Create, CreateButton, CreateMenuEntry, Queue, QueueClientAction ], Menus USING [AppendMenuEntry, CreateEntry, CreateMenu, FindEntry, Menu, MenuEntry, MenuProc, MouseButton, ReplaceMenuEntry ], Process USING [ Detach, Pause, MsecToTicks, SecondsToTicks, SetTimeout], Rope USING [ ActionType, Cat, Concat, Equal, Fetch, Find, Flatten, Length, Map, MakeRope, MaxLen, Replace, ROPE, SkipTo, Substr ], Rules USING [ Create ], Synthesizer USING [ SynthSpecBody, SynthSpec ], TEditScrolling USING [ AutoScroll ], TextEdit USING [ MaxLen, ReplaceByRope ], TextLooks USING [ RopeToLooks ], TextNode USING [ Ref, Root ], ThParty USING [ nullIx, PartyInfo ], Thrush USING [ ActionReport, ConvEventBody, ConversationID, NB, notReallyInConv, nullConvID, Reason, StateInConv ], TiogaButtons USING [ ChangeButtonLooks, CreateButton, CreateViewer, DeleteButton, FindTiogaButton, GetRope, SetStyleFromRope, TiogaButton, TiogaButtonProc, WrongViewerClass], TiogaOps USING [ CallWithLocks, CancelSelection, GetRope, GetSelection, Location, Ref, SelectionError, SelectionGrain, SetSelection, StepForward ], TypeScript USING [ Create ], UserProfile USING [ Boolean, CallWhenProfileChanges, ProfileChangedProc, Token ], VFonts USING [ Font, defaultFont ], ViewerClasses USING [ Viewer, ViewerRec ], ViewerEvents USING [ EventProc, RegisterEventProc, UnRegisterEventProc ], ViewerIO USING [ CreateViewerStreams ], ViewerLocks USING [ CallUnderWriteLock ], ViewerOps USING [AddProp, ComputeColumn, DestroyViewer, FetchProp, PaintViewer, SetOpenHeight], ViewerSpecs USING [ openTopY, openLeftWidth, openRightWidth ], ViewerTools USING [GetSelectionContents, GetContents, MakeNewTextViewer, SetContents, SetSelection ], VoiceUtils USING [ CurrentRName, Report, ReportFR, RegisterWhereToReport, WhereProc ] ; FinchToolImpl: CEDAR MONITOR IMPORTS BasicTime, Buttons, Commander, CommanderOps, Containers, Convert, FinchSmarts, FinchSynthesizer, FinchTool, FS, Icons, IO, Labels, MBQueue, Menus, Rope, Rules, Process, TEditScrolling, TextEdit, TextLooks, TextNode, TiogaButtons, TiogaOps, TypeScript, UserProfile, VFonts, ViewerEvents, ViewerIO, ViewerLocks, ViewerOps, ViewerSpecs, ViewerTools, VoiceUtils EXPORTS FinchTool = { OPEN IO; ConvDesc: TYPE = FinchSmarts.ConvDesc; NB: TYPE = Thrush.NB; Reason: TYPE = Thrush.Reason; ROPE: TYPE = Rope.ROPE; Viewer: TYPE = ViewerClasses.Viewer; finchToolHandle: PUBLIC FinchTool.Handle; cmdHandle: Commander.HandleŽNIL; serverInstance: Rope.ROPE Ž NIL; printLabel: ARRAY Thrush.StateInConv OF Rope.ROPEŽALL["does not make sense"]; finchQueue: PUBLIC MBQueue.Queue Ž MBQueue.Create[]; fadedIcon: Icons.IconFlavor Ž tool; fadingIcon: Icons.IconFlavor Ž tool; finchIcon: Icons.IconFlavor Ž tool; leftFinchIcon: Icons.IconFlavor Ž tool; rightFinchIcon: Icons.IconFlavor Ž tool; outgoingFinchIcon: Icons.IconFlavor Ž tool; outgoingConvIcon: Icons.IconFlavor Ž tool; incomingConvIcon: Icons.IconFlavor Ž tool; labelledFinchIcon: Icons.IconFlavor Ž tool; labelledLeftFinchIcon: Icons.IconFlavor Ž tool; labelledRightFinchIcon: Icons.IconFlavor Ž tool; finchMenu: Menus.Menu; commentEntry, endCommentEntry: Menus.MenuEntry; xFudge: INTEGER = 2; entryHeight: INTEGER = 14; defaultToolHeight: NAT Ž ViewerSpecs.openTopY/5; finchEnabledAtCheckpoint: BOOL Ž FALSE; deleteButtonEnabled: BOOL Ž FALSE; defaultCallLogFileName: Rope.ROPE Ž "/tmp/PersistentCall.log"; Which: TYPE = { self, other, originator, moderator }; PhoneProc: Buttons.ButtonProc = { -- Place call to party identified by primary selection calledPartyText: Rope.ROPE Ž ViewerTools.GetSelectionContents[]; DoPhone[calledPartyText, SELECT mouseButton FROM red, yellow => FALSE, blue=> TRUE, ENDCASE=>ERROR]; }; CalledPartyProc: Buttons.ButtonProc = { -- Place call to Called Party field calledPartyText: Rope.ROPEŽViewerTools.GetContents[finchToolHandle.calledPartyText]; SELECT mouseButton FROM red => ViewerTools.SetSelection[finchToolHandle.calledPartyText, NIL]; yellow => DoPhone[calledPartyText, FALSE]; blue => DoPhone[calledPartyText, TRUE]; ENDCASE => ERROR; }; CallingPartyProc: Buttons.ButtonProc = { -- Place call to Calling Party field callingPartyText: Rope.ROPEŽViewerTools.GetContents[finchToolHandle.callingPartyText]; SELECT mouseButton FROM red => ViewerTools.SetSelection[finchToolHandle.callingPartyText, NIL]; yellow => DoPhone[callingPartyText, FALSE]; blue => DoPhone[callingPartyText, TRUE]; ENDCASE => ERROR; }; PhoneCmd: Commander.CommandProc = { -- CommandTool Phone command ENABLE UNWIND => cmdHandle Ž NIL; calledPartyText: Rope.ROPE Ž cmd.commandLine.Substr[start: 1, len: cmd.commandLine.Length[]-2]; cmdHandle Ž cmd; DoPhone[calledPartyText]; cmdHandle Ž NIL; }; PhoneHomeCmd: Commander.CommandProc = { -- Phone home (ET) command cmd.commandLine Ž " home\n"; [result, msg] Ž PhoneCmd[cmd]; }; DoPhone: PROC[calledPartyText: Rope.ROPE, wantResidence: BOOLŽFALSE] = { callee, newCalledPartyText: Rope.ROPE; pauseNeededInMs: CARDINAL; IF ~CheckActive[finchToolHandle] OR calledPartyText = NIL OR calledPartyText.Length[]=0 THEN RETURN; [callee, newCalledPartyText, wantResidence] Ž ParseCallee[calledPartyText, wantResidence]; Status["Placing call to ", newCalledPartyText]; IF newCalledPartyText#ViewerTools.GetContents[finchToolHandle.calledPartyText] THEN ViewerTools.SetContents[finchToolHandle.calledPartyText, newCalledPartyText]; pauseNeededInMs Ž HangItUp[complain: FALSE, moreToCome: TRUE]; IF pauseNeededInMs#0 THEN Process.Pause[Process.MsecToTicks[pauseNeededInMs]]; -- Maybe a status check wait loop here ?? FinchTool.CallByDescription[description: callee, residence: wantResidence]; }; RedialCmd: Commander.CommandProc = { -- CommandTool Redial command ENABLE UNWIND => cmdHandle Ž NIL; cmdHandle Ž cmd; IF CheckActive[finchToolHandle] THEN DoPhone[ViewerTools.GetContents[finchToolHandle.calledPartyText], FALSE]; cmdHandle Ž NIL; }; RedialOfficeCProc: PROC [data: REF] = { -- Redial from TiogaButton in conversation log WITH data SELECT FROM cDesc: FinchSmarts.ConvDesc => ShowRetriedCall[cDesc]; ENDCASE; RedialIt[data: data]; }; RedialHomeCProc: PROC [data: REF] = { -- Redial from TiogaButton in conversation log WITH data SELECT FROM cDesc: ConvDesc => ShowRetriedCall[cDesc]; ENDCASE; RedialIt[data: data, wantResidence: TRUE]; }; RedialIt: PROC [data: REF, wantResidence: BOOLŽFALSE] = { activeCDesc: FinchSmarts.ConvDesc = GetSelectedDesc[]; IF activeCDesc#NIL AND activeCDesc.situation.self.state > $parsing THEN { Report["Call already in progress"]; RETURN; }; WITH data SELECT FROM cDesc: FinchSmarts.ConvDesc => { IF cDesc.partyInfo=NIL OR cDesc.partyInfo.ixOriginator = ThParty.nullIx THEN Report["Unknown caller/callee"] ELSE IF CheckActive[finchToolHandle] THEN DoPhone[SimplifyName[cDesc, $other], wantResidence]; }; descriptor: Rope.ROPE => { dS: IO.STREAM Ž IO.RIS[descriptor]; start: INT; end: INT; token: Rope.ROPE; TabProc: IO.BreakProc~{RETURN[IF char='\011 THEN $sepr ELSE $other];}; token Ž dS.GetTokenRope[TabProc!IO.EndOfStream => tokenŽNIL].token; -- date token Ž dS.GetTokenRope[TabProc!IO.EndOfStream => tokenŽNIL].token; -- time token Ž dS.GetTokenRope[TabProc!IO.EndOfStream => tokenŽNIL].token; -- status token Ž dS.GetTokenRope[TabProc!IO.EndOfStream => tokenŽNIL].token; -- elapsed time token Ž dS.GetTokenRope[TabProc!IO.EndOfStream => tokenŽNIL].token; -- source/dest IF token=NIL THEN RETURN; start Ž token.Find["from outside line"]; IF start#-1 THEN RETURN; IF (start Ž token.Find["from "]) #-1 THEN startŽ5 ELSE IF (start Ž token.Find["to "]) #-1 THEN startŽ3 ELSE IF (start Ž token.Find["with: "]) #-1 THEN startŽ6 ELSE RETURN; end Ž token.SkipTo[skip: "?\040\011", pos: start]; token Ž token.Substr[start: start, len: end-start]; IF CheckActive[finchToolHandle] THEN DoPhone[token, wantResidence]; }; ENDCASE; }; JoinCmd: Commander.CommandProc = { [] Ž FinchSmarts.Join[CommanderOps.NextArgument[cmd]]; }; AnswerCmd: Commander.CommandProc = { -- CommandTool Answer command ENABLE UNWIND => cmdHandle Ž NIL; cDesc: ConvDesc = GetSelectedDesc[]; cmdHandle Ž cmd; AnswerIt[cDesc: cDesc]; cmdHandle Ž NIL; }; Answer: Menus.MenuProc = TRUSTED { -- Answer from Answer button in Finch Tool cDesc: ConvDesc = GetSelectedDesc[]; AnswerIt[cDesc: cDesc]; }; AnswerCProc: PROC [data: REF] = { -- Answer from TiogaButton in conversation log AnswerIt[cDesc: NARROW[data]]; }; AnswerIt: PROC [cDesc: ConvDescŽNIL] = TRUSTED { IF cDesc = NIL OR (SELECT cDesc.situation.self.state FROM $notified, $ringing =>FALSE, ENDCASE=>TRUE) THEN { Report[" No conversation to join"]; RETURN; }; FinchSmarts.AnswerCall[convID: cDesc.situation.self.convID]; }; HangUpCmd: Commander.CommandProc = { -- HangUp command ENABLE UNWIND => cmdHandle Ž NIL; action: Menus.MouseButton Ž $red; IF CheckForMouseButtonSpec[cmd.commandLine, " middle", FALSE, TRUE].isMatch THEN action Ž $yellow; IF CheckForMouseButtonSpec[cmd.commandLine, " right", FALSE, TRUE].isMatch THEN action Ž $blue; cmdHandle Ž cmd; [] Ž HangItUp[complain: TRUE, action: action]; cmdHandle Ž NIL; }; Hangup: PUBLIC Menus.MenuProc = { -- Hang up from Disconnect button in Finch Tool []ŽHangItUp[complain: TRUE, action: mouseButton]; }; HangupQuietly: PUBLIC Menus.MenuProc = { []ŽHangItUp[complain: FALSE, action: mouseButton]; }; HangupCProc: PROC [data: REF] = { -- Hang up from TiogaButton in conversation log []ŽHangItUp[complain: TRUE, cDesc: NARROW[data]]; }; ToggleActiveState: PROC [data: REF] = { -- Hang up from TiogaButton in conversation log newState: Thrush.StateInConv; cDesc: ConvDesc Ž NARROW[data]; IF cDesc=NIL THEN { Report["No active or held conversation"]; RETURN; }; SELECT cDesc.situation.self.state FROM $active => newState Ž $inactive; $inactive => newState Ž $active; ENDCASE => { Report["Conversation not active or held"]; RETURN; }; FinchSmarts.DisconnectCall[ -- Function now misnamed convID: cDesc.situation.self.convID, newState: newState]; }; HangItUp: PROC[complain: BOOLŽTRUE, moreToCome: BOOLŽFALSE, cDesc: ConvDescŽNIL, action: Menus.MouseButtonŽ$red] RETURNS [pauseNeededInMs: CARDINALŽ0] = TRUSTED { state: Thrush.StateInConv; isTrunk: BOOL; previousSituation: Thrush.ConvEventBodyŽ[]; IF cDesc=NIL THEN cDesc Ž GetSelectedDesc[]; state Ž IF cDesc=NIL THEN idle ELSE cDesc.situation.self.state; isTrunk Ž IF cDesc#NIL THEN cDesc.partyInfo.conversationInfo.bilateralConv ELSE FALSE; SELECT state FROM $idle => { IF complain THEN Report[" No conversation to leave"]; RETURN }; $inactive, -- flashing for a clear line; not needed $reserved, $parsing => IF moreToCome THEN RETURN; ENDCASE; IF isTrunk THEN pauseNeededInMs Ž IF cDesc.situation.self.state=active THEN 2000 ELSE 500; IF action = $yellow THEN previousSituation Ž cDesc.situation; FinchSmarts.DisconnectCall[ convID: cDesc.situation.self.convID, newState: SELECT action FROM $red => $idle, $yellow => $inactive, ENDCASE => $active]; IF action = $yellow THEN { previousSituation.time Ž cDesc.situation.time; cDesc.previousSituation Ž previousSituation; }; }; ConversationMgmtProc: TiogaButtons.TiogaButtonProc = { IF deleteButtonEnabled AND control AND shift AND mouseButton=blue THEN { TiogaButtons.DeleteButton[button]; RETURN; }; IF clientData=NIL THEN clientData Ž TiogaButtons.GetRope[button]; IF control THEN MBQueue.QueueClientAction[q: finchQueue, proc: HangupCProc, data: clientData] ELSE IF shift THEN MBQueue.QueueClientAction[q: finchQueue, proc: AnswerCProc, data: clientData] ELSE IF mouseButton=yellow THEN MBQueue.QueueClientAction[q: finchQueue, proc: RedialOfficeCProc, data: clientData] ELSE IF mouseButton=blue THEN MBQueue.QueueClientAction[q: finchQueue, proc: RedialHomeCProc, data: clientData] ELSE MBQueue.QueueClientAction[q: finchQueue, proc: ToggleActiveState, data: clientData] }; ParseCallee: PROC[fullCallee: ROPE, wantResidence: BOOL] RETURNS [callee: ROPE, fCallee: ROPE, residence: BOOL] = { endCallee: INT; [callee, residence] Ž CheckForMouseButtonSpec[fullCallee, "left ", wantResidence, wantResidence]; [callee, residence] Ž CheckForMouseButtonSpec[callee, "middle ", residence, residence]; [callee, residence] Ž CheckForMouseButtonSpec[callee, "right ", residence, TRUE]; IF callee.Equal["home", FALSE] THEN { callee Ž VoiceUtils.CurrentRName[]; residence Ž TRUE; }; IF (endCalleeŽcallee.Find[" at home", 0, FALSE]) >= 0 THEN { callee Ž callee.Substr[len: endCallee]; residence Ž TRUE; }; fCallee Ž callee; IF wantResidence THEN fCallee Ž fCallee.Concat[" at home"]; }; CheckForMouseButtonSpec: PROC[ target, prefix: ROPE, trueAlready: BOOL, ifMatch: BOOL] RETURNS [remainingTarget: ROPE, isMatch: BOOL] = { pLen: INT Ž prefix.Length[]; remainingTarget Ž target; isMatch Ž trueAlready; IF target.Length[] >= pLen AND prefix.Equal[target.Substr[len: pLen]] THEN { remainingTarget Ž target.Substr[start: pLen]; isMatch Ž ifMatch; }; }; GetSelectedDesc: PUBLIC PROC[chosenButton: TiogaButtons.TiogaButtonŽNIL, checkActive: BOOLŽTRUE] RETURNS [cDesc: ConvDescŽNIL] = { viewer: Viewer = IF finchToolHandle#NIL THEN finchToolHandle.conversations ELSE NIL; selected: TiogaButtons.TiogaButton Ž IF viewer=NIL THEN NIL ELSE NARROW[ViewerOps.FetchProp[viewer, $selectedEntry]]; IF chosenButton#NIL AND selected#chosenButton THEN RETURN; IF selected = NIL OR (checkActive AND ~CheckActive[finchToolHandle, TRUE]) THEN RETURN; cDesc Ž NARROW[selected.clientData]; }; MakeListener: Buttons.ButtonProc = { Menus.ReplaceMenuEntry[finchToolHandle.outer.menu, endCommentEntry, commentEntry]; ViewerOps.PaintViewer[finchToolHandle.outer, menu]; Status["Listening only enabled"]; }; MakeCommentator: Buttons.ButtonProc = { Menus.ReplaceMenuEntry[finchToolHandle.outer.menu, commentEntry, endCommentEntry]; ViewerOps.PaintViewer[finchToolHandle.outer, menu]; Status["Permission to comment granted"]; }; speakTextFeedbackLen: INT Ž 40; dectalkParaSep: Rope.ROPE Ž " \033P;z.+\033\\"; SpeakSelectedProc: Buttons.ButtonProc = { queueIt: BOOLŽTRUE; IF ~CheckActive[finchToolHandle] THEN RETURN; SELECT mouseButton FROM red => NULL; yellow => RETURN; blue => queueIt Ž FALSE; ENDCASE => NULL; SpeakTextNodesFromSelection[queueIt]; }; SpeakTextCmd: Commander.CommandProc = { ENABLE UNWIND => cmdHandle Ž NIL; textToSpeak: Rope.ROPE Ž cmd.commandLine.Substr[start: 1, len: cmd.commandLine.Length[]-2]; cmdHandle Ž cmd; IF CheckActive[finchToolHandle] AND textToSpeak.Length[]#0 THEN []ŽSpeakTextWithFeedback[TRUE, textToSpeak]; cmdHandle Ž NIL; }; SpeakTextWithFeedback: PROC [ queueIt: BOOL, text: ROPEŽNIL, convID: Thrush.ConversationIDŽThrush.nullConvID] RETURNS [newConvID: Thrush.ConversationID] ~ { nb: NB; Status[ IF ~queueIt THEN "Flushing text. " ELSE NIL, "Speaking text: \"", text.Substr[len: speakTextFeedbackLen], IF text.Length[] > speakTextFeedbackLen THEN "...\"" ELSE "\"" ]; [nb, newConvID] Ž FinchSynthesizer.TextToSpeech[convID: convID, synthSpec: NEW[Synthesizer.SynthSpecBodyŽ[textToSpeak: text]], queueIt: queueIt]; IF nb#$success THEN { newConvID Ž Thrush.nullConvID; Report["Sorry, trouble with text to speech service (%g)", [atom[nb]]]; }; }; SpeakTextNodesFromSelection:PROC[queueIt: BOOL, nodeEnd: Rope.ROPEŽdectalkParaSep] ~ { start, end: TiogaOps.Location; [start: start, end: end] Ž TiogaOps.GetSelection[]; FOR node: TiogaOps.Ref Ž start.node, TiogaOps.StepForward[node] DO convID: Thrush.ConversationID Ž Thrush.nullConvID; tiogaopstext: Rope.ROPE Ž TiogaOps.GetRope[node]; tiogaopstext Ž SELECT TRUE FROM node = start.node => Rope.Substr[ base: tiogaopstext, start: start.where, len: IF node=end.node THEN end.where-start.where+1 ELSE Rope.MaxLen ], node = end.node => Rope.Substr[base: tiogaopstext, len: end.where+1], ENDCASE => tiogaopstext; IF node#end.node THEN tiogaopstext Ž Rope.Concat[tiogaopstext, nodeEnd]; convID Ž SpeakTextWithFeedback[queueIt, tiogaopstext, convID]; queueIt Ž TRUE; IF node=end.node THEN EXIT; ENDLOOP; }; StopSpeechProc: Buttons.ButtonProc = { StopSpeaking[]; }; StopSpeakingCmd: Commander.CommandProc = { ENABLE UNWIND => cmdHandle Ž NIL; cmdHandle Ž cmd; StopSpeaking[]; cmdHandle Ž NIL; }; StopSpeaking: PROC = { nb: NB; IF ~CheckActive[finchToolHandle, TRUE] THEN RETURN; nbŽFinchSynthesizer.StopSpeech[].nb; IF nb#$success THEN Report["Sorry, trouble with text to speech service; could not Stop. (%g)", [atom[nb]]]; }; FeepCmd: Commander.CommandProc = { ENABLE UNWIND => cmdHandle Ž NIL; textToFeep: Rope.ROPE Ž FeepValue[cmd.commandLine.Substr[start: 1, len: cmd.commandLine.Length[]-2]]; cDesc: ConvDesc; cmdHandle Ž cmd; cDesc Ž GetSelectedDesc[]; IF cDesc#NIL THEN FinchSmarts.Feep[cDesc.situation.self.convID, textToFeep] ELSE Report["Sorry, was not able to `feep'."]; cmdHandle Ž NIL; }; FeepValue: PUBLIC PROC[text: ROPE] RETURNS [feepText: ROPE] = { FeepFetch: PROC[data: REF, index: INT] RETURNS [c: CHAR] = { cŽNARROW[data, ROPE].Fetch[index]; c Ž SELECT c FROM IN ['A..'Z] => FeepMap[c+('a-'A)], IN ['a..'z] => FeepMap[c], ENDCASE => c; }; IF text=NIL OR text.Length[]=0 THEN RETURN[text]; RETURN[Rope.Flatten[Rope.MakeRope[base: text, size: text.Length[], fetch: FeepFetch]]]; }; FeepMap: PACKED ARRAY CHAR['a..'z] OF CHAR = [ '2, '2, '2, '3, '3, '3, '4, '4, '4, '5, '5, '5, '6, '6, '6, '7, '7, '7, '7, '8, '8, '8, '9, '9, '9, '9 ]; VisitCmd: Commander.CommandProc = { ENABLE UNWIND => cmdHandle Ž NIL; nb: NB; visitor: Rope.ROPE Ž cmd.commandLine.Substr[start: 1, len: cmd.commandLine.Length[]-2]; IF Rope.Length[visitor] = 0 THEN { -- collect the name now instead IO.Put1[cmd.out, IO.rope["Visitor name: "]]; visitor Ž IO.GetLineRope[cmd.in]; }; cmdHandle Ž cmd; IF CheckActive[finchToolHandle] THEN nb Ž FinchSmarts.IdentifyVisitor[visitor: visitor, password: "", complain: FALSE]; IF nb=$passwordNotValid THEN { -- password required, collect it password: Rope.ROPE Ž ""; IO.PutF1[cmd.out, "password: %l", IO.rope["h"]]; password Ž IO.GetLineRope[cmd.in]; IO.PutF1[cmd.out, "%l", IO.rope["H"]]; [] Ž FinchSmarts.IdentifyVisitor[visitor, password]; }; cmdHandle Ž NIL; }; UnvisitCmd: Commander.CommandProc = { ENABLE UNWIND => cmdHandle Ž NIL; visitor: Rope.ROPE Ž cmd.commandLine.Substr[start: 1, len: cmd.commandLine.Length[]-2]; password: Rope.ROPE Ž ""; cmdHandle Ž cmd; IF CheckActive[finchToolHandle] THEN FinchSmarts.ReleaseVisitor[visitor, password]; cmdHandle Ž NIL; }; TDirCmd: Commander.CommandProc = { ENABLE UNWIND => cmdHandle Ž NIL; fileName: ROPE Ž CommanderOps.NextArgument[cmd]; cP: FS.ComponentPositions; IF fileName = NIL THEN RETURN[$Failed, "No Directory name Supplied"]; [fileName, cP] Ž FS.ExpandName[fileName]; IF cP.ext.length=0 THEN fileName Ž fileName.Substr[len: cP.ext.start] .Concat[".TelephoneDirectory"] .Concat[fileName.Substr[start: cP.ver.start, len: cP.ver.length]]; cmdHandle Ž cmd; FinchTool.BuildDirectory[directoryFile: fileName, newOK: TRUE]; cmdHandle Ž NIL; }; StartFinch: PUBLIC PROC[] = TRUSTED { handle: FinchTool.Handle Ž finchToolHandle; enabled, connected: BOOL; IF handle=NIL THEN { MakeFinchTool[]; handle Ž finchToolHandle; IF handle=NIL THEN { Report["Can't create Finch viewer"]; RETURN; }; }; [enabled, connected] Ž FinchSmarts.FinchIsRunning[]; SELECT TRUE FROM connected => Report["Already running."]; enabled => { Report["Connecting . . ."]; FinchSmarts.Poke[]; }; ENDCASE => { Report["Connecting . . ."]; FinchSmarts.InitFinchSmarts[serverInstance, $FinchTool, ReportSystemState, ReportConversationState, ReportRequestState]; FinchSynthesizer.InitializeFinchSynthesizer[]; --finchToolHandle.--finchEnabledAtCheckpoint Ž FALSE; }; }; FinchCmd: Commander.CommandProc = { ENABLE UNWIND => cmdHandle Ž NIL; cmdHandle Ž cmd; serverInstance Ž CommanderOps.NextArgument[cmd]; StartFinch[]; cmdHandle Ž NIL; }; StopFinch: PUBLIC ENTRY PROC[disable: BOOLŽTRUE] = TRUSTED { handle: FinchTool.Handle = finchToolHandle; IF handle=NIL THEN RETURN; handle.keepTwiddling Ž FALSE; NOTIFY handle.twiddleWait; Process.SetTimeout[@handle.twiddleWait, 1]; WAIT handle.twiddleWait; Report["Disconnecting . . ."]; FinchSmarts.UninitFinchSmarts[disable]; }; ButtonStopFinch: Buttons.ButtonProc = { StopFinch[]; }; UnfinchCmd: Commander.CommandProc = { ENABLE UNWIND => cmdHandle Ž NIL; cmdHandle Ž cmd; StopFinch[]; cmdHandle Ž NIL; }; UnfinchOnDestroy: ViewerEvents.EventProc = { IF finchToolHandle=NIL THEN RETURN; StopFinch[]; IF finchToolHandle.conversations#NIL THEN { finchToolHandle.conversations.inhibitDestroy Ž FALSE; ViewerOps.DestroyViewer[finchToolHandle.conversations]; }; FinchTool.DestroyDirectories[]; IF finchToolHandle.tsIn#NIL THEN finchToolHandle.tsIn.Close[]; IF finchToolHandle.tsOut#NIL THEN finchToolHandle.tsOut.Close[]; IF finchToolHandle.callLog#NIL THEN finchToolHandle.callLog.Close[]; ViewerEvents.UnRegisterEventProc[proc: finchToolHandle.scrollEvent, event: open]; finchToolHandle Ž NIL; }; CheckActive: PUBLIC PROC[handle: FinchTool.Handle, mustAlreadyBeActive: BOOLŽFALSE, complain: BOOLŽTRUE] RETURNS[active: BOOLŽFALSE] = { enabled, connected: BOOL; IF handle=NIL THEN RETURN; -- This is essentially a bug. IF ~FinchSmarts.FinchIsRunning[].finchIsEnabled AND ~mustAlreadyBeActive THEN StartFinch[]; [enabled, connected] Ž FinchSmarts.FinchIsRunning[]; active Ž IF mustAlreadyBeActive THEN connected ELSE enabled; IF complain AND ~active AND mustAlreadyBeActive THEN Report["Sorry, Finch has lost contact with the telephone server"]; }; ReportSystemState: PROC[enabled, connected, voicePath: BOOL] = { cDesc: ConvDesc Ž GetSelectedDesc[checkActive: FALSE]; UpdateIconE[finchToolHandle]; IF finchToolHandle=NIL OR (connected=finchToolHandle.finchConnectedAtLastReport AND enabled=finchToolHandle.finchEnabledAtLastReport) THEN RETURN; finchToolHandle.finchConnectedAtLastReportŽconnected; finchToolHandle.finchEnabledAtLastReportŽenabled; VoiceUtils.Report[where: $FinchOnly, remark: IF ~enabled THEN "Finch is not active." ELSE IO.PutFR1["Finch is active%g connected to the telephone server.", rope[IF connected THEN " and" ELSE ", but is not"]]]; IF ~connected AND cDesc # NIL THEN { cDesc.situation.self.state Ž $idle; ReportConversationState[$success, cDesc, "Server connection broken; state of call unknown"]; }; }; didOurBest: BOOLŽFALSE; defaultTime: CARDINAL Ž 2500; Try: PROC[p: PROC, msecsToWait: CARDINAL Ž defaultTime] = TRUSTED { didOurBest Ž FALSE; Process.Detach[FORK Try1[p]]; IF msecsToWait#0 THEN FOR i: NAT IN [0..100) DO Process.Pause[Process.MsecToTicks[msecsToWait/100]]; IF didOurBest THEN EXIT; ENDLOOP; }; Try1: PROC[p: PROC] = { p[]; didOurBest Ž TRUE; }; joinReason: Thrush.Reason Ž NIL; -- or $join; here to allow testing ReportConversationState: ENTRY PROC[ nb: NB, cDesc: ConvDesc, remark: Rope.ROPE ] = { ENABLE UNWIND => NULL; s: IO.STREAM; difficulty: BOOLŽFALSE; state: Thrush.StateInConv; SELECT nb FROM $success => NULL; ENDCASE => { Status[remark]; RETURN; }; IF cDesc = NIL THEN {Status["No state to report"]; RETURN;}; --Not much more can be done. IF finchToolHandle=NIL THEN RETURN; state Ž cDesc.situation.self.state; IF state=initiating THEN RETURN; -- wait until we know more about this conv IF ~cDesc.originatorRecorded AND cDesc.numParties>1 AND cDesc.partyInfo#NIL THEN SELECT cDesc.partyInfo.ixOriginator FROM cDesc.partyInfo.ixSelf => SetContents[finchToolHandle.calledPartyText, cDesc, $other]; ThParty.nullIx => NULL; ENDCASE => SetContents[finchToolHandle.callingPartyText, cDesc, $originator]; UpdateIcon[finchToolHandle]; s Ž IO.ROS[]; IF state=reserved OR state=parsing THEN s.PutRope[printLabel[state]] ELSE { IF cDesc.firstReport THEN { TRUSTED {Process.Detach[ FORK DurationTimer[cDesc] ]}; -- increments every second cDesc.firstReport Ž FALSE; }; s.PutF["%g\t%g\t%g\t", rope[CallDateAndTime[cDesc]], rope[CallStatus[cDesc]], rope[CallDuration[cDesc]]]; s.PutF1["%g", rope[CallerAndOrCallee[cDesc]]]; IF cDesc.numParties>1 AND cDesc.subject#NIL THEN s.Put[rope["\t"], rope[cDesc.subject]]; }; IF state = $failed THEN -- if failed, elaborate. s.PutF1["%s", rope[ SELECT cDesc.situation.reason FROM $busy => "", -- reported above in status field $notFound => " -- no valid party found", $error => " -- connection failed", $noCircuits => " -- no circuits", ENDCASE => " -- unexplained failure" ] ]; IF cDesc.situation.comment#NIL THEN s.PutF1[" (%s)", rope[cDesc.situation.comment]]; IF remark#NIL THEN s.PutF1[" [%s]", rope[remark]]; IF finchToolHandle.backgrounding THEN SELECT state FROM $notified => { selectedDesc: ConvDesc Ž GetSelectedDesc[checkActive: FALSE]; acceptNew: BOOLŽTRUE; IF selectedDesc#NIL THEN SELECT selectedDesc.situation.self.state FROM $idle, $inactive, $neverWas => NULL; -- OK to accept new $active => IF ConvPriority[cDesc] > ConvPriority[selectedDesc] THEN [] Ž HangItUp[cDesc: selectedDesc, action: $yellow] -- Put active call on hold. ELSE acceptNew Ž FALSE; ENDCASE => acceptNew Ž FALSE; FinchSmarts.DisconnectCall[ convID: cDesc.situation.self.convID, newState: IF acceptNew THEN $ringing ELSE $idle, reason: IF acceptNew THEN NIL ELSE $busy ]; }; $idle, $inactive => { bestCDesc: ConvDesc Ž NIL; convs: LIST OF ConvDesc Ž FinchSmarts.CurrentConversations[]; FOR convs1: LIST OF ConvDesc Ž convs, convs1.rest WHILE convs1#NIL DO pSitBest: Thrush.ConvEventBodyŽ[]; pSit1: Thrush.ConvEventBodyŽ[]; cDesc1: ConvDesc Ž convs1.first; IF cDesc1#NIL THEN pSit1 Ž cDesc1.previousSituation; IF cDesc1 = cDesc OR cDesc1.situation.self.state # $inactive OR pSit1.self.state # $active OR ConvPriority[cDesc1] >= 500 THEN LOOP; -- don't make evergreen connections IF bestCDesc = NIL OR ConvPriority[cDesc1] > ConvPriority[bestCDesc] OR (ConvPriority[cDesc1] = ConvPriority[bestCDesc] AND BasicTime.Period[ from: pSitBest.time, to: pSit1.time] > 0) THEN { bestCDesc Ž cDesc1; pSitBest Ž pSit1; }; ENDLOOP; IF bestCDesc#NIL THEN [] Ž HangItUp[cDesc: bestCDesc, action: $blue]; -- reactivate some background call. }; ENDCASE => NULL; -- no special action UpdateConvLog[ cDesc: cDesc, logEntry: s.RopeFromROS[] ]; IF state <= Thrush.notReallyInConv THEN { IF NOT cDesc.reportComplete THEN ShowIncompleteCall[cDesc]; -- only do this once cDesc.reportComplete Ž TRUE; }; }; CallerAndOrCallee: PROC [cDesc: ConvDesc, showVisitor: BOOLŽTRUE] RETURNS [who: ROPEŽ""] = { pInfo: ThParty.PartyInfo Ž cDesc.partyInfo; isMeeting: BOOL; IF cDesc.numParties<2 OR pInfo=NIL THEN RETURN; who Ž IF ~(isMeeting Ž pInfo.conversationInfo.convType = $meeting) THEN SELECT pInfo.ixOriginator FROM pInfo.ixSelf => Rope.Concat[ "to ", RepairIntelnet[DescribeParty[cDesc, TRUE]]], ThParty.nullIx => Rope.Concat["to/from ", RepairIntelnet[DescribeParty[cDesc, TRUE]]], ENDCASE => IF ~Rope.Equal[pInfo[pInfo.ixSelf].intendedName, pInfo[pInfo.ixSelf].name] THEN ( IF showVisitor THEN Rope.Cat["to visitor: ", RepairIntelnet[DescribeParty[cDesc, TRUE, $self]], " from ", RepairIntelnet[DescribeParty[cDesc, TRUE, $originator]]] ELSE Rope.Concat["to visitor from ", RepairIntelnet[DescribeParty[cDesc, TRUE, $originator]]] ) ELSE Rope.Concat["from ", RepairIntelnet[DescribeParty[cDesc, TRUE, $originator]] ] ELSE SELECT pInfo.ixModerator FROM pInfo.ixSelf => "Transmitting meeting", ENDCASE => Rope.Concat["with: ", RepairIntelnet[DescribeParty[cDesc, TRUE, $moderator]]]; IF isMeeting THEN who Ž Rope.Concat[who, " (meeting)"] ELSE IF cDesc.numParties > 2 THEN who Ž Rope.Concat[who, " (conference)"]; }; CallStatus: PROC [cDesc: ConvDesc] RETURNS [status: ROPE] = { state: Thrush.StateInConv Ž cDesc.situation.self.state; IF state <= Thrush.notReallyInConv THEN { status Ž SELECT TRUE FROM state=$failed AND cDesc.situation.reason=$busy => "busy", state#$idle => "failed", cDesc.ultimateState>ringing => printLabel[$idle], -- state=idle ENDCASE => "abandoned"; -- state=idle } ELSE status Ž printLabel[state]; }; CallDuration: PROC [cDesc: ConvDesc] RETURNS [duration: ROPE] = { s: IO.STREAM Ž IO.ROS[]; s.PutF1["%r", int[MAX[0, BasicTime.Period[ cDesc.startTime, BasicTime.Now[] ]]] ]; duration Ž s.RopeFromROS[]; }; ConvPriority: PROC[cDesc: ConvDesc] RETURNS [priority: INT] = { pInfo: ThParty.PartyInfo; priorityRope: ROPE; IF cDesc=NIL THEN RETURN[500]; -- standard priority; don't take chances. pInfo Ž cDesc.partyInfo; IF pInfo=NIL THEN RETURN[500]; priorityRope Ž FinchSmarts.FetchAttribute[pInfo[pInfo.ixSelf].partyAttributes, $Priority, "500"].value; priority Ž Convert.IntFromRope[priorityRope]; }; ReportRequestState: PROC[cDesc: ConvDesc, actionReport: Thrush.ActionReport, actionRequest: REF] RETURNS[betterActionRequest: REF] = { betterActionRequest Ž actionRequest; SELECT actionReport.actionClass FROM $recording => Status[SELECT actionReport.actionType FROM $started => "Please begin speaking", $finished => "Thank you", $flushed => "Voice transmission stopped", ENDCASE => NIL ]; $playback => IF actionReport.actionType = $flushed THEN Status["Voice transmission stopped"]; $synthesizer => IF actionReport.actionType = $finished AND actionRequest # NIL THEN { spoken: Rope.ROPE = Rope.Substr[base: NARROW[actionRequest, Synthesizer.SynthSpec].textToSpeak, len: speakTextFeedbackLen]; Status["Finished speaking \"", spoken, "...\""]; }; ENDCASE; }; DescribeParty: PROC[ cDesc: ConvDesc, indicateUnauthenticated: BOOLŽFALSE, which: Which Ž $other] RETURNS [otherParty: ROPEŽNIL] = { ix: NATŽThParty.nullIx; pInfo: ThParty.PartyInfo Ž cDesc.partyInfo; IF pInfo#NIL THEN ix Ž SELECT which FROM $self => pInfo.ixSelf, $other => pInfo.ixOther, $originator => pInfo.ixOriginator, $moderator => pInfo.ixModerator, ENDCASE=>ix; otherParty Ž IF ix=ThParty.nullIx THEN "..unknown" ELSE pInfo[ix].intendedName; IF indicateUnauthenticated AND (ix=ThParty.nullIx OR pInfo[ix].type=$telephone) THEN otherParty Ž otherParty.Concat["?"]; }; SetContents: PROC[v: ViewerClasses.Viewer, cDesc: ConvDesc, which: Which] = { ViewerTools.SetContents[v, SimplifyName[cDesc, which], TRUE]; cDesc.originatorRecorded Ž TRUE; }; SimplifyName: PROC [cDesc: ConvDesc, which: Which] RETURNS [name: ROPE] = { i: INT; name Ž RepairIntelnet[DescribeParty[cDesc, FALSE, which]]; i Ž name.Find["(", 0, FALSE]; IF name=NIL OR name.Length[]=0 THEN RETURN; IF i>0 THEN name Ž name.Substr[len: i-1]; }; RepairIntelnet: PROC[num: ROPE] RETURNS[number: ROPE] = { i: INTŽ-1; pause: BOOLŽFALSE; M: Rope.ActionType={iŽi+1; pause Ž c<'\040; RETURN[pause OR c='#]}; number Ž num; WHILE number.Map[0, number.Length[], M] DO len: INTŽ IF pause THEN 2 ELSE 1; repl: ROPE Ž IF pause THEN "*" ELSE ""; number Ž Rope.Replace[base: number, start: i, len: len, with: repl]; iŽ-1; ENDLOOP; }; lineHeight: INT Ž 12; tsHeight: INT Ž 3*lineHeight; convHeight: INT Ž 5*lineHeight; convStyle: ROPE Ž "BeginStyle (Cedar) AttachStyle (basicConv) \"common defs for conversation log entries\" { default 160 bp restIndent clearTabStops 65 bp flushLeft tabStop 140 bp flushLeft tabStop 205 bp flushLeft tabStop 260 bp flushLeft tabStop 475 bp flushLeft tabStop } StyleRule EndStyle"; basicConvFormat: ROPE Ž "basicConv"; MakeFinchTool: PROC = BEGIN finchWidth: INTEGER; dif, wH: INTEGER; bt: Viewer; h: FinchTool.Handle; v: Viewer; initialIconic: BOOL Ž UserProfile.Boolean["Finch.InitialIconic", TRUE]; s: IO.STREAM; initialButton: TiogaButtons.TiogaButton; IF finchToolHandle#NIL THEN { ViewerOps.PaintViewer[finchToolHandle.outer, all]; RETURN; }; finchToolHandle Ž NEW[FinchTool.FinchToolRec]; h Ž finchToolHandle; h.finchToolHeight Ž defaultToolHeight; MakeMenus[]; finchToolHandle.outer Ž Containers.Create[[-- outer container name: "Finch 7.0", -- name displayed in the caption label: "", -- label displayed in the icon icon: fadedIcon, iconic: initialIconic, -- whether tool will not be iconic (small) when first created column: left, -- initially in the left column menu: finchMenu, -- displaying our menu command openHeight: h.finchToolHeight, -- See Walnut scrollable: FALSE ]]; -- inhibit user from scrolling contents v Ž h.outer; finchWidth Ž IF v.column=right THEN ViewerSpecs.openRightWidth ELSE ViewerSpecs.openLeftWidth; []ŽViewerEvents.RegisterEventProc[ proc: UnfinchOnDestroy, event: destroy, filter: v, before: TRUE]; h.scrollEvent Ž ViewerEvents.RegisterEventProc[ proc: ScrollConvsOnOpen, event: open, filter: v, before: FALSE]; bt Ž FirstButton[q: finchQueue, name: "Called Party:", proc: CalledPartyProc, parent: v]; h.calledPartyText Ž NextRightTextViewer[sib: bt, w: finchWidth/2-bt.ww]; bt Ž AnotherButton[q: finchQueue, name: "Calling Party:", proc: CallingPartyProc, sib: h.calledPartyText, newLine: FALSE]; h.callingPartyText Ž NextRightTextViewer[sib: bt, w: finchWidth]; Containers.ChildXBound[h.outer, h.callingPartyText]; bt Ž MakeRuler[sib: bt, h: 2]; wH Ž v.ch; IF (dif Ž (wH-bt.cy)) < tsHeight+convHeight THEN { SetOpenHeight[v, wH + (tsHeight+convHeight-dif)]; IF ~v.iconic THEN ViewerOps.ComputeColumn[v.column]; }; h.typescript Ž MakeTypescript[sib: bt]; [h.tsIn, h.tsOut] Ž ViewerIO.CreateViewerStreams[NIL, h.typescript]; bt Ž MakeRuler[sib: h.typescript, h: 2]; h.conversations Ž TiogaButtons.CreateViewer[ info: [wy: bt.wy + 2, ww: v.ww, wh: convHeight, parent: v, border: FALSE] ]; TiogaButtons.SetStyleFromRope[h.conversations, convStyle]; Containers.ChildXBound[v, h.conversations]; Containers.ChildYBound[v, h.conversations]; s Ž CallLogGenerate[]; IF s#NIL THEN WHILE TRUE DO end: BOOL; entry: Rope.ROPE; [entry, end] Ž CallLogRead[s]; IF end THEN EXIT; IF entry=NIL THEN LOOP; initialButton Ž TiogaButtons.CreateButton[viewer: h.conversations, rope: entry, format: basicConvFormat, looks: NIL, proc: ConversationMgmtProc, clientData: NIL, fork: TRUE ! TiogaButtons.WrongViewerClass => EXIT]; ENDLOOP; CallLogOpen[h]; IF initialButton#NIL THEN ViewerOps.AddProp[h.conversations, $lastButton, initialButton]; ViewerOps.PaintViewer[v, all]; -- reflect above change IF Rope.Equal[s1: "TRUE", case: FALSE, s2: UserProfile.Token[key: "Finch.InitialDirectoriesLeft", default: "FALSE"]] OR Rope.Equal[s1: "TRUE", case: FALSE, s2: UserProfile.Token[key: "Finch.InitialDirectoriesRight", default: "FALSE"]] THEN FinchTool.BuildDirectories[]; UserProfile.CallWhenProfileChanges[CollectUserProfileInfo]; END; MakeMenus: PROC = { MakeOneMenu: PROC[menu: Menus.Menu, name: ROPE, proc: Menus.MenuProc] = { Menus.AppendMenuEntry[ menu: menu, entry: MBQueue.CreateMenuEntry[finchQueue, name, proc, finchToolHandle] ]; }; finchMenu Ž Menus.CreateMenu[]; MakeOneMenu[finchMenu, "Phone", PhoneProc]; MakeOneMenu[finchMenu, "Answer", Answer]; MakeOneMenu[finchMenu, "Disconnect", Hangup]; MakeOneMenu[finchMenu, "SpeakText", SpeakSelectedProc]; MakeOneMenu[finchMenu, "StopSpeech", StopSpeechProc]; MakeOneMenu[finchMenu, "Directory", MakeDirectory]; MakeOneMenu[finchMenu, "Comment", MakeCommentator]; commentEntry Ž Menus.FindEntry[finchMenu, "Comment"]; endCommentEntry Ž Menus.CreateEntry["EndComment", MakeListener]; }; MakeFinchToolCmd: Commander.CommandProc = { ENABLE UNWIND => cmdHandle Ž NIL; cmdHandle Ž cmd; MakeFinchTool[]; cmdHandle Ž NIL; }; UpdateIconE: ENTRY PROC[handle: FinchTool.Handle] = { UpdateIcon[handle]; }; UpdateIcon: INTERNAL PROC [handle: FinchTool.Handle] = { CollatedState: PROC[state: Thrush.StateInConv] RETURNS [cs: NAT] = INLINE { RETURN[IF state=$inactive THEN ORD[Thrush.StateInConv.failed]*2+1 ELSE ORD[state]*2]; }; state: Thrush.StateInConv Ž $idle; cs: NAT Ž CollatedState[state]; cDesc: ConvDesc Ž NIL; conversations: LIST OF ConvDesc Ž FinchSmarts.CurrentConversations[]; IF handle=NIL THEN RETURN; FOR cDL: LIST OF ConvDesc Ž conversations, cDL.rest WHILE cDL#NIL DO cState: Thrush.StateInConv Ž cDL.first.situation.self.state; IF CollatedState[cState] >= cs THEN { cDesc Ž cDL.first; state Ž cState; }; ENDLOOP; IF state=ringing THEN { -- start twiddling if haven't already handle.outer.label Ž CallerAndOrCallee[cDesc: cDesc, showVisitor: FALSE]; IF handle.keepTwiddling THEN RETURN; handle.keepTwiddling Ž TRUE; TRUSTED {Process.Detach[ FORK TwiddleFinchIcon[handle, cDesc] ]} } ELSE { IF handle.keepTwiddling THEN { handle.keepTwiddling Ž FALSE; NOTIFY handle.twiddleWait; }; PaintFinchIcon[handle, cDesc]; }; }; TwiddleFinchIcon: ENTRY PROC [handle: FinchTool.Handle, cDesc: ConvDesc] = { ENABLE UNWIND => NULL; MsPause: INTERNAL PROC [ms: INT] = TRUSTED { Process.SetTimeout[@handle.twiddleWait, Process.MsecToTicks[ms]]; WAIT handle.twiddleWait; }; pauseReps: INT Ž handle.pauseTimeOn/ ((handle.pauseTimeTilted+handle.pauseTimeDrawFactor)*2); WHILE handle.keepTwiddling DO whichTwid: Icons.IconFlavor Ž labelledRightFinchIcon; IF ~handle.outer.iconic THEN { MsPause[250]; LOOP; }; FOR i: INT IN [0..pauseReps) WHILE handle.keepTwiddling DO whichTwid Ž SELECT whichTwid FROM labelledLeftFinchIcon=>labelledRightFinchIcon, ENDCASE=>labelledLeftFinchIcon; PaintFinchIcon[handle, cDesc, whichTwid]; MsPause[handle.pauseTimeTilted]; ENDLOOP; PaintFinchIcon[handle, cDesc, labelledFinchIcon]; MsPause[handle.pauseTimeOff]; ENDLOOP; }; PaintFinchIcon: INTERNAL PROC [handle: FinchTool.Handle, cDesc: ConvDesc, whichTwid: Icons.IconFlavor Ž labelledFinchIcon] = { newIcon, oldIcon: Icons.IconFlavor; newLabel, oldLabel: ROPE; state: Thrush.StateInConv Ž $idle; connected, voicePath: BOOLŽTRUE; -- obtain from system state report, somehow. IF cDesc#NIL THEN { state Ž cDesc.situation.self.state; voicePath Ž TRUE; }; newIcon Ž oldIcon Ž handle.outer.icon; oldLabel Ž handle.outer.label; newLabel Ž ""; [,connected, voicePath] Ž FinchSmarts.FinchIsRunning[]; IF ~connected THEN newIcon Ž fadedIcon ELSE SELECT state FROM $idle => newIcon Ž IF voicePath THEN finchIcon ELSE fadingIcon; $reserved, $parsing => { newIcon Ž outgoingFinchIcon; newLabel Ž printLabel[state]; }; $ringing => { newLabel Ž oldLabel; newIcon Ž whichTwid; }; ENDCASE => { pInfo: ThParty.PartyInfo Ž cDesc.partyInfo; newIcon Ž IF pInfo#NIL AND pInfo.ixOriginator#ThParty.nullIx AND pInfo.ixOriginator#pInfo.ixSelf THEN incomingConvIcon ELSE outgoingConvIcon; newLabel Ž CallerAndOrCallee[cDesc: cDesc, showVisitor: FALSE]; }; handle.outer.icon Ž newIcon; handle.outer.label Ž newLabel; IF handle.outer.iconic AND (oldIcon#newIcon OR ~oldLabel.Equal[newLabel]) THEN ViewerOps.PaintViewer[viewer: handle.outer, hint: all]; }; UpdateConvLog: INTERNAL PROC [cDesc: ConvDesc, logEntry: ROPEŽNIL] = { state: Thrush.StateInConv = cDesc.situation.self.state; button: TiogaButtons.TiogaButton Ž NARROW[cDesc.clientData]; IF button = NIL THEN button Ž AddConvDesc[ cViewer: finchToolHandle.conversations, cDesc: cDesc, bRope: logEntry, bFormat: basicConvFormat ] ELSE IF ~cDesc.reportComplete THEN RelabelTiogaButton[ button: button, newRope: logEntry ]; SelectEntryInConversations[button, state >= $failed AND state#$notified]; }; AddConvDesc: INTERNAL PROC [cViewer: Viewer, cDesc: ConvDesc, bRope: ROPEŽNIL, bFormat: ROPEŽNIL, bLooks: ROPEŽNIL] RETURNS [newButton: TiogaButtons.TiogaButtonŽNIL] = { lastButton: TiogaButtons.TiogaButton; IF finchToolHandle=NIL THEN RETURN; newButton Ž NIL; lastButton Ž NARROW[ViewerOps.FetchProp[finchToolHandle.conversations, $lastButton]]; IF lastButton#NIL THEN { lastDesc: ConvDesc = NARROW[lastButton.clientData]; IF lastDesc#NIL AND lastDesc.reportComplete AND (lastDesc.ultimateState=$reserved OR lastDesc.ultimateState=$parsing OR SuppressMultipleOutsideRingEntries[cDesc, lastDesc] OR SuppressServiceEntries[cDesc, lastDesc] ) THEN { newButton Ž lastButton; CallLogCancel[]; }; }; IF newButton#NIL THEN { RelabelTiogaButton[button: newButton, newRope: bRope, newLooks: bLooks]; newButton.clientData Ž cDesc; } ELSE newButton Ž TiogaButtons.CreateButton[viewer: cViewer, rope: bRope, format: bFormat, looks: bLooks, proc: ConversationMgmtProc, clientData: cDesc, fork: TRUE ! TiogaButtons.WrongViewerClass => GOTO FinchViewerDestroyed]; cDesc.clientData Ž newButton; ViewerOps.AddProp[cViewer, $lastButton, newButton]; EXITS FinchViewerDestroyed => RETURN[NIL]; -- never mind }; SuppressMultipleOutsideRingEntries: INTERNAL PROC [currDesc, lastDesc: ConvDesc] RETURNS [BOOL]= { RETURN[currDesc.situation.self.state=$notified AND lastDesc.ultimateState=$notified AND currDesc.partyInfo[currDesc.partyInfo.ixOriginator].type=$trunk AND lastDesc.partyInfo[lastDesc.partyInfo.ixOriginator].type=$trunk AND BasicTime.Period[from: lastDesc.startTime, to: currDesc.startTime] <= 12]; }; SuppressServiceEntries: INTERNAL PROC [currDesc, lastDesc: ConvDesc] RETURNS [BOOLŽFALSE]= { IF lastDesc.numParties>1 AND lastDesc.partyInfo#NIL AND lastDesc.partyInfo.ixOther#ThParty.nullIx AND lastDesc.partyInfo[lastDesc.partyInfo.ixOther].type=$service THEN SELECT finchToolHandle.logServiceCalls FROM none => RETURN[TRUE]; one => { IF currDesc.numParties>1 AND currDesc.partyInfo#NIL AND currDesc.partyInfo.ixOther#ThParty.nullIx AND currDesc.partyInfo[currDesc.partyInfo.ixOther].type=$service THEN RETURN[TRUE]; }; -- all => -- ENDCASE; }; SelectEntryInConversations: INTERNAL PROC[entryButton: TiogaButtons.TiogaButton, doSelect: BOOLŽTRUE] = { viewer: Viewer = IF finchToolHandle#NIL THEN finchToolHandle.conversations ELSE NIL; prevSelected: TiogaButtons.TiogaButton = IF viewer=NIL THEN NIL ELSE NARROW[ViewerOps.FetchProp[viewer, $selectedEntry]]; IF entryButton = NIL THEN RETURN; IF prevSelected # NIL AND NOT IsDestroyed[prevSelected, viewer] AND ((prevSelected=entryButton) # doSelect) THEN { TiogaButtons.ChangeButtonLooks[button: prevSelected, removeLooks: "bs"]; ViewerOps.AddProp[viewer, $selectedEntry, NIL]; }; IF IsDestroyed[entryButton, viewer] THEN { Report["Conversation is no longer available."]; RETURN; }; IF NOT doSelect THEN RETURN; TiogaButtons.ChangeButtonLooks[button: entryButton, addLooks: "bs"]; -- show it's selected ViewerOps.AddProp[viewer, $selectedEntry, entryButton]; ScrollConversations[entryButton, viewer]; }; ScrollConversations: PROC [button: TiogaButtons.TiogaButton, viewer: Viewer] = { fViewer: Viewer; start, end: TiogaOps.Location; level: TiogaOps.SelectionGrain; caretBefore, pendingDelete: BOOL; [fViewer, start, end, level, caretBefore, pendingDelete] Ž TiogaOps.GetSelection[feedback]; TiogaOps.SetSelection[viewer: viewer, start: button.startLoc, end: button.endLoc, which: feedback]; WaitForFeedback[viewer, start, end]; TEditScrolling.AutoScroll[viewer: viewer, tryToGlitch: TRUE, toEndOfDoc: FALSE, id: feedback]; IF fViewer#NIL THEN -- restore previous feedback selection TiogaOps.SetSelection[fViewer, start, end, level, caretBefore, pendingDelete, feedback ! TiogaOps.SelectionError => {TiogaOps.CancelSelection[feedback]; CONTINUE} ] ELSE -- there wasn't one TiogaOps.CancelSelection[feedback]; }; ScrollConvsOnOpen: ViewerEvents.EventProc = { button: TiogaButtons.TiogaButton; cViewer: Viewer = IF finchToolHandle#NIL THEN finchToolHandle.conversations ELSE NIL; IF cViewer=NIL THEN RETURN; button Ž NARROW[ViewerOps.FetchProp[cViewer, $selectedEntry]]; IF button=NIL THEN button Ž NARROW[ViewerOps.FetchProp[cViewer, $lastButton]]; IF button#NIL THEN TRUSTED { Process.Detach[FORK ScrollConversations[button, cViewer ! ABORTED => CONTINUE]]; }; }; feedbackPause: INT Ž 200; WaitForFeedback: PROC [viewer: Viewer, start, end: TiogaOps.Location] ~ { fViewer: Viewer; fstart, fend: TiogaOps.Location; FOR i: INT IN [1..5] DO -- wait up to one second [fViewer, fstart, fend] Ž TiogaOps.GetSelection[feedback]; IF fViewer=viewer AND fstart=start AND fend=end THEN EXIT; Process.Pause[Process.MsecToTicks[feedbackPause]]; ENDLOOP; }; IsDestroyed: PROC [button: TiogaButtons.TiogaButton, viewer: Viewer] RETURNS [BOOL] = { RETURN [TiogaButtons.FindTiogaButton[viewer, button.startLoc] = NIL]; }; ShowIncompleteCall: INTERNAL PROC [cDesc: ConvDesc] = { button: TiogaButtons.TiogaButton Ž NARROW[cDesc.clientData]; logEntry: Rope.ROPE Ž TiogaButtons.GetRope[button]; IF Rope.Find[logEntry, "completed"] = -1 THEN TiogaButtons.ChangeButtonLooks[button: button, addLooks: "i"]; CallLogRecord[logEntry]; }; ShowRetriedCall: ENTRY PROC [cDesc: ConvDesc] = { button: TiogaButtons.TiogaButton Ž NARROW[cDesc.clientData]; TiogaButtons.ChangeButtonLooks[button: button, removeLooks: "i"]; }; statusField: INT Ž 23; -- index of first char of status field in conversation log entry CallDateAndTime: PROC [cDesc: ConvDesc] RETURNS [time: ROPE] = { gmt: BasicTime.GMT Ž cDesc.startTime; date: ROPEŽRope.Replace[base: Convert.RopeFromTimeRFC822[ gmt ! BasicTime.OutOfRange => GOTO TimeOutOfRange], start: 9, with: "\t"]; -- eg, 1 Jul 87 IF Rope.Fetch[date, 1] = IO.SP THEN date Ž Rope.Concat[" ", Rope.Substr[base: date, len: 8]]; time Ž Convert.RopeFromTime[from: gmt, start: $hours, end: $seconds, includeZone: FALSE]; -- eg, 12:00:00 am IF Rope.Fetch[time, 1] = ': THEN time Ž Rope.Concat[" ", time]; time Ž Rope.Concat[date, time]; EXITS TimeOutOfRange => time Ž "*****\t*****"; }; oneSecond: CONDITION; DurationTimer: ENTRY PROC [cDesc: ConvDesc] = { ENABLE UNWIND => NULL; TRUSTED { Process.SetTimeout[@oneSecond, Process.SecondsToTicks[1]]; }; DO WAIT oneSecond; IF cDesc=NIL OR cDesc.clientData=NIL OR cDesc.reportComplete THEN RETURN; {button: TiogaButtons.TiogaButton Ž NARROW[cDesc.clientData]; durationField: INT Ž Rope.Find[TiogaButtons.GetRope[button], "\t", statusField] + 1; RelabelTiogaButton[button: button, newRope: CallDuration[cDesc], start: durationField, len: 8, newLooks: "bs"]; }; ENDLOOP; }; MakeDirectory: Menus.MenuProc = { FinchTool.BuildDirectories[]; }; RelabelTiogaButton: INTERNAL PROC [button: TiogaButtons.TiogaButton, newRope: ROPEŽNIL, start: INTŽ0, len: INTŽINT.LAST, newLooks: ROPEŽNIL] = { node, rootNode: TextNode.Ref; newLen: INTŽ0; LockedRelabel: PROC [root: TiogaOps.Ref] = { [resultLen: newLen] Ž TextEdit.ReplaceByRope[root: rootNode, dest: node, rope: newRope, start: start, len: len, looks: TextLooks.RopeToLooks[newLooks]]; }; IF button#NIL THEN { node Ž button.startLoc.node; rootNode Ž TextNode.Root[node]; IF button.startLoc.where # -1 THEN { -- button < whole node, adjust params start Ž button.startLoc.where + MAX[LONG[0], start]; len Ž MIN[button.endLoc.where - start, len]; }; TiogaOps.CallWithLocks[LockedRelabel, rootNode]; IF button.startLoc.where # -1 THEN { -- button < whole node, adjust endpoint button.endLoc.where Ž button.endLoc.where + (newLen-len); }; }; }; SetOpenHeight: PROC[viewer: ViewerClasses.Viewer, clientHeight: INTEGER] = { LockedSetHeight: PROC = {ViewerOps.SetOpenHeight[viewer, clientHeight]}; ViewerLocks.CallUnderWriteLock[LockedSetHeight, viewer]; }; FirstLabel: PROC[name: ROPE, parent: Viewer] RETURNS [nV: Viewer] = { IF ~FinchTool.CheckAborted[parent] THEN RETURN; nVŽ Labels.Create[ info: [name: name, parent: parent, wh: entryHeight, wy: 1, wx: IF parent.scrollable THEN 0 ELSE xFudge, border: FALSE]]; }; ImmediateButton: PUBLIC PROC[name: ROPE, proc: Buttons.ButtonProc, border: BOOL, sib: ViewerŽNIL, parent: ViewerŽNIL, fork: BOOLŽ TRUE, guarded: BOOLŽ FALSE, newLine: BOOLŽ FALSE] RETURNS[Viewer] = { info: ViewerClasses.ViewerRec; wy: INTEGER Ž 1; sibWh: INTEGER Ž 0; IF parent=NIL THEN IF sib#NIL THEN parent Ž sib.parent ELSE ERROR; IF sib#NIL THEN { wy Ž sib.wy; sibWh Ž sib.wh; }; info Ž [name: name, wy: wy, wh: entryHeight, parent: parent, border: border]; IF (~newLine) AND sib=NIL THEN ERROR; IF newLine THEN { -- first button on new line info.wyŽ info.wy + sibWh + (IF border THEN 1 ELSE 0); -- extra bit info.wxŽ IF parent.scrollable THEN 0 ELSE xFudge; } ELSE -- next button right on same line as previous info.wxŽ sib.wx+sib.ww+xFudge; RETURN[Buttons.Create[info: info, proc: proc, fork: fork, guarded: guarded]]; }; QueuedButton: PUBLIC PROC[name: ROPE, proc: Buttons.ButtonProc, border: BOOL, sib: Viewer, guarded: BOOLŽ FALSE, newLine: BOOLŽ FALSE] RETURNS[Viewer] = { info: ViewerClasses.ViewerRecŽ [name: name, wy: sib.wy, wh: entryHeight, parent: sib.parent, border: border]; IF newLine THEN -- first button on new line { info.wyŽ sib.wy + sib.wh + (IF border THEN 1 ELSE 0); -- extra bit info.wxŽ IF sib.parent.scrollable THEN 0 ELSE xFudge; } ELSE -- next button right on same line as previous info.wxŽ sib.wx+sib.ww+xFudge; RETURN[MBQueue.CreateButton[q: finchQueue, info: info, proc: proc, guarded: guarded]]; }; -- sib is a viewer to the left or above the button to be made AnotherButton: PUBLIC PROC[ q: MBQueue.Queue, name: ROPE, proc: Buttons.ButtonProc, sib: Viewer, data: REF ANYŽ NIL, border: BOOLŽ FALSE, width: INTEGERŽ 0, guarded: BOOLŽ FALSE, font: VFonts.Font Ž VFonts.defaultFont, newLine: BOOLŽ FALSE] RETURNS [nV: Viewer] = { info: ViewerClasses.ViewerRecŽ [name: name, wy: sib.wy, ww: width, wh: entryHeight, parent: sib.parent, border: border]; IF ~CheckAborted[sib] THEN RETURN; IF newLine THEN { -- first button on new line info.wyŽ sib.wy + sib.wh + (IF border THEN 1 ELSE 0); -- extra bit info.wxŽ IF sib.parent.scrollable THEN 0 ELSE xFudge; } ELSE -- next button right on same line as previous info.wxŽ sib.wx+sib.ww+xFudge; RETURN[MBQueue.CreateButton[ q: q, info: info, proc: proc, clientData: data, font: font, guarded: guarded]] }; FirstButton: PUBLIC PROC[ q: MBQueue.Queue, name: ROPE, proc: Buttons.ButtonProc, parent: Viewer, data: REF ANYŽ NIL, border: BOOLŽ FALSE, width: INTEGERŽ 0, guarded: BOOLŽ FALSE, font: VFonts.Font Ž VFonts.defaultFont] RETURNS [nV: Viewer] = { info: ViewerClasses.ViewerRecŽ [name: name, parent: parent, wh: entryHeight, wy: 1, ww: width, wx: IF parent.scrollable THEN 0 ELSE xFudge, border: border]; IF ~CheckAborted[parent] THEN RETURN; nVŽ MBQueue.CreateButton[ q: q, info: info, proc: proc, clientData: data, font: font, guarded: guarded]; }; CheckAborted: PUBLIC PROC[sib: Viewer] RETURNS[ok: BOOL] = { IF sib = NIL THEN RETURN[TRUE]; IF sib.destroyed THEN RETURN[FALSE]; RETURN[TRUE]; }; NextRightTextViewer: PUBLIC PROC[sib: Viewer, w: INTEGER] RETURNS [nV: Viewer] = { IF ~FinchTool.CheckAborted[sib] THEN RETURN; nVŽ ViewerTools.MakeNewTextViewer[ info: [parent: sib.parent, wx: sib.wx+sib.ww+xFudge, wy: sib.wy, ww: w, wh: entryHeight, border: FALSE]]; }; MakeRuler: PROC[sib: Viewer, h: INTEGERŽ 1] RETURNS [r: Viewer] = { IF ~FinchTool.CheckAborted[sib] THEN RETURN; rŽ Rules.Create[ info: [parent: sib.parent, wy: sib.wy+sib.wh+1, ww: sib.parent.ww, wh: h]]; Containers.ChildXBound[sib.parent, r]; }; MakeTypescript: PROC[sib: Viewer] RETURNS [ts: Viewer] = { y: INTEGERŽ sib.wy+sib.wh+xFudge; IF ~CheckAborted[sib] THEN RETURN; tsŽ TypeScript.Create[ info: [parent: sib.parent, ww: sib.cw, wy: y, wh: tsHeight, border: FALSE] ]; Containers.ChildXBound[sib.parent, ts]; }; CallLogOpen: PROC [h: FinchTool.Handle] ~ { callLogFileName: Rope.ROPE Ž UserProfile.Token[ key: "Finch.CallLogFileName" , default: defaultCallLogFileName]; h.callLog Ž FS.StreamOpen[fileName: callLogFileName, accessOptions: $append]; }; CallLogRecord: PROC[entry: Rope.ROPE] ~ { tempOpen: BOOLŽFALSE; IF entry=NIL OR entry.Length[]=0 THEN RETURN; finchToolHandle.callLog.Flush[!IO.EndOfStream => { tempOpen Ž TRUE; CallLogOpen[finchToolHandle]; CONTINUE; }]; finchToolHandle.callLog.PutRope[entry]; finchToolHandle.callLog.PutChar['\n]; finchToolHandle.callLog.Flush[]; IF tempOpen THEN finchToolHandle.callLog.Close[]; }; CallLogCancel: PROC ~ { finchToolHandle.callLog.PutRope["_\n"]; finchToolHandle.callLog.Flush[]; }; CallLogGenerate: PROC RETURNS [s: IO.STREAM] ~ { callLogFileName: Rope.ROPE Ž UserProfile.Token[ key: "Finch.CallLogFileName" , default: defaultCallLogFileName]; s Ž FS.StreamOpen[fileName: callLogFileName! FS.Error => IF error.group=$user THEN {sŽNIL; CONTINUE; }]; }; CallLogRead: PROC[s: IO.STREAM] RETURNS [entry: Rope.ROPE, end: BOOLŽFALSE] ~ { entry Ž s.GetLineRope[!IO.EndOfStream => {endŽTRUE; entry Ž NIL; CONTINUE; }]; IF NOT end THEN { c: CHAR Ž 'a; c Ž s.PeekChar[!IO.EndOfStream => CONTINUE]; IF c='_ THEN { [] Ž s.GetLineRope[]; [entry, end] Ž CallLogRead[s]; }; }; }; Report: PUBLIC PROC[msg: ROPE, a1, a2, a3: IO.Value Ž [null[]]] = { VoiceUtils.ReportFR[where: $Finch, remark: msg, a1: a1, a2: a2, a3: a3]; }; ReportRope: PUBLIC PROC[msg1: ROPE] = { IF msg1#NIL THEN VoiceUtils.Report[where: $Finch, remark: msg1]; }; Status: PUBLIC PROC[msg1, msg2, msg3, msg4: ROPEŽNIL] = { what: ROPE = Rope.Cat[msg1, msg2, msg3, msg4]; ReportRope[what]; }; FinchWhereProc: VoiceUtils.WhereProc = { RETURN[IF cmdHandle#NIL THEN cmdHandle.out ELSE IF finchToolHandle=NIL THEN NIL ELSE finchToolHandle.tsOut]; }; FinchOnlyWhereProc: VoiceUtils.WhereProc = { RETURN[IF finchToolHandle=NIL THEN NIL ELSE finchToolHandle.tsOut]; }; ViewCmd: Commander.CommandProc = TRUSTED { }; CollectUserProfileInfo: UserProfile.ProfileChangedProc = { logServiceCalls: ROPE Ž UserProfile.Token["Finch.LogServiceCalls", "all"]; IF finchToolHandle=NIL THEN RETURN; finchToolHandle.logServiceCalls Ž SELECT TRUE FROM Rope.Equal[s1: logServiceCalls, s2: "none", case: FALSE] => $none, Rope.Equal[s1: logServiceCalls, s2: "one", case: FALSE] => $one, ENDCASE => $all; IF reason # $newUser OR FinchSmarts.CurrentRName[].Equal[VoiceUtils.CurrentRName[], FALSE] OR ~FinchSmarts.FinchIsRunning[].finchIsEnabled THEN RETURN; ViewerOps.DestroyViewer[finchToolHandle.outer]; -- Clean sweep! StartFinch[]; -- Like now }; doAnnounce: BOOL Ž TRUE; -- belongs in finchToolHandle AnnounceOnCmd: Commander.CommandProc = TRUSTED { doAnnounce Ž TRUE; }; AnnounceOffCmd: Commander.CommandProc = TRUSTED { doAnnounce Ž FALSE; }; finchIcon Ž Icons.NewIconFromFile["Finch.icons", 0!ANY=>CONTINUE]; leftFinchIcon Ž Icons.NewIconFromFile["Finch.icons", 1!ANY=>CONTINUE]; rightFinchIcon Ž Icons.NewIconFromFile["Finch.icons", 2!ANY=>CONTINUE]; labelledFinchIcon Ž Icons.NewIconFromFile["Finch.icons", 3!ANY=>CONTINUE]; labelledLeftFinchIcon Ž Icons.NewIconFromFile["Finch.icons", 4!ANY=>CONTINUE]; labelledRightFinchIcon Ž Icons.NewIconFromFile["Finch.icons", 5!ANY=>CONTINUE]; outgoingFinchIcon Ž Icons.NewIconFromFile["Finch.icons", 6!ANY=>CONTINUE]; outgoingConvIcon Ž Icons.NewIconFromFile["Finch.icons", 9!ANY=>CONTINUE]; incomingConvIcon Ž Icons.NewIconFromFile["Finch.icons", 10!ANY=>CONTINUE]; fadedIcon Ž Icons.NewIconFromFile["Finch.icons", 26!ANY=>CONTINUE]; fadingIcon Ž Icons.NewIconFromFile["Finch.icons", 13!ANY=>CONTINUE]; Commander.Register[key: "FinchTool", proc: MakeFinchToolCmd, doc: "Create a Finch viewers tool" ]; Commander.Register["Finch", FinchCmd, "Start Finch (connect to server)"]; Commander.Register["Unfinch", UnfinchCmd, "Stop Finch (disconnect server)"]; -- initialize text for print form of connect state Commander.Register["Phone", PhoneCmd, "Place telephone call to specified party"]; Commander.Register["ET", PhoneHomeCmd, "Phonnne hommmmmmme"]; Commander.Register["Redial", RedialCmd, "Hang up and try current call again"]; Commander.Register["Answer", AnswerCmd, "Answer current conversation"]; Commander.Register["HangUp", HangUpCmd, "Hang up current conversation"]; Commander.Register["SpeakText", SpeakTextCmd, "Utter the remainder of the command"]; Commander.Register["StopSpeech", StopSpeakingCmd, "Stop speaking"]; Commander.Register["Feep", FeepCmd, "Issue touch-tones"]; Commander.Register["Visit", VisitCmd, "Identify an office visitor (by RName)"]; Commander.Register["Unvisit", UnvisitCmd, "Cancel visiting for specified visitor (by RName) or self"]; Commander.Register["TDir", TDirCmd, "Create telephone directory for named file"]; Commander.Register["Join", JoinCmd, "Join an ongoing conversation by name"]; VoiceUtils.RegisterWhereToReport[proc: FinchWhereProc, where: $System]; VoiceUtils.RegisterWhereToReport[proc: FinchWhereProc, where: $Finch]; VoiceUtils.RegisterWhereToReport[proc: FinchOnlyWhereProc, where: $FinchOnly]; printLabel[$active] Ž "active"; printLabel[$idle] Ž "completed"; printLabel[$inactive] Ž "on hold"; printLabel[$ringing] Ž "ringing"; printLabel[$notified] Ž " "; printLabel[$initiating] Ž " "; printLabel[$ringback] Ž "ringing"; printLabel[$reserved] Ž "Telephone is off hook"; printLabel[$parsing] Ž "Call is being dialed"; Commander.Register["AnnounceOn", AnnounceOnCmd, "Allow announcement calls."]; Commander.Register["AnnounceOff", AnnounceOffCmd, "Disallow announcement calls."]; Commander.Register["VuFinchTool", ViewCmd, "Program Management variables for FinchTool"]; }. U, FinchToolImpl.mesa Copyright Ó 1985, 1987, 1988, 1992 by Xerox Corporation. All rights reserved. Last Edited by: Swinehart, June 4, 1992 10:36 pm PDT Last Edited by: Pier, April 17, 1984 3:51:54 pm PST Polle Zellweger (PTZ) July 31, 1990 10:44:14 pm PDT List USING [Reverse], Nice USING [ View ], Current assumption, subject to change as we develop the notion of multiple calls: Only one call is allowed, by Lark, to reach interesting states (>=failed, +notified). We can select any call that does go beyond an interesting state, to indicate the "current" call; all actions that deal with ongoing call will refer to that one. We will not, for the moment, allow any manual selection of conversation-log entries. When there isn't an entry, it's OK to place calls without hanging up first. So Lark and FinchTool/Directory know about the one-call philosophy; FinchSmarts does not, at present. Current assumption: all descriptions, including called/calling party fields and conversation logs, describe only the first other party; in a conference, they'll ignore late arrivals. Types and Typeoids Placing and Controlling Calls [parent: REF ANY, clientData: REF ANY _ NIL, mouseButton: Menus.MouseButton _ red, shift: BOOL _ FALSE, control: BOOL _ FALSE] [parent: REF ANY, clientData: REF ANY _ NIL, mouseButton: Menus.MouseButton _ red, shift: BOOL _ FALSE, control: BOOL _ FALSE] [parent: REF ANY, clientData: REF ANY _ NIL, mouseButton: Menus.MouseButton _ red, shift: BOOL _ FALSE, control: BOOL _ FALSE] No Conversation ID needed here because assumed not active in any now. Refers to entries in the viewer finchToolHandle.conversations, not necessarily current one. Refers to entries in the viewer finchToolHandle.conversations, not necessarily current one. Refers to entries in the viewer finchToolHandle.conversations, not necessarily current one. Called to disconnect a call in progress without logging a message. PROC [button: TiogaButton, clientData: REF ANY _ NIL, mouseButton: ViewerClasses.MouseButton _ red, shift, control: BOOL _ FALSE] Button-invoked operations when applied directly to conversation-viewer log entries. Hack for controlling contents of conversation viewer. Parses single party-specification: phone number (leading character is not a letter), or named recipient. Names can be RNames or other text sequences that can be looked up in one's private (TDir-style) telephone directory. "XXX at home" is a request to call a residence rather than office number. "home" is equivalent to " at home". "left XXX" and "middle XXX" are equivalent to "XXX". "right XXX" is the same as "XXX at home". This is to accommodate the strange Command Tool. "XXX at " says you want to call the specified number, and the recipient is XXX (not implemented yet.) [parent: REF ANY, clientData: REF ANY _ NIL, mouseButton: Menus.MouseButton _ red, shift: BOOL _ FALSE, control: BOOL _ FALSE] [parent: REF ANY, clientData: REF ANY _ NIL, mouseButton: Menus.MouseButton _ red, shift: BOOL _ FALSE, control: BOOL _ FALSE] Speaking Text We let FinchSynthesizer invent the actionID's, we don't track reports, but we do return the conversationID, so that subsequent adjacent requests can be reliably queued under the same conversation. Other User Commands Convert an arbitrary rope into characters found on a DTMF touchpad. '*, '#, and digits and all punctuation (often used to format phone numbers) map to themselves. Letters (either case) map to their standard telephone-dial locations. Also, 'Q->'7 and 'Z->'9. One can put a leading '. or something in front of an address to force higher levels to interpret it as a "number" instead of a name. Now people can dial .FAX or .HELP or 9(800)TheCard, or 9Fastell. a b c d e f g h i j k l m n o p q r s t u v w x y z try first time w/o password IF Rope.Length[visitor] = 0 THEN {}; -- no name => self No password for unvisiting, either. Starting and Stopping If not active, try to make it active, unless mustAlreadyBeActive. If complain is FALSE, don't complain when not active, if you have any control over it. Here "active" refers not to the state of a party in a conversation, but to the state of connectedness of Finch to the server. Conversation Management Don't use conversation buttons for failure reports If we originated, called-party is already custom-tailored from local information. Else pick up calling party (incoming), or called-party (randomly-chosen if multiple). The following reinstated October 3, 1989 4:55:17 pm PDT; announcements don't work; see below. Now report the original state; subsequent reports will cause our changes to be reflected. Compute a best match: previous state must have been active, priority must be below foreground, but take the highest such, and among those, take the latest such to transition to held. Backgrounding completely disabled October 3, 1989 4:54:53 pm PDT; for further study. SELECT state FROM $notified => { selectedDesc: ConvDesc _ GetSelectedDesc[checkActive: FALSE]; acceptNew: BOOL_TRUE; newState: Thrush.StateInConv; reason: Thrush.Reason; IF selectedDesc # cDesc THEN { -- otherwise just another report; ignore it IF doAnnounce AND Rope.Equal[cDesc.subject, "Announcement"] THEN Announcement stuff currently vestigial; there are bugs -- PTZ 3/31/89 { newState _ $active; reason _ joinReason; } ELSE { newState _ $ringing; reason _ NIL; }; IF selectedDesc#NIL THEN SELECT selectedDesc.situation.self.state FROM $idle, $inactive, $neverWas => NULL; -- OK to accept new $active => IF finchToolHandle.backgrounding AND ConvPriority[cDesc] > ConvPriority[selectedDesc] THEN [] _ HangItUp[cDesc: selectedDesc, action: $yellow] -- Put active call on hold. ELSE acceptNew _ FALSE; ENDCASE => acceptNew _ FALSE; FinchSmarts.DisconnectCall[ convID: cDesc.situation.self.convID, newState: IF acceptNew THEN newState ELSE $idle, reason: IF acceptNew THEN reason ELSE $busy ]; }; Now report the original state; subsequent reports will cause our changes to be reflected. }; $idle, $inactive => IF finchToolHandle.backgrounding THEN { bestCDesc: ConvDesc _ NIL; convs: LIST OF ConvDesc _ FinchSmarts.CurrentConversations[]; FOR convs1: LIST OF ConvDesc _ convs, convs1.rest WHILE convs1#NIL DO Compute a best match: previous state must have been active, priority must be below foreground, but take the highest such, and among those, take the latest such to transition to held. cDesc1: ConvDesc _ convs1.first; IF cDesc1 = cDesc THEN LOOP; -- don't make evergreen connections See above pSit reftab garbage code if you reactive this. IF cDesc1.situation.self.state # $inactive OR cDesc1.previousSituation.self.state # $active OR ConvPriority[cDesc1] >= 500 THEN LOOP; IF bestCDesc = NIL OR ConvPriority[cDesc1] > ConvPriority[bestCDesc] OR (ConvPriority[cDesc1] = ConvPriority[bestCDesc]AND BasicTime.Period[from: bestCDesc.previousSituation.time, to: cDesc1.previousSituation.time] > 0) THEN bestCDesc _ cDesc1; ENDLOOP; IF bestCDesc#NIL THEN [] _ HangItUp[cDesc: bestCDesc, action: $blue]; -- reactivate some background call. }; ENDCASE => NULL; -- no special action Use empty string rather than NIL ROPE to prevent Viewers from using Viewer name as a backup for a NIL label. We might receive reports about IntervalSpec requests or voice synthesizer requests. The actionRequest will be REF that we supplied in the original request subject to change. We may also have to add parameters (first, last) to this report. . Describe the other party. Other party descriptions can be of the form "xxx.pa (nnnn)", which confuses Phone commands. Number in parens and lack-of-authentication question mark should be eliminated from number field being set here. Other party description can contain Intelnet pause characters and authentication code. Intelnet pause characters are all characters < '\040, followed by "P". For the present, we assume that there's a P following the pause in one of these; other values are possible, but we don't expect them in phone numbers. Should be repaired (pause => "*" and auth-code removed) wherever the user sees it. There may also be "#" codes embedded in the number, to serve as "enter" commands. Remove those, as well. Viewer Construction and Management Finch Viewer Check how much space is left (if any) in the control window for a typescript. Finch Tool Viewer MakeOneMenu[finchMenu, "Drop Out", ButtonStopFinch]; Removed for now; use "Unfinch". Should be replaced by toggle that also serves to indicate current enabled/connected state. Finch Icon Find the "most-active" cDesc A nit: $inactive > $active, but must rank below all other real states. Conversations subviewer ViewersOps.AddProp[newButton, $convDesc, NIL]; Above could be used to forget about any conversation but the latest one. Once we're dealing with multiple converations, that won't be good enough. Perhaps a better place to do that is where we detect them going idle. At present, this occurs only when one had dial-tone or was dialing and hung up. Otherwise, busy calls would be lost and we some day hope to be able to use the information about busy calls in redialing. STCWN (Subject to change without notice.) no way to specify new format for new rope! Incoming back door calls are retried once per ring if rejected because the called telephone is busy. Combine them in the log. This is a hack. desc.numParties>1 does not imply desc.partyInfo#NIL in pathological cases This implementation has a flaw: if the user made a service call followed by a "null" call (eg pick up receiver), lastButton will be the "null" call rather than the service call, so this call will be logged. But when the log entry is actually made, it will replace the "null" call, so two service calls will appear juxtaposed. do it even if prevSelected = entryButton, because we may have relabeled the previous one inbetween, which will remove looks PROC [viewer: Viewer, event: ViewerEvent, before: BOOL] RETURNS [abort: BOOL _ FALSE] FORK process to avoid deadlock with column lock held by ViewerEvents. sample conversation log entry date time status duration from/to subject (priority) 30 Dec 86 10:39:01 am abandoned 00:00:22 Dan Swinehart (4473) Finch (urgent) Directory Viewer and Button Utilities Replaces the rope from [button.startLoc.where+start..button.startLoc.where+start+len] with newRope, using the specified new looks Creates a text viewer, next right on the same line as sib sib must be a Viewer, not NIL Make an h-bit wide line after sib Sib is sibling to create TS after Containers.ChildYBound[sib.parent, ts]; Call Log Management Manage a persistent log of calls. This should be recorded and maintained by the server, but a promise is a promise and time's a wastin'. A brief reading will indicate that many things need to be improved before this is acceptable. DCS, October 14, 1991 4:15:11 pm PDT Five operations are currently supported: CallLogOpen: Opens a stream on a persistent log of calls. CallLogRecord: Records the final value of a call log entry into a log file, flushing after itself. CallLogCancel: Records a characteristic sequence ("_") into the log, which will cause the previous entry to be ignored when the log is later replayed. CallLogCancel: Returns a stream for reading the log. CallLogRead[stream]: Returns a line from the log, or NIL for EOF; Reporting and Logging Nice.View[finchToolHandle, "FinchTool PD"]; Registration, Initialization Register a command with the UserExec that will create an instance of this tool Debugging nonsense Swinehart, August 7, 1985 8:56:34 am PDT Merge PTZ TextSpeech stuff changes to: DIRECTORY, MakeMenus, SpeakSelectedProc Polle Zellweger (PTZ) August 19, 1985 5:00:46 pm PDT Handle Prose flushing. changes to: MakeMenus, ReportConversationState, StopSpeechProc Polle Zellweger (PTZ) August 19, 1985 5:54:32 pm PDT changes to: DIRECTORY, FinchToolImpl, ReportConversationState, StopSpeechProc Polle Zellweger (PTZ) September 3, 1985 5:57:21 pm PDT Curtail intermediate Prose reports if ~finchToolHandle.debug; allow user to specify SpeakText queueIt via red|blue button; some cosmetic feedback changes. changes to: ReportConversationState, SpeakSelectedProc, speakTextFeedbackLen, SpeakTextWithFeedback, StopSpeechProc Swinehart, September 6, 1985 10:03:45 am PDT Don't OPEN FinchTool.bcd, eliminate (status line, extra Phone button). Eliminate dual-menu, demote Drop-Out. Merge Phone, Redial. PhoneSelection becomes CommandTool feature, with user profile help. Add names->feepNum features (can dial 9(800)TheCard) Add feep command (for talking to your bank or whatever). Add HangUp, SpeakText, StopSpeaking commands. Pick up incoming call idents, outgoing calls originated at telset, in called-party fields. Automatically connect to Server when command is issued; don't delete conversation viewer when disconnecting any more. Reorganize order of procedures. Polle Zellweger (PTZ) October 4, 1985 6:58:47 pm PDT Add labelled icons, used to display caller when Finch tool etc is closed. changes to: labelledFinchIcon, labelledLeftFinchIcon, labelledRightFinchIcon, labelledConversationIcon, TwiddleFinchIcon, (module initialization) Polle Zellweger (PTZ) October 16, 1985 4:38:58 pm PDT Change Finch telephone icon to labelled conversation icon while conversation in progress. Added monitor to protect icon twiddling and restoring. changes to: FinchToolImpl, ConversationHandle, ReportConversationState, PaintFinchIcon, TwiddleFinchIcon, NewIcon, MsPause Polle Zellweger (PTZ) October 18, 1985 4:08:31 pm PDT Add ability to recognize node boundaries in selection for text-to-speech. changes to: SpeakTextWithFeedback, RopeWithNodesFromSelection (new) Swinehart, October 27, 1986 5:12:27 pm PST Synthesis had been removed from development versions of Finch. Put it back in, now using the service facilities provided by the FinchSynthesizer interface. Polle Zellweger (PTZ) February 9, 1987 6:27:46 pm PST changes to: FeepMap, VisitCmd, Commander, Commander Polle Zellweger (PTZ) February 19, 1987 10:29:47 pm PST changes to: VisitCmd, DIRECTORY Polle Zellweger (PTZ) February 20, 1987 6:19:47 pm PST changes to: VisitCmd Polle Zellweger (PTZ) February 23, 1987 2:15:31 pm PST changes to: StartFinch Polle Zellweger (PTZ) February 25, 1987 5:08:58 pm PST changes to: UnvisitCmd Polle Zellweger (PTZ) February 26, 1987 11:30:32 am PST changes to: Commander, VoiceUtils, DIRECTORY Polle Zellweger (PTZ) February 27, 1987 2:35:40 pm PST changes to: DIRECTORY, VisitCmd Polle Zellweger (PTZ) July 27, 1987 11:24:07 pm PDT Better party identification in ReportConversationState. changes to: ReportConversationState, OtherParty, SetContents, DIRECTORY, serverInstance, us, unknown, PaintFinchIcon Polle Zellweger (PTZ) July 28, 1987 8:34:12 pm PDT Replace buttons with TiogaButtons in conversation log; no new functionality. changes to: DIRECTORY, FinchToolImpl, PhoneProc, GetSelectedDesc, AddConvDesc, ConversationMgmtProc, HangupCProc, AnswerCProc, RedialOfficeCProc, RedialHomeCProc, Hangup, HangItUp, ReportConversationState, convStyle, basicConvFormat, MakeFinchTool, MakeMenus, RelabelTiogaButton, LockedRelabel (local of RelabelTiogaButton), SetOpenHeight, finchMenu, SelectEntryInConversations, IsDestroyed Polle Zellweger (PTZ) July 30, 1987 3:00:51 pm PDT Add ability to Redial from TiogaButtons in conversation log. changes to: RedialOfficeCProc, RedialHomeCProc, RedialIt, Answer, AnswerCProc, AnswerIt, HangupCProc, HangItUp, ConversationMgmtProc, SetContents, SimplifyName Polle Zellweger (PTZ) July 31, 1987 11:36:39 pm PDT Changed format of conversation log entries; added process to increment call duration field while active. changes to: FinchToolImpl, PhoneHomeCmd, RedialIt, UnfinchOnDestroy, CheckActive, ReportConversationState, RepairIntelnet, RelabelTiogaButton, LockedRelabel (local of RelabelTiogaButton), VoiceUtils, printLabel, IsDestroyed, CallDateAndTime, DurationTimer, DIRECTORY, CallStatus, CallDuration, ReportRequestState, convStyle, printLabel, printLabel, printLabel, printLabel, printLabel, SelectEntryInConversations, ReportConversationState, SimplifyName Polle Zellweger (PTZ) August 3, 1987 6:52:36 pm PDT Bug fixes re conv log reports; new icons (larger labels) changes to: ReportConversationState, CallStatus, AddConvDesc, RelabelTiogaButton, LockedRelabel (local of RelabelTiogaButton), DurationTimer, PaintFinchIcon, labelledFinchIcon, labelledLeftFinchIcon, labelledRightFinchIcon, outgoingConvIcon, incomingConvIcon Polle Zellweger (PTZ) August 4, 1987 4:44:02 pm PDT More bug fixes re conv log reports changes to: ReportConversationState, CallerAndOrCallee, CallStatus, PaintFinchIcon Polle Zellweger (PTZ) August 7, 1987 3:37:15 pm PDT Improve reporting of names; change monitor structure to fix race condition between conv log entries and duration timer; fix icon mgmt for notified calls changes to: ReportConversationState, CallerAndOrCallee, PaintFinchIcon, AddConvDesc, SelectEntryInConversations, DurationTimer, RelabelTiogaButton, oneSecond, PaintFinchIcon, TwiddleFinchIcon Polle Zellweger (PTZ) August 11, 1987 2:41:54 pm PDT Inhibit redialing from conv log buttons if any call is in progress (like Directory buttons); add outgoingFinchIcon for reserved or parsing states; partition ReportConversationState so that it fits in one screen. changes to: RedialIt, ReportConversationState, CallerAndOrCallee, MakeFinchTool, PaintFinchIcon, DurationTimer new: outgoingFinchIcon, UpdateIcon, UpdateConvLog Polle Zellweger (PTZ) August 13, 1987 3:59:35 pm PDT Fix bug in reporting of Intelnet numbers in conv log; add logServiceCalls UserProfile option to allow user to control logging of service calls. changes to: DIRECTORY, ReportConversationState, ReportRequestState, RepairIntelnet, MakeFinchTool, UpdateIcon, CheckUserLogOptions, UpdateConvLog, ViewCmd, CollectUserProfileInfo, UserProfile, VoiceUtils, Commander, initialization Polle Zellweger (PTZ) August 14, 1987 5:36:19 pm PDT changes to: CheckUserLogOptions, CollectUserProfileInfo, ReportConversationState, MakeFinchTool, AddConvDesc, SuppressServiceEntries, SuppressMultipleOutsideRingEntries, SelectEntryInConversations Polle Zellweger (PTZ) August 17, 1987 11:19:54 am PDT changes to: PaintFinchIcon Polle Zellweger (PTZ) September 14, 1987 10:07:17 pm PDT DCS changes in the meantime: totally grey icon for disconnected Finch; grey handset icon for disconnected Lark. PTZ changes: fix bug in setting italic looks on conv log entries when another call is in progress; add feature to remove italic looks when conv log entry is used to retry call. changes to: RedialOfficeCProc, RedialHomeCProc, ReportConversationState, UpdateConvLog, SelectEntryInConversations, ShowRetriedCall, RelabelTiogaButton Polle Zellweger (PTZ) September 14, 1987 10:31:18 pm PDT changes to: SelectEntryInConversations Polle Zellweger (PTZ) September 16, 1987 12:04:15 pm PDT Change STOP! command to StopSpeech. changes to: Commander Polle Zellweger (PTZ) September 18, 1987 2:47:44 pm PDT Fix bug in icon painting: icon & label fields not set if Finch not iconic. changes to: PaintFinchIcon Polle Zellweger (PTZ) October 29, 1987 2:32:37 pm PST Catch BasicTimeImpl.OutOfRange in case Thrush sends a bogus convDesc; also make sure conversation logging can handle bogus convDescs & missing Finch viewer. changes to: CallDateAndTime, DIRECTORY, SelectEntryInConversations Swinehart, February 16, 1988 11:11:24 pm PST Change to use new PartyInfo indexing scheme. Generally improves things. changes to: finchEnabledAtCheckpoint, Which, RedialIt, ReportConversationState, CallerAndOrCallee, DescribeParty, SetContents, SimplifyName, PaintFinchIcon, SuppressMultipleOutsideRingEntries, SuppressServiceEntries Polle Zellweger (PTZ) February 28, 1988 12:07:39 pm PST Add Comment|EndComment toggle menu button for meeting commentator. changes to: DIRECTORY, listenEntry, commentEntry, MakeListener, MakeCommentator, MakeMenus Swinehart, December 1988 Improve interaction between working directories and TDir command. Polle Zellweger (PTZ) January 13, 1989 6:04:19 pm PST Fix AddressFault in ReportRequestState if request=NIL; scroll conv log to view current conv (requested by Pavel); add Answer command (requested by Pier); catch TiogaButtons error if user destroys Finch viewer as a call is coming in. changes to: DIRECTORY, FinchToolImpl, AnswerCmd, ReportRequestState, MakeFinchTool, AddConvDesc, SelectEntryInConversations, ScrollConversations, ScrollConvsOnOpen, initialization Polle Zellweger (PTZ) March 25, 1989 2:34:54 pm PST Answer automatically if subject=Announcement and no higher-priority call is active. changes to: ReportConversationState Polle Zellweger (PTZ) March 29, 1989 11:13:43 pm PST Creature comforts for above, but without changing interfaces. changes to: ReportConversationState, doAnnounce, AnnounceOnCmd, AnnounceOffCmd Polle Zellweger (PTZ) June 9, 1989 4:33:53 pm PDT Trying to fix Finch conversation lockups. Change to use TeditScrolling.AutoScroll changes to: DIRECTORY, ScrollConversations Polle Zellweger (PTZ) June 13, 1989 11:40:41 am PDT Also wait for feedback selection to be made. changes to: DIRECTORY, ScrollConversations, feedbackPause, WaitForFeedback Polle Zellweger (PTZ) June 26, 1989 10:49:16 am PDT FORK process from ScrollConvsOnOpen, so column lock held by Viewers will be released. Also remove scroll event registration on UnFinch. changes to: UnfinchOnDestroy, MakeFinchTool, ScrollConversations, ScrollConvsOnOpen, ScrollConvsOnOpen Swinehart, September 8, 1990 5:46:38 pm PDT Added previousSituation field to FinchSmarts; remove hack. Swinehart, May 22, 1992 5:04:41 pm PDT Add Polle Zellweger's fix to SuppressServiceEntries -- more tests to avoid NIL-faults and the like. ĘCX•NewlineDelimiter –(cedarcode) style™– "Cedar" stylešœ™Icode– "Cedar" stylešœ ÏeœC™NJ™4J™3Kšœ3™3K™—šÏk ˜ Kšœ žœžœ˜1Kšœžœ˜%Kšœ žœ#˜2Kšœ žœ˜$Kšœ žœ1˜AKšœžœ3˜@šœ žœ˜KšœÂ˜Â—Kšœžœ:˜PKšœ ˜ Kšžœžœ7˜?Kšœžœ!˜,Kšžœ˜Kšœžœ ˜Jšœžœ ™KšœžœE˜RKšœ}˜}Jšœžœ ™Kšœžœ;˜HKšœžœažœ˜‚Kšœžœ ˜Kšœ žœ˜/Kšœžœ˜%Kšœ žœ˜*Kšœ žœ˜!Kšœ žœ˜Kšœžœ˜$Kšœžœ0žœ5˜sKšœ žœœ˜ŪKšœ žœ…˜“Kšœ žœ ˜Kšœ žœ@˜QKšœžœ˜#Kšœžœ˜*Kšœ žœ7˜IKšœ žœ˜'Kšœ žœ˜)Kšœ žœP˜_Kšœ žœ-˜>Kšœ žœT˜eKšœ žœE˜UKšœ˜K˜—šÐbl œž˜(Kšžœmžœ žœė˜íKšžœ˜Kšžœžœ˜K˜—Idefault™ÓL™L™ķL™™J™Kšœ žœ˜&Kšžœžœ žœ˜Kšœžœ˜Kšžœžœžœ˜Kšœžœ˜$K˜Kšœžœ˜)Kšœžœ˜ Kšœžœžœ˜!K˜Kš œ žœžœžœžœ˜MKšœ žœ"˜4K˜#K˜$K˜#K˜'K˜(K˜+K˜*K˜*K˜K˜+K˜/K˜0K˜K˜K˜/K˜Kšœžœ˜Kšœ žœ˜Kšœžœ˜0Kšœžœžœ˜'Kšœžœžœ˜"Kšœžœ˜>K˜K˜5K˜—™J™•StartOfExpansion‚ -- [parent: REF ANY, clientData: REF ANY _ NIL, mouseButton: Menus.MouseButton _ red, shift: BOOL _ FALSE, control: BOOL _ FALSE]šÏn œÏc6˜XJšÐck~™~Kšœžœ&˜@šœ˜šžœ ž˜Kš œžœ žœžœžœ˜3——K˜K˜—–‚ -- [parent: REF ANY, clientData: REF ANY _ NIL, mouseButton: Menus.MouseButton _ red, shift: BOOL _ FALSE, control: BOOL _ FALSE]š œĄ#˜KJšĒ~™~Kšœžœ:˜Tšžœ ž˜KšœAžœ˜FKšœ#žœ˜*Kšœ!žœ˜'Kšžœžœ˜—Kšœ˜K˜—–‚ -- [parent: REF ANY, clientData: REF ANY _ NIL, mouseButton: Menus.MouseButton _ red, shift: BOOL _ FALSE, control: BOOL _ FALSE]š œĄ$˜MJšĒ~™~Kšœžœ;˜Všžœ ž˜KšœBžœ˜GKšœ$žœ˜+Kšœ"žœ˜(Kšžœžœ˜—Kšœ˜K˜—š œĄ˜@Kšžœžœžœ˜!KšœžœE˜_K˜K˜Kšœ žœ˜K˜K˜—š  œĄ˜BK˜K˜K˜J™—š  œžœžœžœžœ˜HKšœ!žœ˜&Kšœžœ˜šžœžœžœžœ˜WKšžœžœ˜ —K˜ZKšœ/˜/šžœMž˜SKšœM˜M—Kšœ%žœžœ˜>šžœžœ5˜NKšĄ)˜)—šœK˜KJ™E—K˜—K˜š  œĄ˜BKšžœžœžœ˜!K˜šžœž˜$KšœBžœ˜I—Kšœ žœ˜K˜K˜—š œžœžœĄ.˜Všžœžœž˜Kšœ6˜6Kšžœ˜—Kšœ˜Kšœ˜J™—š œžœžœĄ.˜Tšžœžœž˜Kšœ*˜*Kšžœ˜—Kšœ$žœ˜*Kšœ˜J™—š  œžœžœžœžœ˜9K˜6šžœ žœžœ-žœ˜IKšœ$žœ˜.—šžœžœž˜˜ šžœžœžœ/ž˜LKšœ˜—šžœžœž˜)Kšœ4˜4—K˜—šœžœ˜Kš œžœžœžœžœ ˜#Kšœžœ˜ Kšœžœ˜ Kšœ žœ˜Kš  œžœ žœžœ žœžœ ˜FKšœ žœžœ Ą˜KKšœ žœžœ Ą˜KKšœ žœžœ Ą ˜MKšœ žœžœ Ą˜SKšœ žœžœ Ą˜RKšžœžœžœžœ˜K˜(Kšžœ žœžœ˜Kšžœ#žœ˜1Kšžœžœ!žœ˜4Kšžœžœ$žœ˜7Kšžœžœ˜ K˜2K˜3Kšžœžœ˜CK˜—Kšžœ˜—Kšœ˜—K˜š œ˜"K˜6K˜—K˜š  œĄ˜BJšœ[™[Kšžœžœžœ˜!Kšœ$˜$K˜Kšœ˜Kšœ žœ˜K˜K˜—š œžœĄ*˜NJšœ[™[Kšœ$˜$Kšœ˜Kšœ˜K˜—š  œžœžœĄ.˜PKšœžœ ˜!K˜—š œžœžœžœ˜0Kšžœ žœžœžœžœžœžœžœ˜ešžœ˜Kšœžœ$žœ˜0—Kšœ<˜Kšœ˜—˜?Kšœ žœC˜R—šžœ žœ˜K˜K˜FK˜—Kšœ˜K˜—š œžœ žœžœ˜VKšœ˜K˜3šžœ=ž˜BK˜2Kšœžœ˜1šœžœžœž˜šœ!˜!Kšœ˜Kšœ˜Kšœžœžœžœ ˜CKšœ˜—Kšœžœ3˜E—Kšžœ˜Kšžœžœ3˜HK˜>Kšœ žœ˜Kšžœžœžœ˜Kšžœ˜—K˜K˜—K˜Kš œ+˜9š œ˜*Kšžœžœžœ˜!K˜Kšœ˜Kšœ žœ˜Kšœ˜K˜—š  œžœ˜Kšœžœ˜Kšžœžœžœžœ˜3K˜$šžœ ž˜K˜W—K˜—J™—™J™š œ˜"Kšžœžœžœ˜!KšœžœP˜eK˜K˜K˜Kšžœžœžœ:˜KKšžœ*˜.Kšœ žœ˜K˜K˜—š   œž œžœžœ žœ˜?J™‰J™@š   œžœžœ žœžœžœ˜Kšžœžœžœ˜@Kšžœžœžœ!˜DKšœQ˜QKšœžœ˜Kšœ˜K˜—K˜š  œžœžœ0žœžœ žœžœžœ žœžœ˜‰JšœRžœÁ™˜Kšœžœ˜Kš žœžœžœžœĄ˜8šžœ.žœ˜HKšžœ˜—K˜4Kšœ žœžœ žœ ˜žœ˜Y——Kšžœ žœ%˜6Kšžœžœžœ)˜JKšœ˜K˜—šĨ œžœžœ žœ˜=K˜7šžœ!žœ˜)šœžœž˜Kšœ žœ'˜9Kšœ˜Kšœ3Ą ˜@KšžœĄ ˜&—K˜—Kšžœžœžœ˜ Kšœ˜K˜—šĨ œžœžœ žœ˜AKš œžœžœžœžœ˜Kšœžœ=˜RK˜Kšœ˜K˜—š  œžœžœ žœ˜?K˜Kšœžœ˜Kš žœžœžœžœĄ)˜HK˜Kšžœžœžœžœ˜˜K˜X—K˜-K˜K˜K˜—š  œžœDžœžœžœ˜†Jšœožœ*ĪœUĪœ™ôK˜$šžœž˜$šœ ˜ šœžœž˜*K˜$K˜K˜)Kšžœž˜Kšœ˜——šœ ˜ Kšžœ$žœ&˜P—šœžœ%žœžœ˜Ušœ žœ˜KšœžœO˜gK˜0K˜——Kšžœ˜—K˜—J™J™š  œžœ˜Kšœ˜Kšœžœžœ˜$K˜Kšžœžœžœ˜"Kšœžœ˜K˜+š žœžœžœžœž˜(KšœR˜RKšœ!žœ˜-—Kšœ žœžœ žœ˜Ošžœžœž˜4Kšœžœ%˜D—K˜J™—J™Íš  œžœ<˜MKšœ7žœ˜=Kšœžœ˜ K˜K˜—š  œžœ!žœžœ˜KKšœžœ˜Kšœ+žœ ˜:Kšœžœ˜Kš žœžœžœžœžœ˜+Kšžœžœ˜)K˜K˜—™VJ™ÞJ™RJ™i—š  œžœžœžœ žœ˜9Kšœžœ˜ Kšœžœžœ˜Kš œ+žœžœ˜CK˜ šžœ#ž˜*Kš œžœžœžœžœ˜!Kš œžœžœžœžœ˜'K˜DK˜Kšžœ˜—K˜—K˜—™"J™™ K˜Kšœ žœ˜Kšœ žœ˜Kšœ žœ˜Kšœ žœ$Ģ œë˜§šœžœ˜$K˜—š  œžœž˜Kšœ žœ˜Kšœ žœ˜K˜ Kšœ˜K˜ Kšœžœ.žœ˜GKšœžœžœ˜ K˜(šžœžœžœ˜Kšœ3žœ˜=—Kšœžœ˜.K˜K˜&K˜ šœ+Ðac˜=KšœĄ!˜4KšœĄ˜.K˜KšœĄ=˜UKšœĄ˜0KšœĄ ˜0KšœĄ ˜,Kšœ žœžœĄ'˜>—K˜ Kšœ žœžœžœ˜^˜"Kšœ;žœ˜A—˜/Kšœ9žœ˜@—K˜YK˜HKšœsžœ˜zK˜AK˜4K˜JšœM™MK˜ šžœ*žœ˜2Kšœ1˜1Kšžœ žœ#˜4K˜—K˜'Kšœ1žœ˜DK˜(˜,šœ˜Kšœ#žœ˜,——Kšœ:˜:Kšœ+˜+Kšœ+˜+K˜š žœžœžœžœžœž˜Kšœžœ˜ Kšœ žœ˜K˜Kšžœžœžœ˜Kšžœžœžœžœ˜Kšœpžœ*žœž˜ŽKšœ#žœ˜)Kšžœ˜—K˜Kšžœžœžœ@˜YKšœ"Ą˜9K˜šžœžœ˜&KšœM˜M—šžœžœ˜(šœOž˜SK˜——K˜Kšœ;˜;Kšžœ˜K˜—š  œžœ˜J™š  œžœžœ˜Išœ˜Kšœ ˜ KšœG˜GKšœ˜—K˜—K˜Kšœ+˜+Kšœ)˜)Kšœ-˜-Kšœ7˜7Kšœ5˜5Kšœ3˜3Kšœ3˜3K˜5K˜@šœ4™4JšĪœ{Ī™}—Kšœ˜K˜—šĢœ˜+Kšžœžœžœ˜!K˜Kšœ˜Kšœ žœ˜Kšœ˜——K˜J™ ™š  œžœžœ˜5K˜K˜—š  œž œ˜8–[]™J™F—š   œžœžœžœžœ˜KKšžœžœžœžœ˜AKšžœžœ˜—K˜"Kšœžœ˜Kšœžœ˜Kšœžœžœ/˜EKšžœžœžœžœ˜š žœžœžœ$žœžœž˜DK˜ž˜§Kšœ0žœ™Išžœ!ž˜+Kšœžœžœ˜šœ˜K™ÆKšžœžœžœžœ+žœ>žœžœžœ˜ĩK˜—KšĄ œžœ˜——Kšœ˜K˜—š œž œ2žœžœ˜iKš œžœžœžœžœžœ˜Tšœ)žœžœžœž˜?Kšžœžœ.˜9—Kšžœžœžœžœ˜!š žœžœžœžœ#žœ(žœ˜rKšœH˜HKšœ*žœ˜/K˜—šžœ"ž˜(Kšœ1žœ˜<—Kšžœžœ žœžœ˜šœEĄ˜ZJšœ{™{—Kšœ7˜7Kšœ)˜)K˜K˜—š œžœ7˜PKšœ˜Kšœ˜Kšœ˜Kšœžœ˜!K˜[Kšœc˜cKšœ$˜$Kšœ7žœžœ˜^šžœ žœžœĄ&˜:Kšœ™žœ˜Ī—šžœĄ˜K˜#—Kšœ˜K˜—šĨœ˜-Jš žœ.žœžœ žœžœ™VKšœ!˜!Kš œžœžœžœžœžœ˜UKšžœ žœžœžœ˜Kšœ žœ/˜>Kšžœžœžœ žœ,˜Nšžœžœžœžœ˜KšžœA™EKšœžœ'žœžœ˜PK˜—Kšœ˜K˜—šœžœ˜K˜—šĨœžœ4˜IKšœ˜Kšœ ˜ š žœžœžœžœĄ˜1K˜:Kš žœžœžœ žœžœ˜:Kšœ2˜2Kšžœ˜—Kšœ˜K˜—šĨ œžœ4žœžœ˜WKšžœ:žœ˜EKšœ˜K˜—šĨœž œ˜7Kšœ"žœ˜˜>—K˜Kšœ˜K˜—šĨœž œ˜1Kšœ"žœ˜˜>š  œž œ˜Kšœžœ(˜DKš œžœžœžœ žœžœ žœ˜;Kš œ žœžœ3žœžœ˜SKšžœ˜—K˜Ušœ&˜&Kšžœžœžœ˜"šžœ žœĄ˜-KšœžœžœžœĄ ˜BKšœ žœžœžœ˜5Kšœ˜—šžœĄ-˜2K˜—šžœ˜KšœN˜N—Kšœ˜—K˜š  œž œ˜Kšœžœ˜7Kš œžœžœžœ žœžœ žœ˜KKšœ žœžœ)˜=Kšžœ˜˜Kšœ?˜?Kšœžœžœžœ˜>—Kšžœžœžœ˜%˜KšœN˜N—Kšœ˜K˜—š   œžœžœžœžœ˜—šœ4™4Kšœ ŠA™M—šœ6™6Kšœš™šKšœ Šg™s—™,KšœF™FKšœ;™;K™CK™4K™8K™-K™ZK™•—™4K™IKšœ Š…™‘—™5K™‘Kšœ Šn™z—šœ5™5K™IKšœ Š1œ™C—™*K™œ—™5Kšœ Š'™3—™7Kšœ Š™—™6Kšœ Š™—™6Kšœ Š ™—™6Kšœ Š ™—™7Kšœ Š ™,—™6Kšœ Š™—™3K™7Kšœ Šh™t—™2K™LKšœ Š™œŠC™†—™2K™™J—™3Kšžœ„™ˆKšœ Š4œŠ%™f—K™™+K™:—™&K™c—K™—…—ÝXuÜ