VoiceInTextImpl.mesa
Copyright Ó 1987 by Xerox Corporation. All rights reserved.
Ades, September 24, 1986 5:52:47 pm PDT
Swinehart, April 9, 1987 12:24:18 pm PDT
Polle Zellweger (PTZ) July 21, 1987 12:14:36 pm PDT
DIRECTORY
Atom USING [PropList],
BasicTime USING [GMT],
Commander USING [CommandProc, Register],
CommandTool USING [ArgumentVector, Failed, Parse ],
Convert USING [IntFromRope, RopeFromInt, RopeFromTime ],
FileReader USING [FromStream],
FS USING [defaultStreamOptions, Error, FileInfo, StreamOpen, StreamOptions ],
FSBackdoor USING [RemoteEvent, NextRemoteEvent ],
Imager USING [Color, Context, MaskStroke, Move, SetColor, SetStrokeJoint, SetStrokeWidth, ShowChar, ShowRope ],
ImagerColor USING [ConstantColor, ColorFromStipple],
ImagerFont USING [Extents, FontBoundingBox, Escapement ],
ImagerPath USING [PathProc],
IO USING [int, PutF, PutFR, rope, STREAM ],
MBQueue USING [Create, CreateMenuEntry, Queue ],
Menus USING [AppendMenuEntry, CreateEntry, FindEntry, GetLine, GetNumberOfLines, InsertMenuEntry, Menu, MenuEntry, MenuLine, MenuProc, ReplaceMenuEntry, SetLine ],
MessageWindow USING [Append, Blink, Confirm],
NodeStyle USING [GetReal, Ref ],
NodeStyleOps USING [OfStyle],
Process USING [Detach, priorityBackground, SetPriority ],
Rope USING [Cat, Concat, Equal, Fetch, Find, Length, MaxLen, ROPE, Substr ],
TEditFormat USING [CharacterArtwork, CharacterArtworkClass, CharacterArtworkClassRep, CharacterArtworkRep, GetFont, RegisterCharacterArtwork ],
TextEdit USING [CharSet, FetchChar, GetCharProp, PutCharProp, PutProp, Size ],
TextNode USING [Location, Ref, Root, StepForward],
TiogaAccess USING [EndOf, FromFile, FromSelection, Get, Reader, TiogaChar ],
TiogaButtons USING [TextNodeRef, TiogaOpsRef],
TiogaMenuOps USING [tiogaMenu],
TiogaOps USING [CallWithLocks, CommandProc, GetSelection, NoSelection, Ref, RegisterCommand, SelectionGrain, SetSelection, StepForward, ViewerDoc ],
TiogaOpsDefs USING [Location, Ref],
TiogaVoicePrivate USING [ AddVoiceProc, BuildVoiceViewer, CancelProc, DictationMachine, FindAttachedVoiceViewers, GetVoiceViewerInfo, MakeVoiceEdited, PlayBackMenuProc, PlayRopeWithoutCue, RemoveParentPointersTo, RemoveParentViewer, RopeFromTextList, SetParentViewer, VoiceViewerInfo, VoiceViewerInfoList, VoiceWindowRec, VoiceWindowRef ],
Vector2 USING [VEC],
ViewerBLT USING [ChangeNumberOfLines],
ViewerClasses USING [Viewer],
ViewerEvents USING [EventProc, EventRegistration, RegisterEventProc, UnRegisterEventProc ],
ViewerForkers USING [ CallBack, ForkCall],
ViewerOps USING [FetchProp, PaintViewer],
VoiceRope USING [Handle, Length, Open, Retain, VoiceRope, VoiceRopeInterval ]
;
VoiceInTextImpl:
CEDAR
PROGRAM
IMPORTS Commander, CommandTool, Convert, FileReader, FS, FSBackdoor, Imager, ImagerColor, ImagerFont, IO, MBQueue, Menus, MessageWindow, NodeStyle, Process, Rope, TEditFormat, TextEdit, TextNode, TiogaAccess, TiogaButtons, TiogaMenuOps, TiogaOps, TiogaVoicePrivate, ViewerBLT, ViewerEvents, ViewerForkers, ViewerOps, VoiceRope EXPORTS TiogaVoicePrivate SHARES ViewerClasses = {
TiogaVoicePrivate
VoiceMenu: Menus.MenuProc = { ChangeMenu[
NARROW[parent], voiceMenu] };
basic code to add voice to textual tioga documents and to replay that voice
this just toggles the voice submenu on and off the screen
ChangeMenu:
PUBLIC
PROC [
viewer: ViewerClasses.Viewer, subMenu: Menus.MenuEntry] = {
see comments in interface about this
menu: Menus.Menu ← viewer.menu;
found: BOOL ← FALSE;
numLines: Menus.MenuLine = Menus.GetNumberOfLines[menu];
newLines: Menus.MenuLine ← numLines;
FOR i: Menus.MenuLine
IN [1..numLines)
DO
see if already showing the submenu
IF Rope.Equal[Menus.GetLine[menu,i].name, subMenu.name]
THEN {
-- yes, so remove it
FOR j: Menus.MenuLine
IN (i..numLines)
DO
Menus.SetLine[menu, j-1, Menus.GetLine[menu, j]];
ENDLOOP;
newLines ← newLines-1;
found ← TRUE; EXIT
};
ENDLOOP;
IF ~found
THEN {
add it. do insertion sort to get it in the right place
GoesBefore:
PROC [m1, m2: Menus.MenuEntry]
RETURNS [
BOOL] = {
Priority:
PROC [m: Menus.MenuEntry]
RETURNS [
INTEGER] = {
higher priority means goes above in series of submenus
looks at rope for first item to identify the subMenu.
RETURN [
SELECT
TRUE
FROM
Rope.Equal[m.name, "Find"] => 1,
Rope.Equal[m.name, "FirstLevelOnly"] => 0,
ENDCASE => -1 -- unknown menu goes at bottom -- ]
};
RETURN [Priority[m1] > Priority[m2]]
};
newLast: Menus.MenuLine = MIN[numLines, LAST[Menus.MenuLine]];
newLines ← newLines+1;
FOR i: Menus.MenuLine
IN [1..numLines)
DO
IF GoesBefore[subMenu, Menus.GetLine[menu, i]]
THEN {
put it here
FOR j: Menus.MenuLine
DECREASING
IN (i..newLast]
DO
Menus.SetLine[menu, j, Menus.GetLine[menu, j-1]]; ENDLOOP;
Menus.SetLine[menu, i, subMenu];
found ← TRUE; EXIT
};
ENDLOOP;
IF ~found THEN Menus.SetLine[menu, newLast, subMenu];
};
ViewerBLT.ChangeNumberOfLines[viewer, newLines];
};
ApplyToCharsInPrimarySelection:
PUBLIC
PROC [
ActionProc: PROC [TiogaOpsDefs.Location]] = {
a convenience: looks after all the tree walking and calls the given ActionProc once for each character in the primary selection
ScanLocked:
PROC [root: TiogaOps.Ref] = {
current, end: TiogaOpsDefs.Location;
[start: current, end: end] ← TiogaOps.GetSelection[];
IF current.node = end.node
THEN FOR i: INT IN [current.where..end.where] DO ActionProc[[current.node, i]] ENDLOOP
ELSE
{
FOR i:
INT
IN [current.where..TextEdit.Size[TiogaButtons.TextNodeRef[current.node]])
DO ActionProc[[current.node, i]]
ENDLOOP;
DO
current.node ← TiogaOps.StepForward[current.node];
IF current.node = end.node THEN
{
FOR i:
INT
IN [0..end.where)
DO ActionProc[[current.node, i]]
ENDLOOP;
RETURN
}
ELSE FOR i: INT IN [0..TextEdit.Size[TiogaButtons.TextNodeRef[current.node]]) DO ActionProc[[current.node, i]] ENDLOOP
ENDLOOP
}
};
TiogaOps.CallWithLocks[ScanLocked]
ApplyToLockedChars:
PUBLIC
PROC [
ActionProc:
PROC [TiogaOpsDefs.Location]] = {
this is the same as above, except that it should be called when the primary selection is already locked
current, end: TiogaOpsDefs.Location;
[start: current, end: end] ← TiogaOps.GetSelection[];
IF current.node = end.node
THEN FOR i: INT IN [current.where..end.where] DO ActionProc[[current.node, i]] ENDLOOP
ELSE
{
FOR i:
INT
IN [current.where..TextEdit.Size[TiogaButtons.TextNodeRef[current.node]])
DO ActionProc[[current.node, i]]
ENDLOOP;
DO
current.node ← TiogaOps.StepForward[current.node];
IF current.node = end.node THEN
{
FOR i:
INT
IN [0..end.where)
DO ActionProc[[current.node, i]]
ENDLOOP;
RETURN
}
ELSE FOR i: INT IN [0..TextEdit.Size[TiogaButtons.TextNodeRef[current.node]]) DO ActionProc[[current.node, i]] ENDLOOP
ENDLOOP
}
StoreVoiceAtSelection:
PUBLIC
PROC [
voiceViewerInfo: TiogaVoicePrivate.VoiceViewerInfo] RETURNS [succeeded: BOOLEAN ← FALSE] = {
procedure to put voice from a voice viewer into a text viewer
selectedViewer: ViewerClasses.Viewer;
suitableViewer: BOOLEAN;
alreadyVoiceThere: BOOLEAN;
SuitableViewer:
PROC
RETURNS [
BOOLEAN] = {
RETURN [selectedViewer.class.flavor = $Text AND ViewerOps.FetchProp[selectedViewer, $voiceViewerInfo] = NIL] };
AddVoiceMarkerAtPrimarySelection:
PROC [root: TiogaOps.Ref] = {
startChar, endChar, targetChar: TiogaOpsDefs.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[];
IF pendingDelete AND suitableViewer THEN
{ ApplyToLockedChars[DeleteVoiceFromChar];
TiogaOps.SetSelection[viewer: selectedViewer, start: startChar, end: endChar,
level: level, caretBefore: caretBefore,
pendingDelete: FALSE, which: primary] -- simply makes not pending delete
IF suitableViewer THEN
{ targetChar ←
IF caretBefore
THEN startChar
ELSE endChar;
node ← TiogaButtons.TextNodeRef[targetChar.node]; -- just a type convertor
alreadyVoiceThere ← TextEdit.GetCharProp[node, targetChar.where, $voice] # NIL;
IF alreadyVoiceThere THEN RETURN;
TextEdit.PutCharProp[node, targetChar.where, $voice, voiceViewerInfo.ropeInterval.ropeID];
TextEdit.PutCharProp[node, targetChar.where, $textInVoice, TiogaVoicePrivate.RopeFromTextList[voiceViewerInfo.textMarkList]];
next line places a 'talks bubble' on the selected character - see TalksBubbleImpl
TextEdit.PutCharProp[node, targetChar.where, $Artwork, NARROW["TalksBubble", Rope.ROPE]]; -- the NARROW prevents a REF TEXT being created
TiogaVoicePrivate.SetParentViewer[voiceViewerInfo, selectedViewer, targetChar];
Make sure that a $voiceWindow property never points to a viewerInfo with parentViewer=NIL unless you really mean it (important for ValidateSourceMarker).
TextEdit.PutCharProp[node, targetChar.where, $voiceWindow, NEW[TiogaVoicePrivate.VoiceWindowRec ← [label: Rope.Concat["Sound Viewer #", Convert.RopeFromInt[voiceViewerInfo.viewerNumber]]]]];
}
};
IF voiceViewerInfo.ropeInterval.ropeID =
NIL
THEN
RETURN;
TiogaOps.CallWithLocks[AddVoiceMarkerAtPrimarySelection ! TiogaOps.NoSelection => {suitableViewer ← FALSE; CONTINUE}];
test for failure conditions and report them to the user after releasing the viewer lock
IF NOT suitableViewer THEN
{ MessageWindow.Append["Make a selection in a tioga viewer first",
TRUE];
MessageWindow.Blink[]
}
ELSE
{
IF alreadyVoiceThere
THEN
{ MessageWindow.Append["Cannot add sound on top of another sound; delete old sound if desired",
TRUE];
MessageWindow.Blink[]
}
ELSE succeeded ← TRUE
}
DeleteVoiceProc: Menus.MenuProc = {
ApplyToCharsInPrimarySelection[DeleteVoiceFromChar] };
DeleteVoiceFromChar:
PUBLIC
PROC [position: TiogaOpsDefs.Location] = {
Called under TiogaOps locks.
node: TextNode.Ref ← TiogaButtons.TextNodeRef[position.node];
offset: INT ← position.where;
viewerInfo: TiogaVoicePrivate.VoiceViewerInfo ← ValidateSourceMarker[node, offset];
TextEdit.PutCharProp[node, offset, $Artwork, NIL];
TextEdit.PutCharProp[node, offset, $voice, NIL];
TextEdit.PutCharProp[node, offset, $textInVoice, NIL];
IF viewerInfo #
NIL
THEN {
TextEdit.PutCharProp[node, offset, $voiceWindow, NIL];
need to see whether there are any more copies of this source marker/voice rope in this doc before breaking links or indicating edited
TiogaVoicePrivate.MakeVoiceEdited[viewerInfo.viewer];
can no longer guarantee that this rope is stored anywhere
TiogaVoicePrivate.RemoveParentViewer[viewerInfo];
probably better to wait for save voice viewer for now anyway
};
};
PlaySelection:
PUBLIC
PROC = {
selectStream: TiogaAccess.Reader ← TiogaAccess.FromSelection[];
charsInSelection: INT ← 0;
soundsInSelection: INT ← 0;
heavyChar: TiogaAccess.TiogaChar;
props: Atom.PropList;
IF
NOT TiogaAccess.EndOf[selectStream]
THEN
DO
heavyChar ← TiogaAccess.Get[selectStream];
IF TiogaAccess.EndOf[selectStream] THEN EXIT;
charsInSelection ← charsInSelection + 1;
FOR props ← heavyChar.propList, props.rest
WHILE props #
NIL
DO
IF props.first.key = $voice THEN
{ TiogaVoicePrivate.PlayRopeWithoutCue[
NARROW[props.first.val, Rope.
ROPE]];
soundsInSelection ← soundsInSelection + 1
};
ENDLOOP
ENDLOOP;
IF soundsInSelection = 0 THEN MessageWindow.Append["No sounds in selection", TRUE];
DebugRope[IO.PutFR["%d characters in selection\n", IO.int[charsInSelection]]]
};
EditVoiceProc: Menus.MenuProc = {
this procedure is in three parts: first look in the primary selection for characters with the property $voice but not $voiceWindow; next open up a window for each: then add $voiceWindow to the relevant characters. If the expected characters don't occur in the expected order then voice window(s) may be created without association to the parent viewer.
voiceList: LIST OF Rope.ROPE ← NIL;
textInVoiceList: LIST OF Rope.ROPE ← NIL;
newViewerInfoList: TiogaVoicePrivate.VoiceViewerInfoList ←
NIL;
SearchForVoice:
PROC [targetChar: TiogaOpsDefs.Location] = {
node: TextNode.Ref ← TiogaButtons.TextNodeRef[targetChar.node];
voiceRopeID: Rope.ROPE ← NARROW[TextEdit.GetCharProp[node, targetChar.where, $voice], Rope.ROPE];
viewerInfo: TiogaVoicePrivate.VoiceViewerInfo ← ValidateSourceMarker[node, targetChar.where]; -- take this opportunity to fix up if needed
IF voiceRopeID #
NIL
THEN {
fullRope: VoiceRope.VoiceRope ← NEW [VoiceRope.VoiceRopeInterval ← [voiceRopeID, 0, 0]];
fullRope.length ← VoiceRope.Length[handle: thrushHandle, vr: fullRope];
IF fullRope.length <= 0
THEN
MessageWindow.Append["Non-existent or zero length voice utterance(s) found in selection", TRUE]
ELSE {
IF viewerInfo =
NIL
THEN {
voiceList ← AppendRope[voiceList, voiceRopeID];
textInVoiceList ← AppendRope[textInVoiceList, NARROW[TextEdit.GetCharProp[node, targetChar.where, $textInVoice], Rope.ROPE]]
}
ELSE DebugRope["Voice already has an associated viewer\n"]
}
}
};
AddVoiceWindowProps:
PROC [targetChar: TiogaOpsDefs.Location] = {
because we are under a tioga lock, it is safe to muck with the 'edited' status of the viewer
philosophically, putting a source marker in a voice viewer does not constitute editing it, so keep the 'edited' status of the viewer constant through this operation
node: TextNode.Ref ← TiogaButtons.TextNodeRef[targetChar.node];
voiceRopeID: Rope.ROPE ← NARROW[TextEdit.GetCharProp[node, targetChar.where, $voice], Rope.ROPE];
IF voiceList = NIL THEN RETURN;
IF voiceRopeID = NIL THEN RETURN;
IF TextEdit.GetCharProp[node, targetChar.where, $voiceWindow] #
NIL
THEN
RETURN;
We just broke all invalid links in SearchForVoice above, so this is just an already open voice viewer.
IF voiceRopeID.Equal[voiceList.first] THEN
{ parentViewer: ViewerClasses.Viewer ← TiogaOps.GetSelection[].viewer;
alreadyEdited: BOOLEAN ← parentViewer.newVersion;
TiogaVoicePrivate.SetParentViewer[newViewerInfoList.first, parentViewer, targetChar];
Make sure that a $voiceWindow property never points to a viewerInfo with parentViewer=NIL unless you really mean it (important for ValidateSourceMarker).
TextEdit.PutCharProp[node, targetChar.where, $voiceWindow, NEW[TiogaVoicePrivate.VoiceWindowRec ← [label: Rope.Concat["Sound Viewer #", Convert.RopeFromInt[newViewerInfoList.first.viewerNumber]]]]];
IF ~alreadyEdited THEN
{ parentViewer.newVersion ←
FALSE;
ViewerOps.PaintViewer[parentViewer, caption, FALSE]
};
voiceList ← voiceList.rest;
newViewerInfoList ← newViewerInfoList.rest;
we only move down the voice (etc.) lists if the char property voice rope ID matched the head of the voice list. A non-match may be due to the rope being non-existent (and hence having been filtered out from voiceList by SearchForVoice). The other possibility is that the selection has been changed, in which case voice viewers may or may not be given correct links to their parents
};
};
ApplyToCharsInPrimarySelection[SearchForVoice ! TiogaOps.NoSelection => GOTO Quit];
IF voiceList = NIL THEN
{ MessageWindow.Append["No undisplayed sounds in selection",
TRUE];
RETURN
};
FOR l:
LIST
OF Rope.
ROPE ← voiceList, l.rest
WHILE l #
NIL
DO
newInfo: TiogaVoicePrivate.VoiceViewerInfo;
[viewerInfo: newInfo] ← TiogaVoicePrivate.BuildVoiceViewer[voiceID: l.first, textInVoice: textInVoiceList.first, youngVoice: FALSE];
newViewerInfoList ← AppendViewerInfo[newViewerInfoList, newInfo];
textInVoiceList ← textInVoiceList.rest
ENDLOOP;
ApplyToCharsInPrimarySelection[AddVoiceWindowProps ! TiogaOps.NoSelection => CONTINUE]
DeleteSourceMarkers:
PUBLIC
PROC [viewer: ViewerClasses.Viewer, voiceViewerNumber:
INT, exceptionLoc: TiogaOpsDefs.Location] = {
Called when a voice viewer is stored (so old links should be broken) or destroyed. If exceptionLoc is not NIL, it represents a marker we just added, so don't delete it. This routine is called with the voice's parent viewer, so all links we find are guaranteed to be valid.
DeleteSourceMarker: LocActionProc = {
PROC [node: TextNode.Ref, offset: INT, charProp: REF]
IF CheckVoiceWindowProp[propVal: voiceWindowRope, charProp: charProp]
AND
NOT (node=TiogaButtons.TextNodeRef[exceptionLoc.node]
AND offset=exceptionLoc.where)
THEN {
foundOne ← TRUE;
TextEdit.PutCharProp[node, offset, $voiceWindow, NIL];
may be that the source marker was put in as a result of DictationMachine being bugged, in which case there is no voice rope present - remove the artwork
IF TextEdit.GetCharProp[node, offset, $voice] =
NIL
THEN
TextEdit.PutCharProp[node, offset, $Artwork, NIL];
};
};
DoIt:
PROC [root: TiogaOps.Ref] = {
MapCharProps[viewer: viewer, propName: $voiceWindow, actionProc: DeleteSourceMarker];
rootNode: TiogaOps.Ref ← TiogaOps.ViewerDoc[viewer];
alreadyEdited:
BOOL ← viewer.newVersion;
see comments about 'edited' status at head of procedure AddVoiceWindowProps
foundOne: BOOL ← FALSE;
voiceWindowRope: Rope.ROPE ← Rope.Concat["Sound Viewer #", Convert.RopeFromInt[voiceViewerNumber]];
TiogaOps.CallWithLocks[DoIt, rootNode];
IF
NOT foundOne
AND exceptionLoc.node=
NIL
THEN {
It wasn't there and we aren't in the process of inserting a new one, so don't bother to look again.
DebugRope["Source marker not found in expected document!!\n"];
TiogaVoicePrivate.RemoveParentViewer[TiogaVoicePrivate.GetVoiceViewerInfo[voiceViewerNumber]];
}
ELSE IF ~alreadyEdited
THEN {
viewer.newVersion ← FALSE;
ViewerOps.PaintViewer[viewer, caption, FALSE];
};
SaveRopeAtSourceMarkers:
PUBLIC
PROC [viewer: ViewerClasses.Viewer, voiceViewerNumber:
INT, voiceRopeID: Rope.
ROPE, textInVoice: Rope.
ROPE]
RETURNS [succeeded:
BOOL] = {
Called when a voice viewer is saved. This routine is called with the voice's parent viewer, so all links we find are guaranteed to be valid.
SaveRopeAtSourceMarker: LocActionProc = {
PROC [node: TextNode.Ref, offset: INT, charProp: REF]
IF CheckVoiceWindowProp[propVal: voiceWindowRope, charProp: charProp]
THEN {
TextEdit.PutCharProp[node, offset, $voice, voiceRopeID];
TextEdit.PutCharProp[node, offset, $textInVoice, textInVoice];
succeeded ← TRUE;
};
};
DoIt:
PROC [root: TiogaOps.Ref] = {
MapCharProps[viewer: viewer, propName: $voiceWindow, actionProc: SaveRopeAtSourceMarker];
rootNode: TiogaOps.Ref ← TiogaOps.ViewerDoc[viewer];
voiceWindowRope: Rope.ROPE ← Rope.Concat["Sound Viewer #", Convert.RopeFromInt[voiceViewerNumber]];
succeeded ← FALSE;
TiogaOps.CallWithLocks[DoIt, rootNode];
IF
NOT succeeded
THEN
-- since it wasn't there, don't bother to look again
TiogaVoicePrivate.RemoveParentViewer[TiogaVoicePrivate.GetVoiceViewerInfo[voiceViewerNumber]];
ScanForVoice: TiogaOps.CommandProc = {
IF viewer.newFile
OR viewer.newVersion
THEN
RETURN[quit: RecordVoiceInstancesAtRoot[viewer]];
};
DeleteLinks: Menus.MenuProc = {
this procedure severs any parent pointers that voice viewers might have back to this viewer and removes the source marker artworks. [It can also be used to get rid of source marker artworks embarrassingly left in a file by bad tracking etc.] Needn't worry about whether or not these links are valid.
viewer: ViewerClasses.Viewer ← NARROW[parent];
RemoveAnySourceMarker: LocActionProc = {
PROC [node: TextNode.Ref, offset: INT, charProp: REF]
TextEdit.PutCharProp[node, offset, $voiceWindow, NIL];
};
DoIt:
PROC [root: TiogaOps.Ref] = {
MapCharProps[viewer: viewer, propName: $voiceWindow, actionProc: RemoveAnySourceMarker];
rootNode: TiogaOps.Ref ← TiogaOps.ViewerDoc[viewer];
alreadyEdited:
BOOL ← viewer.newVersion;
see comments about 'edited' status at head of procedure AddVoiceWindowProps
TiogaOps.CallWithLocks[DoIt, rootNode];
IF ~alreadyEdited
THEN {
viewer.newVersion ← FALSE;
ViewerOps.PaintViewer[viewer, caption, FALSE];
};
TiogaVoicePrivate.RemoveParentPointersTo[viewer: viewer, findSplits: FALSE];
RecordVoiceInstancesAtRoot:
PROC [viewer: ViewerClasses.Viewer]
RETURNS [quit:
BOOL ←
FALSE] = {
Consider locking issue: this routine used to read the file with TiogaAccess, so didn't worry.
rootNode: TextNode.Ref ← TiogaButtons.TextNodeRef[TiogaOps.ViewerDoc[viewer]];
voiceList: Rope.ROPE ← NIL;
DoIt:
PROC [root: TiogaOps.Ref] = {
TextEdit.PutProp[rootNode, $voicelist, voiceList];
soundsInDocument: INT ← 0;
userConfirmed: BOOL ← FALSE;
DebugRope["Voice messages in document:"];
FOR n: TextNode.Ref ← rootNode, TextNode.StepForward[n]
UNTIL n =
NIL
DO
FOR i:
INT
IN [0..TextEdit.Size[n])
DO
voiceRope: Rope.ROPE;
viewerInfo: TiogaVoicePrivate.VoiceViewerInfo ← ValidateSourceMarker[n, i];
IF viewerInfo#
NIL
THEN {
The voiceWindow is really linked to this doc, so if the voiceWindow has been edited, ask user to confirm save without those edits.
IF
NOT userConfirmed
AND viewerInfo.edited
THEN {
IF
NOT MessageWindow.Confirm["Confirm discard of voice edits . . . "]
THEN {
MessageWindow.Append["Save aborted."];
RETURN[TRUE];
}
ELSE userConfirmed ← TRUE;
};
};
voiceRope ← NARROW[TextEdit.GetCharProp[n, i, $voice]];
IF voiceRope#
NIL
THEN {
DebugRope["\n"];
DebugRope[voiceRope];
soundsInDocument ← soundsInDocument + 1;
voiceList ← Rope.Cat[voiceList, "&", voiceRope];
};
ENDLOOP;
ENDLOOP;
TiogaOps.CallWithLocks[DoIt, TiogaButtons.TiogaOpsRef[rootNode]];
IF soundsInDocument = 0 THEN DebugRope[" none\n"] ELSE DebugRope["\n"]
};
InstallMenuButton:
PROC [name: Rope.
ROPE, proc: Menus.MenuProc] = {
old: Menus.MenuEntry =
Menus.FindEntry[menu: TiogaMenuOps.tiogaMenu, entryName: name];
new: Menus.MenuEntry =
Menus.CreateEntry[name: name, proc: proc];
IF old =
NIL
THEN Menus.AppendMenuEntry[TiogaMenuOps.tiogaMenu, new]
ELSE Menus.ReplaceMenuEntry[TiogaMenuOps.tiogaMenu, old, new];
};
thrushHandle: PUBLIC VoiceRope.Handle ← VoiceRope.Open[];
voiceMenu: Menus.MenuEntry;
voiceButtonQueue:
PUBLIC MBQueue.Queue ← MBQueue.Create[];
all buttons registered using MBQueue.CreateMenuEntry are serialised on this one queue: all the buttons in this voice system, or having to do with voice [e.g. Finch buttons], ought to be put on this queue, except for those which simply toggle menus on and off the display
AppendRope:
PROC [list:
LIST
OF Rope.
ROPE, entry: Rope.
ROPE]
RETURNS [
LIST
OF Rope.
ROPE] = {
oneElementList: LIST OF Rope.ROPE ← CONS[entry, NIL];
hangOffPoint: LIST OF Rope.ROPE ← list;
IF list = NIL THEN RETURN [oneElementList];
WHILE hangOffPoint.rest # NIL DO hangOffPoint ← hangOffPoint.rest ENDLOOP;
hangOffPoint.rest ← oneElementList;
RETURN [list]
};
AppendViewerInfo:
PROC [list:
LIST
OF TiogaVoicePrivate.VoiceViewerInfo, entry: TiogaVoicePrivate.VoiceViewerInfo]
RETURNS [
LIST
OF TiogaVoicePrivate.VoiceViewerInfo] = {
oneElementList: LIST OF TiogaVoicePrivate.VoiceViewerInfo ← CONS[entry, NIL];
hangOffPoint: LIST OF TiogaVoicePrivate.VoiceViewerInfo ← list;
IF list = NIL THEN RETURN [oneElementList];
WHILE hangOffPoint.rest # NIL DO hangOffPoint ← hangOffPoint.rest ENDLOOP;
hangOffPoint.rest ← oneElementList;
RETURN [list]
};
debugStream: IO.STREAM ← NIL;
DebugRope:
PUBLIC
PROC [rope: Rope.
ROPE] = {
IF debugStream # NIL THEN debugStream.PutF[rope]
};
DebugStreamInit: Commander.CommandProc = { debugStream ← cmd.out };
VoiceInterests
a mechanism to track all files copied to global file space and register interest in any voice contained in them, plus a command to register interests in any voice found in a named file [which must be global or with global attachment: the interest is registered for the global name]
the code is very inefficient in the way that it deals with files: it opens the file as a stream to see if it is a tioga file and then gets a TiogaAccess.Reader on each tioga file, which means making a complete copy of the file into TiogaAccess.Reader format, and then reads its root properties
faster code could be produced by using the 'control' rope reader returned by FileReader.FromStream but that means writing fairly grubby code that knows a lot about the tioga file format [see FileOps.mesa in tioga.df if interested]
backgroundCommentary:
IO.
STREAM ←
NIL;
lastRemoteEvent:
REF
READONLY FSBackdoor.RemoteEvent ←
NIL;
MonitorCopiesToGlobal:
PROC = {
-- invoked as a separate process
Process.SetPriority[Process.priorityBackground];
DO
lastRemoteEvent ← FSBackdoor.NextRemoteEvent[lastRemoteEvent];
IF lastRemoteEvent.op = endStoring THEN RegisterInterest[lastRemoteEvent.fName, backgroundCommentary]
ENDLOOP
RegisterInterest:
PROC [file: Rope.
ROPE, commentary:
IO.
STREAM ←
NIL] = {
fullFName, attachedTo, globalName: Rope.ROPE;
keep: CARDINAL;
voiceList: Rope.ROPE ← NIL;
createDate: BasicTime.GMT;
Commentate: PROC [remark: Rope.ROPE] = { IF commentary # NIL THEN commentary.PutF[remark] };
[fullFName: fullFName, attachedTo: attachedTo, keep: keep, created: createDate] ← FS.FileInfo[file]; -- read FS.Mesa to understand the next few lines
Commentate[IO.PutFR["File %g:\n", IO.rope[fullFName]]];
IF attachedTo = NIL
THEN
{
IF keep # 0
THEN
ERROR LocalFile[]
-- neither a global file or a local one with global attachment
ELSE { globalName ← fullFName; Commentate["File is global:\n"] }
}
ELSE { globalName ← attachedTo; Commentate[IO.PutFR["File is attached to global file %g:\n", IO.rope[globalName]]] };
{ fileStream:
IO.
STREAM;
streamOptions: FS.StreamOptions ← FS.defaultStreamOptions;
tiogaFile: BOOLEAN;
in order to determine if some file is a tioga file, we need to open it as a plain file [hence following line] and use FileReader.FromStream to determine whether it is of the correct format. FromStream also returns the text, comments and control sections of the documents if we are interested [see comments at the head of this code]
streamOptions[tiogaRead] ← FALSE;
fileStream ← FS.StreamOpen[fileName: fullFName, streamOptions: streamOptions];
[tiogaFile: tiogaFile] ← FileReader.FromStream[fileStream, LAST[INT]];
IF ~tiogaFile THEN
{ Commentate[" Not a tioga file\n"];
RETURN
}
};
{ fileStream: TiogaAccess.Reader ← TiogaAccess.FromFile[fullFName];
rootChar: TiogaAccess.TiogaChar ← TiogaAccess.Get[fileStream]; -- first character produces the root properties
FOR rootProps: Atom.PropList ← rootChar.propList, rootProps.rest
WHILE rootProps #
NIL
DO
IF rootProps.first.key = $voicelist THEN { IF voiceList = NIL THEN voiceList ← NARROW[rootProps.first.val, Rope.ROPE] ELSE ERROR }
ENDLOOP
};
IF voiceList = NIL
THEN
{ Commentate[" No voice in file\n"];
RETURN
};
Commentate[" Voice message IDs are\n"];
{ nextVoice: Rope.
ROPE;
startOfID: INT ← 1;
endOfID: INT;
IF NOT ( voiceList.Length > 0 AND voiceList.Fetch[0] = '& ) THEN ERROR;
DO
endOfID ← voiceList.Find["&", startOfID];
nextVoice ← voiceList.Substr[startOfID, IF endOfID = -1 THEN Rope.MaxLen ELSE endOfID - startOfID];
IF nextVoice.Length = 0 THEN ERROR;
the voiceRope specification in this procedure call is only used to extract the RopeID - the start and length are not used
VoiceRope.Retain[handle: thrushHandle, vr: NEW [VoiceRope.VoiceRopeInterval ← [nextVoice, 0, 0]], refID: globalName.Cat[" ", Convert.RopeFromTime[createDate]], class: "TiogaVoice"];
Commentate[nextVoice]; Commentate["\n"];
IF endOfID = -1 THEN EXIT;
startOfID ← endOfID + 1
ENDLOOP
}
RegisterVoiceInterest: Commander.CommandProc = {
{
ENABLE {
FS.Error => IF error.group # user THEN REJECT ELSE {msg ← error.explanation; GOTO Quit};
LocalFile => {msg ← "File must be global or have a global attachment"; GOTO Quit};
};
argv: CommandTool.ArgumentVector;
argv ← CommandTool.Parse[cmd ! CommandTool.Failed => {msg ← errorMsg; GOTO Quit}];
IF argv.argc # 2
THEN {
msg ← "Usage: RegisterVoiceInterest inFile";
GOTO Quit;
};
RegisterInterest[argv[1], cmd.out]
EXITS
Quit => RETURN [$Failure, msg]
backgroundCommentaryInit: Commander.CommandProc = { backgroundCommentary ← cmd.out };
p: PROCESS ← FORK MonitorCopiesToGlobal;
TalksBubbles
routines to insert distinctive markers around a character to indicate that it contains voice
TalksBubbleDataRep:
TYPE ~
RECORD [
letter: CHAR,
label: Rope.ROPE,
ascent: REAL,
descent: REAL,
width: REAL,
bearoff: REAL,
firstCharWidth: REAL
];
TalksBubblePaint:
PROC [self: TEditFormat.CharacterArtwork, context: Imager.Context] ~ {
data: REF TalksBubbleDataRep ~ NARROW[self.data];
DrawBubble: ImagerPath.PathProc = {
height: REAL ← data.ascent + data.descent;
moveTo[[data.bearoff, -data.descent+(height/4)]];
lineTo[[data.bearoff, -data.descent+(3*height/4)]];
arcTo[[data.bearoff+(data.width/2), -data.descent+height], [data.bearoff + data.width, -data.descent+(3*height/4)]];
lineTo[[data.bearoff+data.width, -data.descent+(height/4)]];
arcTo[[data.bearoff+(15*data.width/16), -data.descent+(height/16)], [data.bearoff + (3*data.width/4), -data.descent]];
lineTo[[data.bearoff+(data.width/4), -data.descent-(height/2)]];
lineTo[[data.bearoff+(data.width/2), -data.descent]];
lineTo[[data.bearoff+(data.width/4), -data.descent]];
arcTo[[data.bearoff+(data.width/16), -data.descent+(height/16)], [data.bearoff, -data.descent+(height/4)]];
DrawBox: ImagerPath.PathProc = {
moveTo[[data.bearoff+data.firstCharWidth, -data.descent]];
lineTo[[data.bearoff+data.firstCharWidth, data.ascent]];
lineTo[[data.bearoff+data.width, data.ascent]];
lineTo[[data.bearoff+data.width, -data.descent]];
lineTo[[data.bearoff+data.firstCharWidth, -data.descent]]
Imager.Move[context];
Imager.ShowChar[context, data.letter];
IF data.label = NIL
THEN
{ Imager.SetStrokeWidth[context, 1.0];
Imager.SetStrokeJoint[context, round];
Imager.MaskStroke[context, DrawBubble, TRUE]
}
ELSE
{ Imager.SetColor[context, textColor];
Imager.ShowRope[context, data.label];
Imager.SetStrokeWidth[context, 1.0];
Imager.SetStrokeJoint[context, round];
Imager.MaskStroke[context, DrawBox, TRUE]
}
TalksBubbleFormat:
PROC [class: TEditFormat.CharacterArtworkClass, loc: TextNode.Location, style: NodeStyle.Ref, kind: NodeStyleOps.OfStyle]
RETURNS [TEditFormat.CharacterArtwork] ~ {
charSet: TextEdit.CharSet ← TextEdit.FetchChar[loc.node, loc.where].charSet;
letter: CHAR ← TextEdit.FetchChar[loc.node, loc.where].char;
ascent: REAL ← NodeStyle.GetReal[style, backgroundAscent];
descent: REAL ← NodeStyle.GetReal[style, backgroundDescent];
width: REAL;
bearoff: REAL ← NodeStyle.GetReal[style, outlineboxBearoff];
escapement: Vector2.VEC ← ImagerFont.Escapement[TEditFormat.GetFont[style], [set: charSet, code: letter-'\000]];
firstCharWidth: REAL ← escapement.x;
label: Rope.ROPE ← NIL;
IF ValidateSourceMarker[loc.node, loc.where,
TRUE]#
NIL
THEN
Can't remove the $voiceWindow property from inside a paintproc, because it locks the document for reading.
label ← GetVoiceWindowRope[loc.node, loc.where];
IF label #
NIL
THEN
FOR i:
INT
IN [0..label.Length)
DO
escapement.x ← escapement.x + ImagerFont.Escapement[TEditFormat.GetFont[style], [set: charSet, code: label.Fetch[i]-'\000]].x
ENDLOOP;
width ← escapement.x;
IF ascent+descent <= 0.0
THEN {
fontBoundingBox: ImagerFont.Extents ~ ImagerFont.FontBoundingBox[TEditFormat.GetFont[style]];
ascent ← fontBoundingBox.ascent + bearoff;
descent ← fontBoundingBox.descent - bearoff;
};
{ data:
REF TalksBubbleDataRep ~
NEW[TalksBubbleDataRep ← [
letter: letter,
label: label,
ascent: ascent,
descent: descent,
width: width,
bearoff: bearoff,
firstCharWidth: firstCharWidth
]];
extents: ImagerFont.Extents ← [leftExtent: -data.bearoff+2.0, rightExtent: data.bearoff+data.width+2.0, ascent: data.ascent+2.0, descent: (data.descent+data.ascent)/2];
RETURN [NEW[TEditFormat.CharacterArtworkRep ← [paint: TalksBubblePaint, extents: extents, escapement: escapement, data: data]]]
}
talksBubbleClass: TEditFormat.CharacterArtworkClass ~
NEW[TEditFormat.CharacterArtworkClassRep ← [
name: $TalksBubble,
format: TalksBubbleFormat,
data: NIL
textColor: ImagerColor.ConstantColor ← ImagerColor.ColorFromStipple[7BDEH, [or, null]];
SourceMarkerTracking
code that ensures that the records of parent viewers & positions within them attached to voice viewers are kept consistent as the parent viewers are edited
first section deals with what happens when you delete a text viewer which is pointed to by voice viewer(s)
ViewerAndDestroyProc:
TYPE =
RECORD [
viewer: ViewerClasses.Viewer,
destroyProc: ViewerEvents.EventRegistration
];
registeredViewerList:
LIST
OF ViewerAndDestroyProc;
this is a list of all the viewers in which source markers have been created and which therefore have a destroyProc set up to sever parent links. When the destroyProc is called the viewer will be deleted from this list; however when all relevant source markers have been deleted from the viewer we do not bother to delete it from the list
RegisterViewer:
PUBLIC
PROC [viewer: ViewerClasses.Viewer] = {
called everytime a source marker is created within a text viewer
alreadyInList: BOOLEAN ← FALSE;
FOR l: LIST OF ViewerAndDestroyProc ← registeredViewerList, l.rest WHILE l # NIL AND ~alreadyInList DO IF l.first.viewer = viewer THEN alreadyInList ← TRUE ENDLOOP;
IF ~alreadyInList THEN
{ newEntry: ViewerAndDestroyProc ← [viewer, ViewerEvents.RegisterEventProc[
proc: DestroyViewerEvent, event: destroy, filter: viewer, before:
TRUE]];
registeredViewerList ← CONS [newEntry, registeredViewerList]
}
DestroyViewerEvent: ViewerEvents.EventProc = {
firstEntry: BOOLEAN ← viewer = registeredViewerList.first.viewer;
previousEntry: LIST OF ViewerAndDestroyProc ← registeredViewerList;
IF ~firstEntry
THEN
WHILE previousEntry.rest.first.viewer # viewer
DO
previousEntry ← previousEntry.rest
ENDLOOP;
if this runs off the end then something has gone seriously wrong
ViewerEvents.UnRegisterEventProc[(IF firstEntry THEN registeredViewerList ELSE previousEntry.rest).first.destroyProc, destroy];
IF firstEntry THEN registeredViewerList ← registeredViewerList.rest
ELSE previousEntry.rest ← previousEntry.rest.rest;
TiogaVoicePrivate.RemoveParentPointersTo[viewer: viewer, findSplits: TRUE];
};
remainder of the code deals with tracking pointers correctly as the text they point to is edited
TrackDeletes:
PUBLIC
PROC = {
called before the primary selection is deleted, if in a text viewer
N.B. that the selection may span more than one node
viewer: ViewerClasses.Viewer;
start, end, current: TiogaOpsDefs.Location;
infoList: LIST OF TiogaVoicePrivate.VoiceViewerInfo ← NIL;
SeverLinks:
PROC [node: TiogaOpsDefs.Ref, from, to:
INT] = {
FOR l: TiogaVoicePrivate.VoiceViewerInfoList ← infoList, l.rest
WHILE l #
NIL
DO
IF l.first.positionInParent.node = node AND l.first.positionInParent.where IN [from..to]
THEN
{ l.first.parentViewer ←
NIL;
DebugRope[IO.PutFR["detaching parent link for voice viewer %d\n", IO.int[l.first.viewerNumber]]]
}
ENDLOOP
};
[viewer: viewer, start: start, end: end] ← TiogaOps.GetSelection[];
infoList ← TiogaVoicePrivate.FindAttachedVoiceViewers[viewer];
IF infoList = NIL THEN RETURN;
we now have a list of all the voice viewers whose parent links point at the viewer containing the primary selection: we need to go through all the nodes in the selection severing the links which point within the selection and altering those which lie in the last node of the selection, beyond the end of the selection
IF start.node = end.node
THEN SeverLinks[start.node, start.where, end.where]
ELSE
{ current ← start;
SeverLinks[current.node, current.where, TextEdit.Size[TiogaButtons.TextNodeRef[current.node]]-1];
DO
current.node ← TiogaOps.StepForward[current.node];
IF current.node = end.node THEN
{ SeverLinks[current.node, 0, end.where];
EXIT
}
ELSE SeverLinks[current.node, 0, TextEdit.Size[TiogaButtons.TextNodeRef[current.node]]-1]
ENDLOOP
};
{ positionAdjustment:
INT ← end.where - start.where + 1;
FOR l: TiogaVoicePrivate.VoiceViewerInfoList ← infoList, l.rest
WHILE l #
NIL
DO
IF l.first.positionInParent.node = end.node
AND l.first.positionInParent.where > end.where
THEN
l.first.positionInParent ← [start.node, l.first.positionInParent.where - positionAdjustment]
ENDLOOP
}
};
Utilities
WhereData: TYPE ~ REF WhereDataRec;
WhereDataRec:
TYPE ~
RECORD [
root: TiogaOps.Ref,
node: TextNode.Ref,
offset: INT
];
BreakVoiceWindowLinks: ViewerForkers.CallBack = {
CallBack: TYPE = PROC [data: REF]
DoIt:
PROC [root: TiogaOps.Ref] = {
TextEdit.PutCharProp[whereData.node, whereData.offset, $voiceWindow, NIL];
};
whereData: WhereData ← NARROW[data];
TiogaOps.CallWithLocks[DoIt, whereData.root];
};
ValidateSourceMarker:
PROC [node: TextNode.Ref, offset:
INT, locked:
BOOL ←
FALSE]
RETURNS [viewerInfo: TiogaVoicePrivate.VoiceViewerInfo ←
NIL] ~ {
If this character has a $voiceWindow property and the associated voice viewer believes it's connected to this document, then return the viewerInfo for that viewer. Otherwise remove the property and return NIL.
Links from text documents to voice viewers do not survive copies/moves to different documents because it would be too hard to search every visible document for a possible link. Two other options would be possible: Tioga could provide help via a callback when properties are moved or copied, or TiogaVoice could monitor all moves, copies, and undos.
root: TiogaOps.Ref;
v: TiogaVoicePrivate.VoiceWindowRef ← NARROW[TextEdit.GetCharProp[node, offset, $voiceWindow]];
IF v=NIL THEN RETURN;
viewerInfo ← TiogaVoicePrivate.GetVoiceViewerInfo[GetVoiceViewerNumber[v.label]];
IF viewerInfo=NIL THEN RETURN;
root ← TiogaButtons.TiogaOpsRef[TextNode.Root[node]];
IF viewerInfo.parentViewer=
NIL
OR root#TiogaOps.ViewerDoc[viewerInfo.parentViewer]
THEN {
Break the link from the text document to the voice viewer. Can't break the link in the other direction (from viewerInfo) because we don't know whether this character got here by a move or a copy.
whereData: WhereData ← NEW[WhereDataRec ← [root, node, offset]];
IF locked THEN ViewerForkers.ForkCall[proc: BreakVoiceWindowLinks, data: whereData]
ELSE BreakVoiceWindowLinks[whereData];
Can't simply remove the $voiceWindow property from inside a paintproc, because it locks the document for reading.
viewerInfo ← NIL;
};
};
GetVoiceViewerNumber:
PROC [voiceLabel: Rope.
ROPE]
RETURNS [
INT] = {
RETURN[Convert.IntFromRope[voiceLabel.Substr[voiceLabel.Find["#"]+1]]];
GetVoiceWindowRope:
PROC [node: TextNode.Ref, offset:
INT]
RETURNS [Rope.
ROPE] ~ {
v: TiogaVoicePrivate.VoiceWindowRef ← NARROW[TextEdit.GetCharProp[node, offset, $voiceWindow]];
IF v#NIL THEN RETURN[v.label] ELSE RETURN[NIL];
};
CheckPropProc: TYPE ~ PROC [propVal: REF, charProp: REF] RETURNS [BOOL];
LocActionProc:
TYPE ~
PROC [node: TextNode.Ref, offset:
INT, charProp:
REF];
CheckVoiceWindowProp: CheckPropProc ~ {
IF charProp#
NIL
THEN {
v: TiogaVoicePrivate.VoiceWindowRef ← NARROW[charProp];
IF Rope.Equal[v.label, NARROW[propVal, Rope.ROPE]] THEN RETURN[TRUE];
};
RETURN [FALSE]
};
MapCharProps:
PROC [viewer: ViewerClasses.Viewer, propName:
ATOM, actionProc: LocActionProc] ~ {
Maps actionProc over all non-NIL occurrences of character property propName in the document represented by viewer.
rootNode: TextNode.Ref ← TiogaButtons.TextNodeRef[TiogaOps.ViewerDoc[viewer]]; -- TextNodeRef is just a type convertor - replace with a more cautious implementation??
FOR n: TextNode.Ref ← rootNode, TextNode.StepForward[n]
UNTIL n =
NIL
DO
IF n.hascharprops
THEN
FOR i:
INT
IN [0..TextEdit.Size[n])
DO
charProp: REF ← TextEdit.GetCharProp[n, i, propName];
IF charProp#NIL THEN actionProc[n, i, charProp];
ENDLOOP;
ENDLOOP;
};
FindCharProp:
PROC [viewer: ViewerClasses.Viewer, propName:
ATOM, propVal:
REF, checkPropProc: CheckPropProc, locHint: TiogaOpsDefs.Location]
RETURNS [foundLoc: TiogaOpsDefs.Location] ~ {
Searches document for occurrence of character property propName with value propVal. Starts with locHint if specified, then locHint.node, then searches forward from the beginning, skipping locHint.node.
Search:
PROC [startnode, endnode: TextNode.Ref] ~ {
Search nodes [startnode..endnode) for property. No search if startnode=endnode.
FOR n: TextNode.Ref ← startnode, TextNode.StepForward[n]
UNTIL n = endnode
DO
startChar: INT ← 0;
endChar: INT ← TextEdit.Size[n];
SearchCharProps:
PROC
RETURNS [found:
BOOLEAN ←
FALSE] ~ {
FOR i:
INT
IN [startChar..endChar)
UNTIL found
DO
charProp: REF ← TextEdit.GetCharProp[n, i, propName];
IF checkPropProc[propVal, charProp] THEN {foundLoc.where ← i; found ← TRUE;}
ENDLOOP;
};
IF n.hascharprops
AND SearchCharProps[].found
THEN {
foundLoc.node ← TiogaButtons.TiogaOpsRef[n];
RETURN;
};
ENDLOOP;
};
hintNode, nextNode: TextNode.Ref ← NIL;
foundLoc.node ← NIL;
hintNode ← TiogaButtons.TextNodeRef[locHint.node];
IF TextNode.Root[hintNode]=
NIL
THEN hintNode ←
NIL;
Indicates hintNode has been deleted - shouldn't be needed if all else has gone well. PTZ, June 12, 1987 7:32:27 pm PDT
IF hintNode#
NIL
THEN {
IF locHint.where < TextEdit.Size[hintNode]
AND checkPropProc[propVal, TextEdit.GetCharProp[hintNode, locHint.where, propName]]
THEN
RETURN [locHint];
nextNode ← TextNode.StepForward[hintNode];
Search[hintNode, nextNode];
IF foundLoc.node#NIL THEN RETURN;
};
Search[TiogaButtons.TextNodeRef[TiogaOps.ViewerDoc[viewer]], hintNode];
IF foundLoc.node#NIL THEN RETURN;
Search[nextNode, NIL]; -- = Search[NIL, NIL] if no locHint
};
Initialization
**** voiceButtonQueue should be deleted, or used for ALL button activities
Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "DeleteLinks", DeleteLinks], 1];
Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "DictationMachine", TiogaVoicePrivate.DictationMachine], 1];
Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "DeleteVoice", DeleteVoiceProc], 1];
Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "EditVoice", EditVoiceProc], 1];
Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "STOP",
TiogaVoicePrivate.CancelProc], 1];
Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "PlayVoice", TiogaVoicePrivate.PlayBackMenuProc], 1];
Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "AddVoice", TiogaVoicePrivate.AddVoiceProc], 1];
voiceMenu ← Menus.GetLine[TiogaMenuOps.tiogaMenu, 1];
InstallMenuButton["Voice", VoiceMenu];
Menus.SetLine[TiogaMenuOps.tiogaMenu, 1,
NIL];
TiogaOps.RegisterCommand[name: $RedSave, proc: ScanForVoice];
TiogaOps.RegisterCommand[name: $YellowSave, proc: ScanForVoice];
TiogaOps.RegisterCommand[name: $BlueSave, proc: ScanForVoice];
TiogaOps.RegisterCommand[name: $RedStore, proc: ScanForVoice];
TiogaOps.RegisterCommand[name: $YellowStore, proc: ScanForVoice];
TiogaOps.RegisterCommand[name: $BlueStore, proc: ScanForVoice];
TRUSTED {Process.Detach[p]};
Commander.Register[key: "RegisterVoiceInterest", proc: RegisterVoiceInterest, doc: "RegisterVoiceInterest inFile: register loganberry interests for all voice messages in a global or globally attached file"];
Commander.Register[key: "InterestInfo", proc: backgroundCommentaryInit, doc: "InterestInfo: registers an output stream for commentary about voice interest registration"];
TEditFormat.RegisterCharacterArtwork[talksBubbleClass];
}.
Swinehart, April 9, 1987 10:42:03 am PDT
Gathers up VoiceInTextImpl, VoiceInterestImpl, TalksBubbleImpl, SourceMarkerTrackingImpl
Polle Zellweger (PTZ) June 11, 1987 4:11:26 pm PDT
Treat VoiceViewerInfo.positionInParent as only a hint for location of associated SourceMarker; untracked edits may change location.
changes to: VoiceInTextImpl, DeleteSourceMarker, SaveRopeAtSourceMarker, GetVoiceWindowRope, CheckPropProc, CheckVoiceWindowProp, FindCharProp, Search(local of FindCharProp), SearchCharProps (local of SearchNode, local of FindCharProp)
Polle Zellweger (PTZ) June 12, 1987 7:32:46 pm PDT
changes to: FindCharProp
Polle Zellweger (PTZ) June 17, 1987 11:11:34 am PDT
Link to sibling split text viewer if appropriate when text viewer destroyed. Set voice viewer status=edited when voice deleted from doc (pessimistic view; might be stored elsewhere).
changes to: DIRECTORY, DeleteVoiceFromChar, RemoveParentPointersTo
Polle Zellweger (PTZ) June 18, 1987 2:16:09 pm PDT
changes to: DIRECTORY, ScanForVoice (tell Tioga if Save should abort), RecordVoiceInstancesAtRoot (request user confirmation if there are linked unsaved voice viewers), FindCharProp
Polle Zellweger (PTZ) June 20, 1987 11:20:18 pm PDT
Changes for new policy: voice markers in text viewers are only a hint; must be validated (whenever possible: painting talks bubbles, saving doc, etc.). Voice markers can't be copied or moved across doc boundaries.
changes to: DIRECTORY, DeleteVoiceFromChar, SearchForVoice, AddVoiceWindowProps, DeleteSourceMarkers, GetVoiceWindowRope, SaveRopeAtSourceMarkers, DeleteLinks, RemoveAnySourceMarker, RecordVoiceInstancesAtRoot, TalksBubbleFormat,
new: GetVoiceViewerNumber, ValidateSourceMarker, MapCharProps
Polle Zellweger (PTZ) July 10, 1987 2:02:35 pm PDT
Handling cases where voiceViewerInfo.parentViewer=NIL inside ValidateSourceMarker: reduce cases by monotonically increasing voice viewer numbers (VoiceViewersImpl) and by care under EditVoiceProc. Also make voiceViewerInfoList private to VoiceViewersImpl.
changes to: DIRECTORY, EditVoiceProc, AddVoiceWindowProps (local of EditVoiceProc), DestroyViewerEvent, SeverLinks (local of TrackDeletes), TrackDeletes, DeleteLinks, AppendViewerInfo, ValidateSourceMarker
Polle Zellweger (PTZ) July 17, 1987 6:12:25 pm PDT
Move (w/changes) CancelProc to VoiceRecordImpl - part of progress/connection mgmt cleanup.
changes to: DIRECTORY, CancelProc
Polle Zellweger (PTZ) July 21, 1987 12:14:36 pm PDT
changes to: DIRECTORY