FinchToolImpl.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
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],
List USING [Reverse],
MBQueue USING [ Create, CreateButton, CreateMenuEntry, Queue, QueueClientAction ],
Menus USING [AppendMenuEntry, CreateEntry, CreateMenu, FindEntry, Menu, MenuEntry, MenuProc, MouseButton, ReplaceMenuEntry ],
Nice USING [ View ],
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;
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
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.ROPENIL;
printLabel: ARRAY Thrush.StateInConv OF Rope.ROPEALL["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: BOOLFALSE;
deleteButtonEnabled: BOOLFALSE;
Which: TYPE = { self, other, originator, moderator };
Placing and Controlling Calls
PhoneProc: Buttons.ButtonProc = { -- Place call to party identified by primary selection
[parent: REF ANY, clientData: REF ANY ← NIL, mouseButton: Menus.MouseButton ← red, shift: BOOL ← FALSE, control: BOOL ← FALSE]
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
[parent: REF ANY, clientData: REF ANY ← NIL, mouseButton: Menus.MouseButton ← red, shift: BOOL ← FALSE, control: BOOL ← FALSE]
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
[parent: REF ANY, clientData: REF ANY ← NIL, mouseButton: Menus.MouseButton ← red, shift: BOOL ← FALSE, control: BOOL ← FALSE]
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: BOOLFALSE] = {
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];
No Conversation ID needed here because assumed not active in any now.
};
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: BOOLFALSE] = {
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
Refers to entries in the viewer finchToolHandle.conversations, not necessarily current one.
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
Refers to entries in the viewer finchToolHandle.conversations, not necessarily current one.
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]; };
Refers to entries in the viewer finchToolHandle.conversations, not necessarily current one.
HangupQuietly: PUBLIC Menus.MenuProc = { []←HangItUp[complain: FALSE, action: mouseButton]; };
Called to disconnect a call in progress without logging a message.
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: BOOLTRUE, moreToCome: BOOLFALSE, cDesc: ConvDesc←NIL, action: Menus.MouseButton←$red] RETURNS [pauseNeededInMs: CARDINAL𡤀] = 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 = {
PROC [button: TiogaButton, clientData: REF ANYNIL,
mouseButton: ViewerClasses.MouseButton ← red, shift, control: BOOLFALSE]
Button-invoked operations when applied directly to conversation-viewer log entries.
IF deleteButtonEnabled AND control AND shift AND mouseButton=blue THEN {
Hack for controlling contents of conversation viewer.
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] = {
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 "<logged-in user> 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 <number>" says you want to call the specified number, and the recipient is XXX (not implemented yet.)
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�llee.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: BOOLTRUE] 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 = {
[parent: REF ANY, clientData: REF ANY ← NIL, mouseButton: Menus.MouseButton ← red, shift: BOOL ← FALSE, control: BOOL ← FALSE]
Menus.ReplaceMenuEntry[finchToolHandle.outer.menu, endCommentEntry, commentEntry];
ViewerOps.PaintViewer[finchToolHandle.outer, menu];
Status["Listening only enabled"];
};
MakeCommentator: Buttons.ButtonProc = {
[parent: REF ANY, clientData: REF ANY ← NIL, mouseButton: Menus.MouseButton ← red, shift: BOOL ← FALSE, control: BOOL ← FALSE]
Menus.ReplaceMenuEntry[finchToolHandle.outer.menu, commentEntry, endCommentEntry];
ViewerOps.PaintViewer[finchToolHandle.outer, menu];
Status["Permission to comment granted"];
};
Speaking Text
speakTextFeedbackLen: INT ← 40;
dectalkParaSep: Rope.ROPE ← " \033P;z.+\033\\";
SpeakSelectedProc: Buttons.ButtonProc = {
queueIt: BOOLTRUE;
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: ROPENIL, convID: Thrush.ConversationID←Thrush.nullConvID]
RETURNS [newConvID: Thrush.ConversationID] ~ {
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.
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�talkParaSep] ~ {
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𡤏inchSynthesizer.StopSpeech[].nb;
IF nb#$success THEN
Report["Sorry, trouble with text to speech service; could not Stop. (%g)", [atom[nb]]];
};
Other User Commands
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] = {
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.
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 ];
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
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];
try first time w/o password
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 ← "";
IF Rope.Length[visitor] = 0 THEN {}; -- no name => self
No password for unvisiting, either.
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;
};
Starting and Stopping
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: BOOLTRUE] = 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: BOOLFALSE, complain: BOOLTRUE] RETURNS[active: BOOLFALSE] = {
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.
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𡤌onnected;
finchToolHandle.finchEnabledAtLastReport𡤎nabled;
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: BOOLFALSE;
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;
};
Conversation Management
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: BOOLFALSE;
state: Thrush.StateInConv;
SELECT nb FROM $success => NULL; ENDCASE => { Status[remark]; RETURN; };
Don't use conversation buttons for failure reports
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
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).
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]];
The following reinstated October 3, 1989 4:55:17 pm PDT; announcements don't work; see below.
IF finchToolHandle.backgrounding THEN SELECT state FROM
$notified => {
selectedDesc: ConvDesc ← GetSelectedDesc[checkActive: FALSE];
acceptNew: BOOLTRUE;
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
];
Now report the original state; subsequent reports will cause our changes to be reflected.
};
$idle, $inactive => {
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.
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
Backgrounding completely disabled October 3, 1989 4:54:53 pm PDT; for further study.
SELECT state FROM
$notified => {
selectedDesc: ConvDesc ← GetSelectedDesc[checkActive: FALSE];
acceptNew: BOOLTRUE;
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
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: BOOLTRUE]
RETURNS [who: ROPE←""] = {
Use empty string rather than NIL ROPE to prevent Viewers from using Viewer name as a backup for a NIL label.
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 statusprintLabel[state];
};
CallDuration: PROC [cDesc: ConvDesc] RETURNS [duration: ROPE] = {
s: IO.STREAMIO.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] = {
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. .
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;
};
Describe the other party.
DescribeParty: PROC[
cDesc: ConvDesc,
indicateUnauthenticated: BOOLFALSE,
which: Which ← $other]
RETURNS [otherParty: ROPENIL] = {
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["?"];
};
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.
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];
};
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.
RepairIntelnet: PROC[num: ROPE] RETURNS[number: ROPE] = {
i: INT←-1;
pause: BOOLFALSE;
M: Rope.ActionType={i←i+1; pause ← c<'\040; RETURN[pause OR c='#]};
number ← num;
WHILE number.Map[0, number.Length[], M] DO
len: INTIF pause THEN 2 ELSE 1;
repl: ROPEIF pause THEN "*" ELSE "";
number ← Rope.Replace[base: number, start: i, len: len, with: repl];
i←-1;
ENDLOOP;
};
Viewer Construction and Management
Finch Viewer
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];
Check how much space is left (if any) in the control window for a typescript.
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 = {
Finch Tool Viewer
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];
MakeOneMenu[finchMenu, "Drop Out", ButtonStopFinch];
Removed for now; use "Unfinch". Should be replaced by toggle that also serves
to indicate current enabled/connected state.
};
MakeFinchToolCmd: Commander.CommandProc = {
ENABLE UNWIND => cmdHandle ← NIL;
cmdHandle ← cmd;
MakeFinchTool[];
cmdHandle ← NIL;
};
Finch Icon
UpdateIconE: ENTRY PROC[handle: FinchTool.Handle] = {
UpdateIcon[handle]; };
UpdateIcon: INTERNAL PROC [handle: FinchTool.Handle] = {
Find the "most-active" cDesc
A nit: $inactive > $active, but must rank below all other real states.
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: BOOLTRUE; -- 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];
};
Conversations subviewer
UpdateConvLog: INTERNAL PROC [cDesc: ConvDesc, logEntry: ROPENIL] = {
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: ROPENIL, bFormat: ROPENIL, bLooks: ROPENIL]
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];
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.
IF lastDesc#NIL AND lastDesc.reportComplete
AND (lastDesc.ultimateState=$reserved OR lastDesc.ultimateState=$parsing
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.)
OR SuppressMultipleOutsideRingEntries[cDesc, lastDesc]
OR SuppressServiceEntries[cDesc, lastDesc] )
THEN newButton ← lastButton;
};
IF newButton#NIL THEN {
RelabelTiogaButton[button: newButton, newRope: bRope, newLooks: bLooks];
no way to specify new format for new rope!
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];
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.
};
SuppressServiceEntries: INTERNAL PROC [currDesc, lastDesc: ConvDesc]
RETURNS [BOOLFALSE]= {
IF lastDesc.numParties>1 AND lastDesc.partyInfo#NIL AND lastDesc.partyInfo.ixOther#ThParty.nullIx AND lastDesc.partyInfo[lastDesc.partyInfo.ixOther].type=$service THEN
desc.numParties>1 does not imply desc.partyInfo#NIL in pathological cases
SELECT finchToolHandle.logServiceCalls FROM
none => RETURN[TRUE];
one => {
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.
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: BOOLTRUE] = {
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
do it even if prevSelected = entryButton, because we may have relabeled the previous one inbetween, which will remove looks
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 = {
PROC [viewer: Viewer, event: ViewerEvent, before: BOOL]
RETURNS [abort: BOOLFALSE]
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 {
FORK process to avoid deadlock with column lock held by ViewerEvents.
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"];
};
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)
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;
};
Directory
MakeDirectory: Menus.MenuProc = {
FinchTool.BuildDirectories[];
};
Viewer and Button Utilities
RelabelTiogaButton: INTERNAL PROC [button: TiogaButtons.TiogaButton,
newRope: ROPENIL,
start: INT𡤀, len: INTINT.LAST, newLooks: ROPENIL] = {
Replaces the rope from [button.startLoc.where+start..button.startLoc.where+start+len] with newRope, using the specified new looks
node, rootNode: TextNode.Ref;
newLen: INT𡤀
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: BOOLTRUE, guarded: BOOLFALSE, newLine: BOOLFALSE]
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: BOOLFALSE, newLine: BOOLFALSE]
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 ANYNIL, border: BOOLFALSE, width: INTEGER← 0,
guarded: BOOLFALSE, font: VFonts.Font ← VFonts.defaultFont, newLine: BOOLFALSE]
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 ANYNIL, border: BOOLFALSE, width: INTEGER← 0,
guarded: BOOLFALSE, 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];
};
Creates a text viewer, next right on the same line as sib
sib must be a Viewer, not NIL
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] = {
Make an h-bit wide line after sib
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];
};
Sib is sibling to create TS after
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.ChildYBound[sib.parent, ts];
Containers.ChildXBound[sib.parent, ts];
};
Reporting and Logging
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: ROPENIL] = {
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 {
Nice.View[finchToolHandle, "FinchTool PD"];
};
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: BOOLTRUE; -- belongs in finchToolHandle
AnnounceOnCmd: Commander.CommandProc = TRUSTED {
doAnnounce ← TRUE;
};
AnnounceOffCmd: Commander.CommandProc = TRUSTED {
doAnnounce ← FALSE;
};
Registration, Initialization
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];
Register a command with the UserExec that will create an instance of this tool
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."];
Debugging nonsense
Commander.Register["VuFinchTool", ViewCmd,
"Program Management variables for FinchTool"];
}.
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.