DIRECTORY BasicTime USING [ GMT, Now, OutOfRange, Period ], Booting USING [ CheckpointProc, RollbackProc, RegisterProcs ], Buttons USING [ ButtonProc, Create ], Commander USING [ CommandProc, Handle, Register ], CommandTool 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, ExpandName ], 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, 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, TextNodeRef, TiogaButton, TiogaButtonProc, TiogaOpsRef, 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, Booting, Buttons, Commander, CommandTool, 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; 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 cDesc: ConvDesc _ NARROW[data]; IF cDesc#NIL THEN ShowRetriedCall[cDesc]; RedialIt[cDesc: cDesc]; }; RedialHomeCProc: PROC [data: REF] = { -- Redial from TiogaButton in conversation log cDesc: ConvDesc _ NARROW[data]; IF cDesc#NIL THEN ShowRetriedCall[cDesc]; RedialIt[cDesc: cDesc, wantResidence: TRUE]; }; RedialIt: PROC [cDesc: ConvDesc, wantResidence: BOOL_FALSE] = { activeCDesc: FinchSmarts.ConvDesc = GetSelectedDesc[]; IF activeCDesc#NIL AND activeCDesc.situation.self.state > $parsing THEN { Report["Call already in progress"]; RETURN; }; IF cDesc.partyInfo=NIL OR cDesc.partyInfo.ixOriginator = ThParty.nullIx THEN Report["Unknown caller/callee"] ELSE DoPhone[SimplifyName[cDesc, $other], wantResidence]; }; JoinCmd: Commander.CommandProc = { [] _ FinchSmarts.Join[CommandTool.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 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.Cat[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.Put[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.PutF[cmd.out, "password: %l", IO.rope["h"]]; password _ IO.GetLineRope[cmd.in]; IO.PutF[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 _ CommandTool.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 _ CommandTool.NextArgument[cmd]; StartFinch[]; cmdHandle _ NIL; }; ReFinchOnRollback: Booting.RollbackProc = { IF --finchToolHandle=NIL OR-- ~--finchToolHandle.--finchEnabledAtCheckpoint THEN RETURN; --finchToolHandle.--finchEnabledAtCheckpoint _ FALSE; Try[StartFinch, 0]; }; 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[]; ViewerEvents.UnRegisterEventProc[proc: finchToolHandle.scrollEvent, event: open]; finchToolHandle _ NIL; }; UnFinchOnCheckpointOrBoot: Booting.CheckpointProc = { enabled: BOOL; IF finchToolHandle=NIL THEN RETURN; enabled _ FinchSmarts.FinchIsRunning[].finchIsEnabled; --finchToolHandle.--finchEnabledAtCheckpoint _ --finchToolHandle.--finchEnabledAtCheckpoint OR enabled; IF enabled THEN Try[StopF]; }; StopF: PROC = { IF finchToolHandle#NIL THEN ViewerOps.DestroyViewer[finchToolHandle.outer]; }; 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.PutFR["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%g", rope[CallDateAndTime[cDesc]], rope[CallStatus[cDesc]], rope[CallDuration[cDesc]], 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.PutF["%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.PutF[" (%s)", rope[cDesc.situation.comment]]; IF remark#NIL THEN s.PutF[" [%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.Cat["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.PutF["%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]; 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]; 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; }; 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]; IF Rope.Find[TiogaButtons.GetRope[button], "completed"] = -1 THEN TiogaButtons.ChangeButtonLooks[button: button, addLooks: "i"]; }; 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, inherit: FALSE, looks: TextLooks.RopeToLooks[newLooks]]; }; IF button#NIL THEN { node _ TiogaButtons.TextNodeRef[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, TiogaButtons.TiogaOpsRef[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]; }; 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"; Booting.RegisterProcs[c: UnFinchOnCheckpointOrBoot, r: ReFinchOnRollback, b: UnFinchOnCheckpointOrBoot]; Booting.RegisterProcs[c: UnFinchOnCheckpointOrBoot, r: ReFinchOnRollback, b: UnFinchOnCheckpointOrBoot]; Commander.Register["AnnounceOn", AnnounceOnCmd, "Allow announcement calls."]; Commander.Register["AnnounceOff", AnnounceOffCmd, "Disallow announcement calls."]; Commander.Register["VuFinchTool", ViewCmd, "Program Management variables for FinchTool"]; }. RTFinchToolImpl.mesa Copyright Σ 1985, 1987, 1988, 1992 by Xerox Corporation. All rights reserved. Last Edited by: Swinehart, September 22, 1990 2:53 pm PDT Last Edited by: Pier, April 17, 1984 3:51:54 pm PST Polle Zellweger, February 10, 1992 4:12 pm PST 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]; 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. Polle Zellweger (PTZ) June 26, 1989 10:49:16 am PDT changes to: SuppressServiceEntries Fix long-standing bug dereferencing NIL due to incorrect interpretation of ConvDesc invariants. ΚAD•NewlineDelimiter – "cedar" style˜– "Cedar" stylešœ™Icode– "Cedar" stylešœN™NJšœ9™9J™3K™.K™—šΟk ˜ Jšœ œœ˜1Jšœœ1˜>Jšœœ˜%Jšœ œ#˜2Jšœ œ˜#Jšœ œ1˜AJšœœ3˜@šœ œ˜JšœΒ˜Β—Jšœœ:˜PJšœ ˜ Kšœœ$˜,Jšœœ!˜,Jšœ˜Jšœœ ˜Jšœœ ™JšœœE˜RJšœ}˜}Jšœœ ™Jšœœ;˜HJšœœaœ ˜zJšœœ ˜Jšœ œ˜/Jšœœ˜%Jšœ œ˜*Jšœ œ˜!Jšœ œ˜Jšœœ˜$Jšœœ0œ5˜sJšœ œΆ˜ΘJšœ œ…˜“Jšœ œ ˜Jšœ œ@˜QJšœœ˜#Jšœœ˜*Jšœ œ7˜IJšœ œ˜'Jšœ œ˜)Jšœ œP˜_Jšœ œ-˜>Jšœ œT˜eJšœ œE˜UJšœ˜J˜—šΠbl œ˜(Jšœuœ œμ˜υJšœ˜Jšœœ˜J˜—Idefault™ΣL™L™ΆL™™J™Jšœ œ˜&Jšœœ œ˜Jšœœ˜Jšœœœ˜Jšœœ˜$J˜Jšœœ˜)Jšœœ˜ Jšœœœ˜!J˜Jš œ œœœœ˜MJšœ œ"˜4J˜#J˜$J˜#J˜'J˜(Jšœ+˜+J˜*Jšœ*˜*J˜Jšœ+˜+Jšœ/˜/Jšœ0˜0J˜J˜J˜/J˜Jšœœ˜Jšœ œ˜Jšœœ˜0Jšœœœ˜'Jšœœœ˜"J˜J˜5J˜—™J™•StartOfExpansion‚ -- [parent: REF ANY, clientData: REF ANY _ NIL, mouseButton: Menus.MouseButton _ red, shift: BOOL _ FALSE, control: BOOL _ FALSE]šΟn œΟc6˜XJšΠck~™~Jšœœ&˜@šœ˜šœ ˜Jš œœ œœœ˜3——J˜J˜—–‚ -- [parent: REF ANY, clientData: REF ANY _ NIL, mouseButton: Menus.MouseButton _ red, shift: BOOL _ FALSE, control: BOOL _ FALSE]šŸœ #˜KJš‘~™~Jšœœ:˜Tšœ ˜JšœAœ˜FJšœ#œ˜*Jšœ!œ˜'Jšœœ˜—Jšœ˜J˜—–‚ -- [parent: REF ANY, clientData: REF ANY _ NIL, mouseButton: Menus.MouseButton _ red, shift: BOOL _ FALSE, control: BOOL _ FALSE]šŸœ $˜MJš‘~™~Jšœœ;˜Všœ ˜JšœBœ˜GJšœ$œ˜+Jšœ"œ˜(Jšœœ˜—Jšœ˜J˜—šŸœ ˜@Jšœœœ˜!JšœœE˜_J˜J˜Jšœ œ˜J˜J˜—šŸ œ ˜BJšœ˜Jšœ˜J˜J™—š Ÿœœœœœ˜HJšœ!œ˜&Jšœœ˜šœœœœ˜WJšœœ˜ —JšœZ˜ZJšœ/˜/šœM˜SJšœM˜M—Jšœ%œœ˜>šœœ5˜NJš )˜)—šœK˜KJ™E—J˜—J˜šŸ œ ˜BJšœœœ˜!J˜šœ˜$JšœBœ˜I—Jšœ œ˜J˜J˜—šŸœœœ .˜VJšœœ˜Kšœœœ˜)Jšœ˜Jšœ˜J™—šŸœœœ .˜TJšœœ˜Kšœœœ˜)Jšœ&œ˜,Jšœ˜J™—šŸœœ"œœ˜?J˜6šœ œœ-œ˜IJšœ$œ˜.—šœœœ/˜LKšœ˜—Kšœ5˜9Jšœ˜—J˜šŸœ˜"K˜5K˜—K˜šŸ œ ˜BJšœ[™[Jšœœœ˜!Jšœ$˜$J˜Jšœ˜Jšœ œ˜J˜J˜—šŸœœ *˜NJšœ[™[Jšœ$˜$Jšœ˜Jšœ˜J˜—šŸ œœœ .˜PJšœœ ˜!J˜—šŸœœœœ˜0Kšœ œœœœœœœ˜ešœ˜Jšœœ$œ˜0—Jšœ<˜Kšœ˜—šœ?˜?Jšœ œC˜R—šœ œ˜Jšœ˜J˜FJ˜—Kšœ˜K˜—šŸœœ œœ˜VJšœ˜Kšœ3˜3šœ=˜BK˜2Kšœœ˜1šœœœ˜šœ!˜!Kšœ˜Kšœ˜Kšœœœœ ˜CKšœ˜—Kšœœ3˜E—Kšœ˜Kšœœ0˜EK˜>Kšœ œ˜Kšœœœ˜Kšœ˜—K˜K˜—J˜JšŸœ+˜9šŸœ˜*Jšœœœ˜!J˜Jšœ˜Jšœ œ˜Jšœ˜J˜—šŸ œœ˜Jšœœ˜Jšœœœœ˜3Jšœ$˜$šœ ˜J˜W—J˜—J™—™J™šŸœ˜"Jšœœœ˜!JšœœP˜eJ˜J˜J˜Jšœœœ:˜KJšœ*˜.Jšœ œ˜J˜J˜—š Ÿ œ œœœ œ˜?J™‰J™@š Ÿ œœœ œœœ˜Jšœœœ˜@JšœQ˜QJšœœ˜Jšœ˜J˜—šŸœ˜5Jšœ œ˜Jšœœœ˜#Jšœ6˜6š œ˜.Jš œœ ˜8—Jšœ œ ˜J˜šŸœœ˜Jšœœœ3˜N—J˜—J˜šŸ œœœ0œœ œœœ œœ˜‰JšœRœΑ™˜Jšœœ˜Jš œœœœ ˜8šœ.œ˜HJšœ˜—Jšœ4˜4Jšœ œœ œ ˜—J˜ Jšœ œœœ˜^šœ"˜"Jšœ;œ˜A—šœ/˜/Jšœ9œ˜@—JšœY˜YJ˜HJšœsœ˜zJ˜AJ˜4J˜JšœM™MJ˜ šœ*œ˜2Jšœ1˜1Jšœ œ#˜4J˜—J˜'Jšœ1œ˜DJ˜(šœ,˜,šœ˜Jšœ#œ˜,——Jšœ:˜:Jšœ+˜+Jšœ+˜+Jšœ" ˜9K˜šœœ˜&JšœM˜M—šœœ˜(šœO˜SJ˜——J˜Jšœ;˜;Jšœ˜J˜—šŸ œœ˜J™šŸ œœœ˜Išœ˜Jšœ ˜ JšœG˜GJšœ˜—J˜—Jšœ˜Jšœ+˜+Jšœ)˜)Jšœ-˜-Jšœ7˜7Jšœ5˜5Jšœ3˜3Jšœ3˜3Jšœ5˜5Jšœ@˜@šœ4™4Jš£œ{£™}—Jšœ˜J˜—š’œ˜+Jšœœœ˜!J˜Jšœ˜Jšœ œ˜Jšœ˜——J˜J™ ™šŸ œœœ˜5J˜J˜—šŸ œ œ˜8–[]™J™F—š Ÿ œœœœœ˜KJšœœœœ˜AJšœœ˜—J˜"Jšœœ˜Jšœœ˜Jšœœœ/˜EJšœœœœ˜š œœœ$œœ˜DJšœ<˜˜§Kšœ0œ™Išœ!˜+Kšœœœ˜šœ˜K™ΖKšœœœœ+œ>œœœ˜΅K˜—Kš  œœ˜——Jšœ˜J˜—šŸœ œ2œœ˜iJš œœœœœœ˜Tšœ)œœœ˜?Jšœœ.˜9—Kšœœœœ˜!š œœœœ#œ)œ˜rKšœH˜HJšœ*œ˜/J˜—šœ"˜(Jšœ1œ˜<—Jšœœ œœ˜šœE ˜ZJšœ{™{—Jšœ7˜7Jšœ)˜)J˜J˜—šŸœœ7˜PJšœ˜Jšœ˜Jšœ˜Jšœœ˜!Jšœ[˜[Kšœc˜cJšœ$˜$Jšœ7œœ˜^šœ œœ &˜:Kšœ™œ˜€—šœ ˜K˜#—Jšœ˜J˜—š€œ˜-Jš œ.œœ œœ™VKšœ!˜!Kš œœœœœœ˜UKšœ œœœ˜Kšœ œ/˜>Kšœœœ œ,˜Nšœœœœ˜KšœA™EKšœœ'œœ˜PK˜—Jšœ˜J˜—šœœ˜K˜—š€œœ4˜IJšœ˜Jšœ ˜ š œœœœ ˜1Jšœ:˜:Kš œœœ œœ˜:Kšœ2˜2Kšœ˜—Kšœ˜J˜—š€ œœ4œœ˜WKšœ:œ˜EJšœ˜J˜—š€œ œ˜7Jšœ"œ˜<šœ;˜AJšœ>˜>—Jšœ˜J˜—š€œ œ˜1Jšœ"œ˜˜>šŸ œ œ˜Jšœœ(˜DJš œœœœ œœ œ˜;Jš œ œœ3œœ˜SJšœ˜—JšœU˜Ušœ&˜&Jšœœœ˜"šœ œ ˜-Jšœœœœ  ˜BJšœ œœœ˜5Jšœ˜—šœ -˜2Jšœ˜—šœ˜JšœN˜N—Jšœ˜—J˜šŸ œ œ˜Jšœœ˜7Jš œœœœ œœ œ˜KJšœ œœ)˜=Jšœ˜šœ˜Jšœ?˜?Jšœœœœ˜>—Jšœœœ˜%šœ˜JšœN˜N—Jšœ˜J˜—š Ÿ œœœœœ˜—šœ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™:—™3Kšœ ©™"K™_——…—ΤBgΪ