DIRECTORY
Atom USING [ PropList ],
BasicTime USING [ GMT, Now, nullGMT, ToPupTime ],
GList USING [ Append, Member, Remove ],
IO,
Jukebox USING [ bytesPerMS ],
MessageWindow USING [ Append, Blink ],
Process USING [ MsecToTicks, Pause ],
Rope,
TEditSelectionOps USING [ ShowGivenPositionRange ],
TextEdit USING [ GetCharProp, PutCharProp, PutProp, Size ],
TextNode USING [ LocNumber, Ref, StepForward ],
TiogaAccess USING [ EndOf, FromViewer, Get, GetIndex, Reader, TiogaChar ],
TiogaButtons USING [ TextNodeRef, TiogaOpsRef ],
TiogaOps USING [ CallWithLocks, CommandProc, GetSelection, Location, LocOffset, LocRelative, NoSelection, Ref, RegisterCommand, SelectionGrain, SetSelection, ViewerDoc ],
ViewerClasses USING [ Viewer ],
ViewerOps USING [ FetchProp ],
ViewRec USING [ ViewRef ],
TiogaVoicePrivate USING [ PlaySelection, thrushHandle ],
VoiceRope USING [ Length, Stop, VoiceRopeInterval ]
;

NarratedDocsImpl: CEDAR PROGRAM
IMPORTS BasicTime, GList, IO, MessageWindow, Process, Rope, TEditSelectionOps, TextEdit, TextNode, TiogaAccess, TiogaButtons, TiogaOps, ViewerOps, ViewRec, TiogaVoicePrivate, VoiceRope
 = {


ScriptList: TYPE ~ LIST OF Script;

Script: TYPE ~ REF ScriptBody;
ScriptBody: TYPE ~ RECORD [
sid: ScriptID _ 0,
sDesc: ScriptDesc _ "",
sCreator: ScriptCreator _ "",
numEntries: INT _ 0,
firstEntry, lastEntry: ScriptEntry _ NIL,
file: FileID
];

ScriptID: TYPE = CARD;
ScriptDesc: TYPE ~ Rope.ROPE;
ScriptCreator: TYPE ~ Rope.ROPE;

ScriptEntry: TYPE ~ REF ScriptEntryBody;
ScriptEntryBody: TYPE ~ RECORD [
entryID: EntryID _ NIL,
file: FileID,
next, prev: ScriptEntry _ NIL,
pauseBefore: INT _ 0,  -- in msec (is sec more reasonable?)
pauseAfter: INT _ 0,  -- ditto
action: Rope.ROPE _ "",
charIndex: INT _ -1,
seqNum: INT _ -1  -- trust this only if document is not edited
];

EntryID: TYPE ~ REF EntryIDBody;
EntryIDBody: TYPE ~ RECORD [
sid: ScriptID _ 0,
eid: INT _ 0
];

EntryIDList: TYPE ~ LIST OF EntryID;

FileID: TYPE = RECORD [
name: Rope.ROPE _ "",
createTime: BasicTime.GMT _ BasicTime.nullGMT
];

ScriptTool: TYPE = REF ScriptToolBody;
ScriptToolBody: TYPE = RECORD [
scriptID: ScriptID _ 0,
seqNum: INT _ 0,
action: Rope.ROPE _ "",
addEntries: PROC,
deleteEntries: PROC,
findEntry: PROC,
nextEntry: PROC,
prevEntry: PROC,
playEntry: PROC,
playScript: PROC,
stop: PROC,
newScript: PROC,
listScripts: PROC,
msg: Rope.ROPE
];


scriptList: ScriptList _ NIL;


scriptStyleParam: Rope.ROPE _ "0 outlineBoxBearoff 1 outlineBoxThickness";

SuitableViewer: PROC [selectedViewer: ViewerClasses.Viewer] RETURNS [BOOLEAN] = { 
RETURN [selectedViewer.class.flavor = $Text AND ViewerOps.FetchProp[selectedViewer, $voiceViewerInfo] = NIL] };

InsertSelectedAnnotationsAfter: PUBLIC PROC [script: Script, seqNum: INT, action: Rope.ROPE _ NIL] RETURNS [newSeqNum: INT] ~ {
selectedViewer: ViewerClasses.Viewer;
suitableViewer: BOOLEAN;
voiceThere: BOOLEAN;

AddSelectedAnnotations: PROC [root: TiogaOps.Ref] = {
charsInSelection: INT _ 0;
soundsInSelection: INT _ 0;
startChar, endChar, targetChar: TiogaOps.Location;
node: TextNode.Ref;
caretBefore: BOOLEAN;
pendingDelete: BOOLEAN;
level: TiogaOps.SelectionGrain;

[viewer: selectedViewer, start: startChar, end: endChar, caretBefore: caretBefore, pendingDelete: pendingDelete, level: level] _ TiogaOps.GetSelection[];
suitableViewer _ SuitableViewer[selectedViewer];

IF suitableViewer AND pendingDelete THEN {
TiogaOps.SetSelection[viewer: selectedViewer, start: startChar, end: endChar,
level: level, caretBefore: caretBefore,
pendingDelete: FALSE, which: primary] -- simply makes not pending delete
};

IF suitableViewer AND NOT level = point THEN {
targetChar _ startChar;
DO
charsInSelection _ charsInSelection + 1;
node _ TiogaButtons.TextNodeRef[targetChar.node]; -- just a type converter
voiceThere _ TextEdit.GetCharProp[node, targetChar.where, $voice] # NIL;
IF voiceThere THEN {
newEntryID: EntryID _ MakeNewEntryID[script];
newEntryIDList: EntryIDList_ LIST[newEntryID];
entryList: EntryIDList _ NARROW[TextEdit.GetCharProp[node, targetChar.where, $script]];
entryList _ NARROW[GList.Append[entryList, newEntryIDList]];
TextEdit.PutCharProp[node, targetChar.where, $script, entryList];
TextEdit.PutCharProp[node, targetChar.where, $Postfix, scriptStyleParam];
TextEdit.PutCharProp[node, targetChar.where, $Artwork, NIL];
AddEntryViaSeqNum[
entryID: newEntryID, 
script: script, 
seqNum: seqNum,
charIndex: TextNode.LocNumber[at: [node, targetChar.where], skipCommentNodes: FALSE],
filename: selectedViewer.file,
action: action];
seqNum _ seqNum + 1;
soundsInSelection _ soundsInSelection + 1;
};

IF targetChar = endChar THEN EXIT;
targetChar _ TiogaOps.LocRelative[targetChar, 1];
ENDLOOP;
};

IF soundsInSelection = 0 THEN 
MessageWindow.Append["No sounds in selection", TRUE]
ELSE
ScriptToolMsg[IO.PutFR["%d entries added to script\n", IO.int[soundsInSelection]]];
};

TiogaOps.CallWithLocks[AddSelectedAnnotations ! TiogaOps.NoSelection => {suitableViewer _ FALSE; CONTINUE}];
IF NOT suitableViewer THEN {
MessageWindow.Append["Make a selection in a Tioga viewer first", TRUE];
MessageWindow.Blink[];
newSeqNum _ -1;  -- This may be a dumb thing to do
}
ELSE newSeqNum _ seqNum;
};

DeleteSelectedAnnotations: PUBLIC PROC [script: Script, seqNum: INT] RETURNS [newSeqNum: INT] ~ {
selectedViewer: ViewerClasses.Viewer;
suitableViewer: BOOLEAN;
voiceThere: BOOLEAN;

RemoveSelectedAnnotations: PROC [root: TiogaOps.Ref] = {
charsInSelection: INT _ 0;
soundsInSelection: INT _ 0;
startChar, endChar, targetChar: TiogaOps.Location;
node: TextNode.Ref;
caretBefore: BOOLEAN;
pendingDelete: BOOLEAN;
level: TiogaOps.SelectionGrain;

[viewer: selectedViewer, start: startChar, end: endChar, caretBefore: caretBefore, pendingDelete: pendingDelete, level: level] _ TiogaOps.GetSelection[];
suitableViewer _ SuitableViewer[selectedViewer];

IF suitableViewer AND pendingDelete THEN {
TiogaOps.SetSelection[viewer: selectedViewer, start: startChar, end: endChar,
level: level, caretBefore: caretBefore,
pendingDelete: FALSE, which: primary] -- simply makes not pending delete
};

IF suitableViewer AND NOT level = point THEN {
targetChar _ startChar;
DO
charsInSelection _ charsInSelection + 1;
node _ TiogaButtons.TextNodeRef[targetChar.node]; -- just a type converter
voiceThere _ TextEdit.GetCharProp[node, targetChar.where, $voice] # NIL;
IF voiceThere THEN {
oldEntryID: EntryID _ GetEntryIDFromSeqNum[script, seqNum];
entryList: EntryIDList _ NARROW[TextEdit.GetCharProp[node, targetChar.where, $script]];
IF GList.Member[oldEntryID, entryList] THEN
entryList _ NARROW[GList.Remove[oldEntryID, entryList]]
ELSE
EXIT; -- No more in sequence.  Note that seqNum stays constant throughout this operation
TextEdit.PutCharProp[node, targetChar.where, $script, entryList];
IF entryList = NIL THEN {
TextEdit.PutCharProp[node, targetChar.where, $Postfix, NIL];
TextEdit.PutCharProp[node, targetChar.where, $Artwork, NARROW["TalksBubble", Rope.ROPE]]; -- the NARROW prevents a REF TEXT being created
};
DeleteEntryViaSeqNum[script, seqNum];
soundsInSelection _ soundsInSelection + 1;
};

IF seqNum > script.numEntries THEN seqNum _ script.numEntries;
IF targetChar = endChar THEN EXIT;
targetChar _ TiogaOps.LocRelative[location: targetChar, count: 1,
break: 1, skipCommentNodes: FALSE];
ENDLOOP;
};

IF soundsInSelection = 0 THEN 
MessageWindow.Append["No sounds in selection", TRUE]
ELSE
ScriptToolMsg[IO.PutFR["%d entries removed from script\n", IO.int[soundsInSelection]]];
};

TiogaOps.CallWithLocks[RemoveSelectedAnnotations ! TiogaOps.NoSelection => {suitableViewer _ FALSE; CONTINUE}];
IF NOT suitableViewer THEN {
MessageWindow.Append["Make a selection in a Tioga viewer first", TRUE];
MessageWindow.Blink[];
newSeqNum _ -1;  -- This may be a dumb thing to do
}
ELSE newSeqNum _ seqNum;
};

AddEntryViaSeqNum: PROC [entryID: EntryID, script: Script, seqNum: INT, charIndex: INT, filename: Rope.ROPE _ NIL, action: Rope.ROPE _ NIL] ~ {
newEntry, curPtr: ScriptEntry;
IF seqNum < 0 THEN ERROR; -- 
newEntry _ NEW[ScriptEntryBody _ [entryID: entryID, next: NIL, prev: NIL, file: [filename, BasicTime.nullGMT], charIndex: charIndex, action: action] ];
IF seqNum = 0 THEN {
newEntry.next _ script.firstEntry;
newEntry.prev _ NIL;
script.firstEntry _ newEntry;
}
ELSE {
curPtr _ FindPositionOfSeqNum[script, seqNum];
newEntry.next _ curPtr.next;
curPtr.next _ newEntry;
newEntry.prev _ curPtr;
};
IF newEntry.next # NIL THEN
newEntry.next.prev _ newEntry
ELSE {
script.lastEntry _ newEntry;
};
script.numEntries _ script.numEntries + 1;
};

DeleteEntryViaSeqNum: PROC [script: Script, seqNum: INT] ~ {
curPtr: ScriptEntry;
IF seqNum <= 0 THEN ERROR; -- other checks could be done too
curPtr _ FindPositionOfSeqNum[script, seqNum];
IF curPtr.prev = NIL THEN {
script.firstEntry _ curPtr.next;
}
ELSE
curPtr.prev.next _ curPtr.next;
IF curPtr.next # NIL THEN
curPtr.next.prev _ curPtr.prev
ELSE {
script.lastEntry _ curPtr.prev;
};
script.numEntries _ script.numEntries - 1;
};

FindPositionOfSeqNum: PROC [script: Script, seqNum: INT] RETURNS [s: ScriptEntry] ~ {
IF seqNum < script.numEntries/2 THEN { -- go forward from start
s _ script.firstEntry;
FOR n: INT IN (1..seqNum] DO
s _ s.next;
ENDLOOP;
}
ELSE {  -- go backward from end
s _ script.lastEntry;
FOR n: INT DECREASING IN [seqNum..script.numEntries) DO
s _ s.prev;
ENDLOOP;
};
};

GetEntryIDFromSeqNum: PROC [script: Script, seqNum: INT] RETURNS [entryID: EntryID] ~ {
curPtr: ScriptEntry _ FindPositionOfSeqNum[script, seqNum];
RETURN[curPtr.entryID]
};

GetSeqNumFromEntryID: PROC [script: Script, entryID: EntryID] RETURNS [seqNum: INT] ~ {
seqNum _ 0;
FOR se: ScriptEntry _ script.firstEntry, se.next WHILE se # NIL DO
seqNum _ seqNum + 1;
IF EqualEntryID[se.entryID, entryID] THEN EXIT;
ENDLOOP;
};

GetScriptEntryFromEntryID: PROC [script: Script, entryID: EntryID] RETURNS [se: ScriptEntry] ~ {
FOR se _ script.firstEntry, se.next WHILE se # NIL DO
IF EqualEntryID[se.entryID, entryID] THEN EXIT;
ENDLOOP;
};

EqualEntryID: PROC [e1, e2: EntryID] RETURNS [BOOL] ~ {
RETURN[e1.sid = e2.sid AND e1.eid = e2.eid];
};

MakeNewEntryID: PROC [script: Script] RETURNS [entryID: EntryID] ~ {
entryID _ NEW[EntryIDBody _ [sid: script.sid, eid: script.numEntries + 1]];
};

CreateNewScript: PROC RETURNS [script: Script] ~ {
script _ NEW[ScriptBody _ [sid: BasicTime.ToPupTime[BasicTime.Now[]] ]];
scriptList _ CONS[script, scriptList];
};

GetCharIndexFromEntryID: PROC [viewer: ViewerClasses.Viewer, entryID: EntryID] RETURNS [charIndex: INT _ -1] ~ {
wholeFile: TiogaAccess.Reader _ TiogaAccess.FromViewer[viewer];
c: TiogaAccess.TiogaChar;
props: Atom.PropList;

WHILE charIndex < 0 DO
IF TiogaAccess.EndOf[wholeFile] THEN EXIT;
c _ TiogaAccess.Get[wholeFile];
FOR props _ c.propList, props.rest WHILE props # NIL DO
IF props.first.key = $script AND GList.Member[entryID, props.first.val] THEN
charIndex _ TiogaAccess.GetIndex[wholeFile];
ENDLOOP;
ENDLOOP;
charIndex _ charIndex -1;
};

CheckPropProc: TYPE ~ PROC [r: REF, rlist: REF] RETURNS [BOOL];

CProp: CheckPropProc ~ {
e: EntryID _ NARROW[r];
FOR elist: EntryIDList _ NARROW[rlist], elist.rest WHILE elist # NIL DO
IF EqualEntryID[elist.first, e] THEN RETURN [TRUE];
ENDLOOP;
RETURN [FALSE]
};

FindCharProp: PROC [viewer: ViewerClasses.Viewer, propName: ATOM, propVal: REF, checkPropProc: CheckPropProc] RETURNS [foundLoc: TiogaOps.Location] ~ {
rootNode: TextNode.Ref _ TiogaButtons.TextNodeRef[TiogaOps.ViewerDoc[viewer]]; -- TextNodeRef is just a type convertor - replace with a more cautious implementation??
foundLoc _ [NIL, -1];
FOR n: TextNode.Ref _ rootNode, TextNode.StepForward[n] UNTIL n = NIL DO
SearchCharProps: PROC RETURNS [found: BOOLEAN _ FALSE] ~ {
FOR i: INT IN [startChar..endChar) UNTIL found DO
propList: REF _ TextEdit.GetCharProp[n, i, propName];
IF checkPropProc[r: propVal, rlist: propList] THEN {foundLoc.where _ i;  found _ TRUE;}
ENDLOOP;
};
startChar: INT _ 0;
endChar: INT _ TextEdit.Size[n];
IF n.hascharprops AND SearchCharProps[].found THEN {
foundLoc.node _ TiogaButtons.TiogaOpsRef[n];
EXIT;
};
ENDLOOP;
};


GetSeqNumsFromSelection: PROC [script: Script, seqNum: INT] RETURNS [s: ScriptEntry] ~ {
seqNumList: LIST OF INT;
node: TextNode.Ref;
targetChar: TiogaOps.Location;
FOR entryList: EntryIDList _ NARROW[TextEdit.GetCharProp[node, targetChar.where, $script]], entryList.rest DO
seqNumList _ CONS[GetSeqNumFromEntryID[script, entryList.first], seqNumList];
ENDLOOP;
};

StoreScriptsRepAtRoot: TiogaOps.CommandProc ~ {
rootNode: TextNode.Ref _ TiogaButtons.TextNodeRef[TiogaOps.ViewerDoc[viewer]];
stream: IO.STREAM _ IO.ROS[];
FillInCharIndices[rootNode];
FOR s: ScriptList _ scriptList, s.rest WHILE s # NIL DO
WriteOneScript[s.first, stream];
ENDLOOP;
TextEdit.PutProp[rootNode, $scriptList, IO.RopeFromROS[stream]];
};

FillInCharIndices: PROC [root: TextNode.Ref] ~ {
propList: REF;
rootloc: TiogaOps.Location _ [TiogaButtons.TiogaOpsRef[root], 0];
loc: TiogaOps.Location _ rootloc;
DO
[loc, propList] _ TraverseCharProps[$script, loc];
IF loc.node=NIL THEN EXIT;
FOR elist: EntryIDList _ NARROW[propList], elist.rest WHILE elist # NIL DO
e: EntryID _ elist.first;
s: Script _ LookupScript[e.sid];
se: ScriptEntry _ GetScriptEntryFromEntryID[s, e];
se.charIndex _ TiogaOps.LocOffset[rootloc, loc];
ENDLOOP;
loc _ TiogaOps.LocRelative[loc,1];
ENDLOOP;
};

TraverseCharProps: PROC [propName: ATOM, start: TiogaOps.Location] RETURNS [foundLoc: TiogaOps.Location, propList: REF] ~ {
n: TextNode.Ref _ TiogaButtons.TextNodeRef[start.node];
char: INT _ start.where;
foundLoc _ [NIL, -1];
WHILE (foundLoc.node=NIL) AND (n#NIL) DO
endChar: INT _ TextEdit.Size[n];
IF n.hascharprops THEN {
FOR i: INT IN [char..endChar) WHILE foundLoc.node=NIL DO
propList _ TextEdit.GetCharProp[n, i, propName];
IF propList#NIL THEN 
{foundLoc.node _ TiogaButtons.TiogaOpsRef[n]; foundLoc.where _ i};
ENDLOOP;
};
char _ 0;
n _ TextNode.StepForward[n]
ENDLOOP;
};

WriteOneScript: PROC [s: Script, stream: IO.STREAM] ~ {
stream.PutF["[%g \"%g\" %g ", IO.card[s.sid], IO.rope[s.sDesc], IO.int[s.numEntries]];
FOR se: ScriptEntry _ s.firstEntry, se.next WHILE se # NIL DO
WriteOneEntry[se, stream];
ENDLOOP;
stream.Put[IO.rope["]"]];
};

WriteOneEntry: PROC [se: ScriptEntry, stream: IO.STREAM] ~ {
stream.PutF["(%g %g)", IO.int[se.entryID.eid], IO.int[se.charIndex]];
};

LookupScript: PROC [sid: ScriptID] RETURNS [script: Script _ NIL] ~ {
FOR s: ScriptList _ scriptList, s.rest WHILE s # NIL DO
IF s.first.sid=sid THEN RETURN [s.first]
ENDLOOP;
};


PlayEntry: PROC [script: Script, seqNum: INT] ~ {
viewer: ViewerClasses.Viewer;
start: TiogaOps.Location;
[viewer, start] _ FindEntry[script, seqNum];  -- probably should lock things here
TiogaVoicePrivate.PlaySelection[];
WaitForPlayToFinish[start];
ShowPosition[viewer, start, FALSE];
};

WaitForPlayToFinish: PROC [charLoc: TiogaOps.Location] ~ {
node: TextNode.Ref _ TiogaButtons.TextNodeRef[charLoc.node]; -- just a type converter
voiceID: Rope.ROPE _ NARROW[TextEdit.GetCharProp[node, charLoc.where, $voice]];
ropeLength: INT _ VoiceRope.Length[handle: TiogaVoicePrivate.thrushHandle, vr: NEW [VoiceRope.VoiceRopeInterval  _ [voiceID, 0, 0]]];
ropeLengthInMsec: INT _ ropeLength/Jukebox.bytesPerMS;
Process.Pause[Process.MsecToTicks[ropeLengthInMsec]];
};

FindEntry: PROC [script: Script, seqNum: INT] RETURNS [selectedViewer: ViewerClasses.Viewer, start: TiogaOps.Location] ~ {
scriptEntry: ScriptEntry _ FindPositionOfSeqNum[script, seqNum];
[viewer: selectedViewer] _ TiogaOps.GetSelection[];
start _ FindCharProp[selectedViewer, $script, scriptEntry.entryID, CProp];
IF start.node # NIL THEN {
ShowPosition[selectedViewer, start, TRUE];
};
};

ShowPosition: PROC [viewer: ViewerClasses.Viewer, charLoc: TiogaOps.Location, pendingDelete: BOOL]~ {
charIndex: INT _ TextNode.LocNumber[at: [TiogaButtons.TextNodeRef[charLoc.node], charLoc.where], skipCommentNodes: FALSE];
TEditSelectionOps.ShowGivenPositionRange[viewer: viewer, selectionId: primary, posI: charIndex, posF: charIndex, skipCommentNodes: FALSE, pendingDelete: pendingDelete];
};


st: ScriptTool;

InitScriptTool: PROC ~ {
st _ NEW[ScriptToolBody _ [
scriptID: 0,
seqNum: 0,
addEntries: AddEntriesProc,
deleteEntries: DeleteEntriesProc,
findEntry: FindEntryProc,
nextEntry: NextEntryProc,
prevEntry: PrevEntryProc,
playEntry: PlayEntryProc,
playScript: PlayScriptProc,
stop: StopProc,
newScript: CreateScriptProc,
listScripts: ListScriptsProc,
action: "",
msg: ""
] ];
[] _ ViewRec.ViewRef[agg: st, viewerInit: [name: "Script Tool"]];
};

AddEntriesProc: PROC ~ {
ScriptToolMsg[""];
st.seqNum _ InsertSelectedAnnotationsAfter[LookupScript[st.scriptID], st.seqNum, st.action]
};

DeleteEntriesProc: PROC ~ {
ScriptToolMsg[""];
st.seqNum _ DeleteSelectedAnnotations[LookupScript[st.scriptID], st.seqNum]
};

FindEntryProc: PROC ~ {
ScriptToolMsg[""];
[] _ FindEntry[LookupScript[st.scriptID], st.seqNum];
};

NextEntryProc: PROC ~ {
ScriptToolMsg[""];
IF st.seqNum < LookupScript[st.scriptID].numEntries THEN {
st.seqNum _ st.seqNum + 1;
[] _ FindEntry[LookupScript[st.scriptID], st.seqNum];
}
ELSE
ScriptToolMsg["End of script"];
};

PrevEntryProc: PROC ~ {
ScriptToolMsg[""];
IF st.seqNum > 1 THEN {
st.seqNum _ st.seqNum - 1;
[] _ FindEntry[LookupScript[st.scriptID], st.seqNum];
}
ELSE
ScriptToolMsg["Beginning of script"];
};

PlayEntryProc: PROC ~ {
ScriptToolMsg[""];
[] _ PlayEntry[LookupScript[st.scriptID], st.seqNum];
};

PlayScriptProc: PROC ~ {
script: Script _ LookupScript[st.scriptID];
ScriptToolMsg[""];
FOR n: INT _ 1, n+1 WHILE n <= script.numEntries DO
st.seqNum _ n;  -- user feedback
PlayEntry[script, n];
ENDLOOP;
};

StopProc: PROC ~ {
ScriptToolMsg[""];
VoiceRope.Stop[TiogaVoicePrivate.thrushHandle];
};

CreateScriptProc: PROC ~ {
st.scriptID _ CreateNewScript[].sid;
st.seqNum _ 0;
ScriptToolMsg["new script created"];
};

ListScriptsProc: PROC ~ {
st.msg _ "";
FOR s: ScriptList _ scriptList, s.rest WHILE s # NIL DO
st.msg _ Rope.Cat[st.msg, IO.PutFR["%d ", IO.card[s.first.sid]]];
ENDLOOP;
};

ScriptToolMsg: PROC [msg: Rope.ROPE] ~ {
st.msg _ msg;
};


InitScriptTool[];
TiogaOps.RegisterCommand[name: $RedSave, proc: StoreScriptsRepAtRoot];
TiogaOps.RegisterCommand[name: $YellowSave, proc: StoreScriptsRepAtRoot];
TiogaOps.RegisterCommand[name: $BlueSave, proc: StoreScriptsRepAtRoot];
TiogaOps.RegisterCommand[name: $RedStore, proc: StoreScriptsRepAtRoot];
TiogaOps.RegisterCommand[name: $YellowStore, proc: StoreScriptsRepAtRoot];
TiogaOps.RegisterCommand[name: $BlueStore, proc: StoreScriptsRepAtRoot];
}.

IF seqNum < curNum THEN {
IF seqNum < curNum/2 THEN {
curPtr _ script.firstEntry;
FOR curNum: INT IN [2..seqNum] DO
curPtr _ curPtr.next;
ENDLOOP;
}
ELSE {
FOR curNum: INT _ curNum-1, curNum-1 WHILE curNum<seqNum DO
curPtr _ curPtr.prev;
ENDLOOP;
}
}
ELSE {
IF seqNum < (numEntries-curNum)/2 THEN {
FOR curNum: INT IN [seqNum+1..numEntries] DO
curPtr _ curPtr.next;
ENDLOOP;
}
ELSE {
curPtr _ script.lastEntry;
FOR curNum: INT _ numEntries, curNum-1 WHILE curNum>seqNum DO
curPtr _ curPtr.prev;
ENDLOOP;
}
};

��%‚��NarratedDocsImpl.mesa
Copyright Ó 1986, 1987 by Xerox Corporation.  All rights reserved.
Polle Zellweger (PTZ) June 24, 1987 12:19:49 pm PDT
EXPORTS NarratedDocs
Types
do we record a filename and/or viewer here, or do we require the user to click in the scripted viewer to give us a pointer?
may be good to have entry descs too, so user can figure out what is going on.  But would these be voice rope descs & uniquefied for multiple copies in a single script, or would they be an entry desc?  Note that the same voice rope can be attached to different places in the same document and appear in a script at each location, which is of course different than the same annotation appearing multiple times in a script.  So an entry is described by (script, seqNum, docLoc, voiceRope).  voiceRope can be determined from docLoc, but is not VISIBLE from it.  Navigating in a Complex Invisible Space.
these are an internal representation for the script tool
trust this only if document is not edited; a hint to help disambiguate multiple copies otherwise
these are stored (in a list, as $script property) on characters in the document
Declarations
Script Creation and Editing

GList operations assume type = LIST OF REF X for some X.

Is seqNum> script.numEntries an ERROR or a way to specify the end of the list?
Copied from VoiceInTextImpl
Add all voice annotations in selection to script in order following seqNum.
Can't use TiogaAccess because need to change char props.
Add annotation to script: char props
Give user visual feedback that this voice entry is in some script.  WHICH script is a bit more complicated than this.
There are also some real difficulties with crossing document boundaries -- when to resolve the references?  Could be editing 2 anonymous viewers and building up a script across both of them.  So even at the SAVE of one, you don't know what the filename of the other is.  And if you did, the user could change it thereafter.  Damn!  Obviously need something like Interest relations to manage this.

Update seqNum field in tool if middle moused?
test for failure conditions and report them to the user after releasing the viewer lock
Delete the sequence of voice annotations in selection from script starting at seqNum
Remove annotation from script: char props
Give user visual feedback that this voice entry is no longer in any script.  WHICH script is a bit more complicated than this.
removed last entry or seqNum out of range!
Update seqNum field in tool if middle moused?
test for failure conditions and report them to the user after releasing the viewer lock
Add entry to script AFTER seqNum.
IF seqNum # script.numEntries THEN ERROR; -- 
Delete entry seqNum from script.
GList.Member just checks ref equality, not the internal representation.  This is only ok until scripts are stored and recreated.
Searches document forward from the beginning for occurrence of character property propName with value propVal.
.....
TYPES ARE ALL WRONG HERE!!!
.....
if seqNumList has one elt, fill in seqNum field in tool, else clear seqNum field and display seqNumList in msg area
Script Output

PROC [viewer: Viewer _ NIL] RETURNS [recordAtom: BOOL _ TRUE, quit: BOOL _ FALSE]
Document has <$script, entryUIDList> properties scattered throughout it.  Script Tool has linked list of <entryUID, other fields>
IF document has been edited THEN CheckScriptsForDuplicates[];
Searches document forward from location start for occurrence of character property propName.
Script Playback
Should do this in a separate process, so that can accept other user actions (esp stop) during playback.
Should really use progress reports from VoiceRope rather than independent timing.
may be too long? ignores start & length values specified in voice rope
Could of course click PlayEntry, then click Play immediately => the timing would be off.  For now, don't do that.
this may need to be adjusted by 1??
Script Tool
Call this only once per user function, otherwise user won't be able to read it before it disappears.
Body of NarratedDocsImpl


Polle Zellweger (PTZ) August 21, 1986 5:34:34 pm PDT
changes to: DIRECTORY, NarratedDocsImpl
Polle Zellweger (PTZ) August 21, 1986 11:27:32 pm PDT
changes to: AddSelectedAnnotationsToScript, AddEntryToScriptRep, scriptStyleParam, DeleteEntryViaSeqNum, FindPositionOfSeqNum, SuitableViewer (local of InsertSelectedAnnotationsAfter), AddSelectedAnnotations (local of InsertSelectedAnnotationsAfter), DeleteSelectedAnnotations, RemoveSelectedAnnotations (local of DeleteSelectedAnnotations), GetSeqNumsFromSelection, GetEntryIDFromSeqNum

-- curPtr _ FindPositionOfSeqNum[script, seqNum];
go forward from start
go backward from curPtr
go forward from curPtr
go backward from end
Polle Zellweger (PTZ) August 22, 1986 7:17:07 pm PDT
changes to: AddEntryViaSeqNum, DeleteEntryViaSeqNum, FindPositionOfSeqNum, ELSE, DIRECTORY, RemoveSelectedAnnotations (local of DeleteSelectedAnnotations), GetEntryIDFromSeqNum, GetCharFromEntryID, AddSelectedAnnotations (local of InsertSelectedAnnotationsAfter)
Polle Zellweger (PTZ) August 23, 1986 6:20:37 pm PDT
changes to: DIRECTORY, NarratedDocsImpl, NconcIntervals, scriptStyleParam, SuitableViewer, InsertSelectedAnnotationsAfter, AddSelectedAnnotations (local of InsertSelectedAnnotationsAfter), DeleteSelectedAnnotations, RemoveSelectedAnnotations (local of DeleteSelectedAnnotations), AddEntryViaSeqNum, DeleteEntryViaSeqNum, FindPositionOfSeqNum, GetEntryIDFromSeqNum, MakeNewEntryID, CreateNewScript, GetCharIndexFromEntryID, GetSeqNumsFromSelection, IF, ELSE, StoreScriptRepAtRoot, ScriptTool, ScriptToolBody, }, st, InitScriptTool, AddEntries, LookupScript, DeleteEntries, FindEntry, PlayEntryProc, Play, Play
Polle Zellweger (PTZ) August 24, 1986 11:05:38 pm PDT
changes to: InitScriptTool, AddEntriesProc, DeleteEntriesProc, FindEntryProc, PlayEntry, PlayEntryProc, PlayProc, StopProc, NewScriptProc, ListScriptsProc, ListScriptsProc, MakeNewEntryID, GetCharIndexFromEntryID, StoreScriptsRepAtRoot, DIRECTORY, NarratedDocsImpl, ScriptList, Script, ScriptBody, ScriptID, ScriptEntry, ScriptEntryBody, EntryID, EntryIDBody, ScriptTool, ScriptToolBody, scriptStyleParam, SuitableViewer, InsertSelectedAnnotationsAfter, AddSelectedAnnotations (local of InsertSelectedAnnotationsAfter), DeleteSelectedAnnotations, RemoveSelectedAnnotations (local of DeleteSelectedAnnotations), AddEntryViaSeqNum, DeleteEntryViaSeqNum, FindPositionOfSeqNum, GetEntryIDFromSeqNum, CreateNewScript, GetSeqNumsFromSelection, LookupScript, InitScriptTool, EntryIDList, scriptList, FindEntry, GetSeqNumFromEntryID, LookupScript
Polle Zellweger (PTZ) August 25, 1986 11:56:46 pm PDT
changes to: ScriptEntryBody, ScriptBody, GetSeqNumFromEntryID, EqualEntryID, MakeNewEntryID, CheckPropProc, CProp, FindCharProp, SearchCharProps (local of FindCharProp), PlayEntry, FindEntry, DIRECTORY, NarratedDocsImpl, FindEntryProc, PlayProc
Polle Zellweger (PTZ) August 26, 1986 0:25:01 am PDT
changes to: ListScriptsProc, AddSelectedAnnotations (local of InsertSelectedAnnotationsAfter), RemoveSelectedAnnotations (local of DeleteSelectedAnnotations)
Polle Zellweger (PTZ) August 26, 1986 6:10:42 pm PDT
changes to: AddEntryViaSeqNum, DeleteEntryViaSeqNum
Polle Zellweger (PTZ) August 27, 1986 12:21:02 pm PDT
changes to:  ScriptToolBody, RemoveSelectedAnnotations (local of DeleteSelectedAnnotations), InitScriptTool, PlayEntry, WaitForPlayToFinish, FindEntry, ShowPosition, AddEntriesProc, DeleteEntriesProc, FindEntryProc, NextEntryProc, PrevEntryProc, PlayEntryProc, PlayProc, StopProc, NewScriptProc

Polle Zellweger (PTZ) October 17, 1986 2:51:42 pm PDT
Start of changes that never compiled successfully - so .bcd and .mesa diverged.  Idea was to add actions and store scripts in doc root.
changes to: ScriptBody, ScriptName, ScriptEntryBody, EntryIDBody, EntryIDList, FileID, ScriptToolBody, MakeNewEntryID, CreateNewScript, LookupScript, InitScriptTool, AddEntriesProc, DeleteEntriesProc, FindEntryProc, NextEntryProc, PrevEntryProc, PlayEntryProc, PlayScriptProc, CreateScriptProc, DestroyScriptProc, ListScriptsProc
Polle Zellweger (PTZ) October 24, 1986 3:05:28 pm PDT
changes to: ScriptEntry, ScriptEntryBody, ScriptExt, EntryID, FindCharProp, FindCharProp, StoreScriptsRepAtRoot
Polle Zellweger (PTZ) November 19, 1986 6:06:19 pm PST
changes to: DIRECTORY, ScriptBody, ScriptEntryBody, GetSeqNumFromEntryID, GetScriptEntryFromEntryID, CProp, StoreScriptsRepAtRoot, TraverseCharProps, GetProp, WriteOneScript, WriteOneEntry, LookupScript
Polle Zellweger (PTZ) January 6, 1987 7:20:47 pm PST
changes to: ScriptBody, ScriptID, ScriptName, ScriptDesc, ScriptCreator

Polle Zellweger (PTZ) March 23, 1987 6:17:28 pm PST
Going backward to create a file that would compile to form a working simple prototype.
changes to: ScriptBody, ScriptID, EntryIDBody, ScriptToolBody, MakeNewEntryID, CreateNewScript, WriteOneScript, LookupScript, InitScriptTool, AddEntriesProc, DeleteEntriesProc, FindEntryProc, NextEntryProc, PrevEntryProc, PlayEntryProc, PlayScriptProc, CreateScriptProc, DestroyScriptProc, ListScriptsProc, DIRECTORY, ScriptEntryBody, StoreScriptsRepAtRoot, TraverseCharProps, GetProp, NarratedDocsImpl, WriteOneEntry, FillInCharIndices
Polle Zellweger (PTZ) March 24, 1987 12:23:47 pm PST
changes to: DIRECTORY, FillInCharIndices, TraverseCharProps, WriteOneScript, LookupScript, ScriptID, InitScriptTool, ScriptBody
Polle Zellweger (PTZ) June 24, 1987 12:10:53 pm PDT
Update for new TiogaVoice, VoiceInText => TiogaVoicePrivate
changes to: DIRECTORY, NarratedDocsImpl, PlayEntry, WaitForPlayToFinish, StopProc
Êc��•	voicelist(&PolleZ.pa#588500764&PolleZ.pa#588500831˜�Jšœ™šœB™BIcodešœ3™3—J˜�šÏk	˜	Jšœœ˜Jšœ
œ"˜1Jšœœ˜'Jšœ˜Jšœœ˜Jšœœ˜&Jšœœ˜%Jšœ˜Jšœœ˜3Jšœ	œ-˜;Jšœ	œ!˜/Jšœœ9˜JJšœ
œ˜0Jšœ	œœ˜ªJšœœ˜Jšœ
œ˜Jšœœ
˜Jšœœ!˜8Jšœ
œ$˜3J˜J˜�—šÏnœœ˜Jšœœœ˜¸Jšœ
™Jšœ˜J˜�—•	CharPropsdvoicePolleZ.pa#588500764ArtworkTalksBubblePostfix)0 outlineBoxBearoff 1 outlineBoxThickness�™J˜�Kšœœœœ˜"K˜�Kšœœœ˜šœœœ˜K™{Kšœ˜šœ˜Kšœ®Ïb(™Ö—Kšœ˜Kšœœ˜Kšœ%œ˜)Kšœ˜K˜K˜�—Kšœ
œœ˜Kšœœœ˜Kšœœœ˜ K˜�šœ
œœ˜(K™8—šœœœ˜ Kšœœ˜Jšœ
˜
Kšœ˜Kšœ
œÏc$˜;Kšœœ ˜Kšœ
œ˜šœœ˜Kšœ`™`—Kšœœ ,˜>K˜K˜�—šœ	œœ
˜ KšœO™O—šœ
œœ˜Kšœ˜Kšœœ˜K˜—K˜�Kšœ
œœœ	˜$K˜�šœœœ˜Kšœœ˜Kšœœ˜-K˜—K˜�Jšœœœ˜&šœœœ˜Jšœ˜Jšœœ˜Jšœ
œ˜Jšœœ˜Jšœœ˜Jšœœ˜Jšœœ˜Jšœœ˜Jšœœ˜Jšœœ˜Jšœœ˜Jšœœ˜Jšœ
œ˜Jšœ
˜J˜J˜�——™J˜�J˜J˜�—™J™�Kšœ8™8K™�KšÏtœN¡™PK˜�Kšœœ/˜JK˜�šžœœ'œœ˜RJš¡œ¡™Jšœ&œ9œ˜o—J˜�šžœœœœœœœ
œ˜K™KK™8Kšœ%˜%Jšœœ˜Jšœœ˜J˜�šžœœ˜5Jšœœ˜Jšœœ˜Jšœ2˜2Jšœ˜Jšœ
œ˜Jšœœ˜Jšœ˜J˜�Jšœ™˜™Jšœ0˜0J˜�šœœœ˜*Jšœ…œ "˜¾J˜—J˜�šœœœœ˜.Kšœ˜š˜Kšœ(˜(Jšœ2 ˜JJšœDœ˜Hšœœ˜Jšœ-˜-Jšœœ
˜.J™$Jšœœ8˜WJšœœ*˜<JšœA˜AJšœI˜Išœ7œ˜<J™uJ™ŒJ™�—šœ˜Jšœ˜Jšœ˜Jšœ˜JšœNœ˜UJšœ˜Jšœ˜—J˜Jšœ*˜*J˜—K˜�Kšœœœ˜"Jšœ1˜1Kšœ˜—J˜J˜�—šœœ˜Jšœ/œ˜4—š˜Jšœœ'œ˜S—Jš¡œ-¡™/J˜J˜�—JšœZœœ˜lJ™Wšœœœ˜JšœAœ˜GJšœ˜Jšœ Ðct ¢˜4J˜—Jšœ˜J˜—J˜�šžœœœœœ
œ˜aK™TKšœ%˜%Jšœœ˜Jšœœ˜J˜�šžœœ˜8Jšœœ˜Jšœœ˜Jšœ2˜2Jšœ˜Jšœ
œ˜Jšœœ˜Jšœ˜J˜�Jšœ™˜™Jšœ0˜0J˜�šœœœ˜*Jšœ…œ "˜¾J˜—J˜�šœœœœ˜.Kšœ˜š˜Kšœ(˜(Jšœ2 ˜JJšœDœ˜Hšœœ˜Jšœ;˜;J™)Jšœœ8˜Wšœ%˜+Jšœœ%˜7—š˜Kšœ ¢ ¢=˜Z—JšœA˜Ašœ
œœ˜Jšœ7œ˜<Jšœ7œœ /˜‰J™~J˜—Jšœ%˜%Jšœ*˜*J˜—K˜�šœœ˜>Kšœ¡œ¡™,—Kšœœœ˜"Jšœ^œ˜eKšœ˜—J˜J˜�—šœœ˜Jšœ/œ˜4—š˜Jšœœ+œ˜W—Jš¡œ-¡™/J˜J˜�—Jšœ]œœ˜oJ™Wšœœœ˜JšœAœ˜GJšœ˜Jšœ ¢ ¢˜4J˜—Jšœ˜J˜J˜�—šžœœ,œ
œœœœœ˜K™!Kšœ˜Kšœœœ ¢˜Kšœœ,œœO˜—šœœ˜Kšœ"˜"Kšœœ˜Kšœ˜K˜—šœ˜Kšœ.˜.K˜K˜K˜K˜—šœœ˜Kšœ˜—šœ˜Kšœœœ ¢™/Kšœ˜K˜—K˜*K˜K˜�—šžœœœ˜<K™ Kšœ˜Kšœ
œœ ¢ ˜>Kšœ.˜.šœœœ˜Kšœ ˜ K˜—š˜K˜—šœœ˜Kšœ˜—šœ˜Kšœ˜K˜—K˜*K˜—K˜�šžœœœœ˜Ušœœ ˜?Kšœ˜šœœœ
˜Kšœ˜Kšœ˜—K˜—šœ ˜Kšœ˜š	œœ
œœ˜7Kšœ˜Kšœ˜—K˜—K˜—K˜�šžœœœœ˜WKšœ;˜;Kšœ˜K˜—K˜�šžœœ$œ
œ˜WKšœ˜šœ.œœ˜BKšœ˜Kšœ#œœ˜/Kšœ˜—Kšœ˜K˜�—šžœœ$œ˜`šœ!œœ˜5Kšœ#œœ˜/Kšœ˜—Kšœ˜K˜�—šžœœœœ˜7Kšœœ˜,K˜K˜�—šžœœœ˜DKšœ
œ>˜KK˜—K˜�šžœœœ˜2Kšœ	œ<˜HKšœ
œ˜&K˜K˜�—šžœœ2œ
œ
˜pJšœ?˜?Jšœ˜Jšœ˜J˜�šœ˜Jšœœœ˜*Jšœ˜šœ œ	œ˜7šœœ(˜LJš¡œ€¡™‚Jšœ,˜,—Jšœ˜—Jšœ˜—Kšœ˜K˜—K˜�Kš
œœœœ	œœœ˜?K˜�šžœ˜Jšœ
œ˜š	œœœ	œ˜GJšœœœœ˜3Jšœ˜—Jšœœ˜J˜K˜�—š
žœœ*œœ œ"˜—Kšœn™nJšœO W˜¦Kšœœ˜šœ5œœ˜Hš
žœœœ	œœ˜:š	œœœœ˜1Kšœ
œ(˜5Kšœ,œœ˜WKšœ˜—Kšœ˜—Kšœœ˜Kšœ	œ˜ šœœœ˜4Kšœ,˜,Kšœ˜Kšœ˜—Kšœ˜—K˜K˜�K˜�—šžœœœœ˜XKš¡œ¡™Kšœœœœ˜K˜Kšœ˜šœœH˜mšœ
œ<˜MK™—Kšœ˜—Kš¡œ¡™Kšœs™sK˜—K˜�—™
K™�šžœ˜/Kšœœœœœœœ™QK™Kš¡œœ¡™?JšœN˜NKš	œœœœœ˜Kšœ˜šœ$œœ˜7Kšœ ˜ Kšœ˜—Kšœ(œ˜@K˜—K˜�šžœœ˜0Kšœ
œ˜KšœA˜AKšœ!˜!š˜Kšœ2˜2Kšœ
œœœ˜š	œœœ	œ˜JKšœ˜Kšœ ˜ Kšœ2˜2Jšœ0˜0Jšœ˜—K˜"Kšœ˜—K˜K˜�—š
žœœœœ)œ˜{Kšœ(Ÿœ&Ÿœ™\Kšœ7˜7Kšœœ˜Kšœœ˜š	œœœœ˜(Kšœ	œ˜ šœœ˜šœœœœœ˜8Kšœ0˜0šœ
œœ˜KšœB˜B—Kšœ˜—Kšœ˜—K˜	Kšœ˜Kšœ˜—K˜K˜�—šžœœœœ˜7Kšœœœœ˜Všœ)œ˜=Kšœ˜Kšœ˜—Kšœœ˜K˜K˜�—šž
œœœœ˜<Kšœœœ˜EK˜K˜�—šžœœœœ˜Ešœ$œœ˜7Kšœœœ
˜(Kšœ˜—K˜K˜�——™K˜�šž	œœœ˜1Kš¡œg¡™iKšœ˜Kšœ˜Kšœ. ¢  ¢˜SKšœ"˜"Kšœ˜Kšœœ˜#K˜—K˜�šžœœ!˜:Kš¡œQ¡™SJšœ= ˜UJšœœœ4˜Ošœœ@œ3˜…Kš¡œF¡™H—Kšœœ!˜6šœ5˜5K™q—K˜K˜�—šž	œœœœE˜zKšœ@˜@Jšœ3˜3KšœJ˜Jšœœœ˜Kšœ$œ˜*K˜—K˜K˜�—šžœœKœ˜ešœœeœ˜zK™#—Kšœƒœ ˜¨K˜—J˜�—–dvoicePolleZ.pa#588500831ArtworkTalksBubblePostfix)0 outlineBoxBearoff 1 outlineBoxThickness
�™J˜�Jšœ˜J˜�šžœœ˜šœœ˜Jšœ˜Jšœ
˜
Jšœ˜Jšœ!˜!Jšœ˜Jšœ˜Jšœ˜Jšœ˜Jšœ˜Jšœ˜Jšœ˜Jšœ˜J˜Jšœ˜J˜—JšœA˜A˜J˜�——šžœœ˜K˜Kšœ[˜[K˜—K˜�šžœœ˜K˜KšœK˜KK˜—K˜�šž
œœ˜K˜Kšœ5˜5K˜—K˜�šž
œœ˜K˜šœ1œ˜:Kšœ˜Kšœ5˜5K˜—š˜K˜—K˜—K˜�šž
œœ˜K˜šœœ˜Kšœ˜Kšœ5˜5K˜—š˜K˜%—K˜K˜�—šž
œœ˜K˜Kšœ5˜5K˜—K˜�šžœœ˜Kšœ+˜+K˜šœœ
œ˜3Kšœ ˜ Kšœ˜Kšœ˜—K˜—K˜�šžœœ˜K˜Kšœ/˜/K˜K˜�—šžœœ˜Kšœ$˜$K˜K˜$K˜K˜�—šžœœ˜K˜šœ$œœ˜7Kšœœœ˜AKšœ˜—K˜K˜�—šž
œœœ˜(K™dKšœ
˜
K˜—J˜�—™K˜�Kšœ˜K™�KšœF˜FKšœI˜IKšœG˜GKšœG˜GKšœJ˜JKšœH˜HK™�—J˜J˜�™4KšœÏr™'—™5Kšœ£œ*£œ*£6œ%£/™ƒ—K™�šœ1™1šœœ˜šœœ˜K™K˜šœ	œœ
˜!K˜Kšœ˜—K˜—šœ˜K™šœ	œœ˜;K˜Kšœ˜—K˜—K˜—šœ˜šœ œ˜(K™šœ	œœ˜,K˜Kšœ˜—K˜—šœ˜K™K˜šœ	œœ˜=K˜Kšœ˜—K˜—K˜K˜�——™4Kšœ£iœ%£Bœ*™†—™4Kšœ£…œ*£6œ%£Ê™à—™5Kšœ£Ðœ*£6œ%£å™Æ—™5Kšœ£„œ£L™ô—™4Kšœ£'œ*£œ%™—™4Kšœ£'™3—™5Kšœ
£)œ%£Ë™¦K™�—™5K™‡Kšœ£½™É—™5Kšœ£c™o—™6Kšœ£¾™Ê—™4Kšœ£;™GK™�—™3K™VKšœ£¨™´—™4Kšœ£s™—™3K™;Kšœ£E™Q——�…—����JD��‰)��