DIRECTORY Commander USING [CommandProc, Register], CommandTool USING [ArgumentVector, Parse, Failed], Convert USING [RopeFromInt, IntFromRope], Imager USING [Context, Move, ShowChar, SetStrokeWidth, SetStrokeJoint, MaskStroke, SetXY, ShowRope, SetFont, black, SetColor], ImagerFont USING [Extents, Find, Font, Escapement], ImagerPath USING [PathProc], Jukebox USING [bytesPerChirp], MBQueue USING [CreateMenuEntry], Menus USING [MenuProc, MenuEntry, InsertMenuEntry, GetLine, SetLine, ChangeNumberOfLines], MessageWindow USING [Append, Blink], NodeStyle USING [Ref], NodeStyleOps USING [OfStyle], Real USING [Fix], Rope USING [ROPE, Length, Substr, Concat, Fetch, Cat, Find], TEditFormat USING [CharacterArtwork, CharacterArtworkRep, CharacterArtworkClass, CharacterArtworkClassRep, RegisterCharacterArtwork, GetFont], TextEdit USING [CharSet, FetchChar, GetCharProp, PutCharProp], TextNode USING [Location, Ref], TiogaButtons USING [TextNodeRef], TiogaOps USING [GetRope, GetSelection, FirstChild, ViewerDoc, CallWithLocks, SaveSelA, RestoreSelA], TiogaOpsDefs USING [Location, Ref], TiogaVoicePrivate USING [ CancelPlayBack, ChangeMenu, GetVoiceLock, IntPair, LastSilenceInSoundList, PlaySlabSection, RecordInPlaceOfSelection, RedrawViewer, ReplaceSelectionWithSavedInterval, Selection, SelectionRec, SetVoiceViewerEditStatus, SoundChars, SoundInterval, soundRopeCharLength, StopRecording, TextMarkEntry, TextMarkRec, thrushHandle, voiceButtonQueue, voiceCharDescent, voiceCharHeight, voiceCharSet, voiceCharWidth, VoiceViewerInfo, voiceViewerInfoList, voiceViewerMenu ], Vector2 USING [VEC], ViewerClasses USING [Viewer], ViewerOps USING [FetchProp, PaintViewer], VoiceRope USING [Stop, VoiceRopeInterval] ; VoiceMarkersImpl: CEDAR PROGRAM IMPORTS Commander, CommandTool, Convert, Imager, ImagerFont, MBQueue, Menus, MessageWindow, Real, Rope, TEditFormat, TextEdit, TiogaButtons, TiogaOps, TiogaVoicePrivate, ViewerOps, VoiceRope EXPORTS TiogaVoicePrivate = { AddCharMark: PUBLIC Menus.MenuProc = { viewer: ViewerClasses.Viewer; start, end: TiogaOpsDefs.Location; pendingDelete, caretBefore: BOOLEAN; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo; [viewer: viewer, start: start, end: end, pendingDelete: pendingDelete, caretBefore: caretBefore] _ TiogaOps.GetSelection[]; viewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF viewerInfo = NIL THEN { MessageWindow.Append["Make a selection in a voice viewer first", TRUE]; MessageWindow.Blink[]; RETURN }; IF TiogaVoicePrivate.GetVoiceLock[viewerInfo] THEN { IF ~ (start.node = end.node AND start.node = TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]) THEN ERROR; IF pendingDelete OR caretBefore THEN MarkChar[viewerInfo, start.where]; IF pendingDelete OR ~caretBefore THEN MarkChar[viewerInfo, end.where]; { trueContents: Rope.ROPE _ TiogaVoicePrivate.SoundChars[viewerInfo].soundRope; [] _ TiogaVoicePrivate.RedrawViewer[viewer, trueContents, 0, 0, 0, viewerInfo.remnant, FALSE, unAltered]; TiogaVoicePrivate.SetVoiceViewerEditStatus[viewer]; viewerInfo.editInProgress _ FALSE } } }; MarkChar: PROC [ viewerInfo: TiogaVoicePrivate.VoiceViewerInfo, position: INT] = { restOfList: LIST OF INT; IF viewerInfo.charMarkList = NIL THEN viewerInfo.charMarkList _ CONS [position, NIL] ELSE { IF viewerInfo.charMarkList.first >= position THEN { IF viewerInfo.charMarkList.first > position THEN viewerInfo.charMarkList _ CONS [position, viewerInfo.charMarkList] } ELSE { FOR restOfList _ viewerInfo.charMarkList, restOfList.rest WHILE restOfList.rest # NIL DO IF restOfList.rest.first >= position THEN { IF restOfList.rest.first > position THEN restOfList.rest _ CONS [position, restOfList.rest]; RETURN } ENDLOOP; restOfList.rest _ CONS [position, NIL] } } }; LockedAddCharMark: PUBLIC PROC [viewer: ViewerClasses.Viewer, position: INT] = { viewerInfo: TiogaVoicePrivate.VoiceViewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF viewerInfo = NIL THEN ERROR; MarkChar[viewerInfo, position]; { trueContents: Rope.ROPE _ TiogaVoicePrivate.SoundChars[viewerInfo].soundRope; [] _ TiogaVoicePrivate.RedrawViewer[viewer, trueContents, 0, 0, 0, viewerInfo.remnant, FALSE, unAltered]; TiogaVoicePrivate.SetVoiceViewerEditStatus[viewer]; } }; DeleteCharMarks: PUBLIC Menus.MenuProc = { viewer: ViewerClasses.Viewer; start, end: TiogaOpsDefs.Location; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo; RemoveMarkChars: PROC [from, to: INT] = { WHILE viewerInfo.charMarkList # NIL AND viewerInfo.charMarkList.first IN [from..to] DO viewerInfo.charMarkList _ viewerInfo.charMarkList.rest ENDLOOP; IF viewerInfo.charMarkList # NIL THEN FOR l: LIST OF INT _ viewerInfo.charMarkList, l.rest WHILE l # NIL AND l.rest # NIL DO IF l.rest.first IN [from..to] THEN l.rest _ l.rest.rest ENDLOOP -- the dual WHILE condition is because we can delete the next entry and it could be the last }; IF mouseButton = blue THEN -- right click means delete all marks in the viewer { viewer _ NARROW[parent, ViewerClasses.Viewer]; viewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF TiogaVoicePrivate.GetVoiceLock[viewerInfo] THEN { viewerInfo.charMarkList _ NIL; { trueContents: Rope.ROPE _ TiogaVoicePrivate.SoundChars[viewerInfo].soundRope; [] _ TiogaVoicePrivate.RedrawViewer[viewer, trueContents, 0, 0, 0, viewerInfo.remnant, FALSE, unAltered]; TiogaVoicePrivate.SetVoiceViewerEditStatus[viewer]; viewerInfo.editInProgress _ FALSE } } } ELSE -- both other clicks mean delete all marks from selection { [viewer: viewer, start: start, end: end] _ TiogaOps.GetSelection[]; viewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF viewerInfo = NIL THEN { MessageWindow.Append["Make a selection in a voice viewer first", TRUE]; MessageWindow.Blink[]; RETURN }; IF TiogaVoicePrivate.GetVoiceLock[viewerInfo] THEN { IF ~ (start.node = end.node AND start.node = TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]) THEN ERROR; RemoveMarkChars[start.where, end.where]; { trueContents: Rope.ROPE _ TiogaVoicePrivate.SoundChars[viewerInfo].soundRope; [] _ TiogaVoicePrivate.RedrawViewer[viewer, trueContents, 0, 0, 0, viewerInfo.remnant, FALSE, unAltered]; TiogaVoicePrivate.SetVoiceViewerEditStatus[viewer]; viewerInfo.editInProgress _ FALSE } } } }; DisplayCharMarks: PUBLIC PROC [ unMarkedRope: Rope.ROPE, charMarkList: LIST OF INT, skipChars: INT] RETURNS [markedRope: Rope.ROPE] = { endChar: INT _ unMarkedRope.Length - 1; lastMark: INT _ -1; nextMark: INT; markedRope _ NIL; WHILE charMarkList # NIL AND charMarkList.first < skipChars DO charMarkList _ charMarkList.rest ENDLOOP; DO nextMark _ IF charMarkList = NIL THEN endChar + 1 ELSE charMarkList.first - skipChars; markedRope _ markedRope.Concat[unMarkedRope.Substr[lastMark+1, nextMark-lastMark-1]]; IF nextMark = endChar + 1 THEN RETURN; markedRope _ markedRope.Concat["@"]; -- the marker character lastMark _ nextMark; charMarkList _ charMarkList.rest ENDLOOP }; ExtractCharMarks: PUBLIC PROC [ viewerInfo: TiogaVoicePrivate.VoiceViewerInfo, soundInterval: TiogaVoicePrivate.SoundInterval] = { currLastInList: LIST OF INT; soughtStart: INT _ soundInterval.ropeInterval.start/TiogaVoicePrivate.soundRopeCharLength; soughtEnd: INT _ (soundInterval.ropeInterval.start + soundInterval.ropeInterval.length)/TiogaVoicePrivate.soundRopeCharLength; soundInterval.charMarkList _ NIL; -- should be already so, but . . . FOR l: LIST OF INT _ viewerInfo.charMarkList, l.rest WHILE l # NIL AND l.first < soughtEnd DO IF l.first >= soughtStart THEN { IF soundInterval.charMarkList = NIL THEN { soundInterval.charMarkList _ CONS [l.first - soughtStart, NIL]; currLastInList _ soundInterval.charMarkList; } ELSE { currLastInList.rest _ CONS [l.first - soughtStart, NIL]; currLastInList _ currLastInList.rest } } ENDLOOP }; EditCharMarks: PUBLIC PROC [ viewerInfo: TiogaVoicePrivate.VoiceViewerInfo, unchangedHead, deleteChars, insertChars: INT, soundInterval: TiogaVoicePrivate.SoundInterval] = { atHead: BOOLEAN _ TRUE; l: LIST OF INT; WHILE atHead AND viewerInfo.charMarkList # NIL DO SELECT viewerInfo.charMarkList.first FROM >= unchangedHead+deleteChars => {viewerInfo.charMarkList.first _ viewerInfo.charMarkList.first + insertChars - deleteChars; atHead _ FALSE}; >= unchangedHead => viewerInfo.charMarkList _ viewerInfo.charMarkList.rest; ENDCASE => atHead _ FALSE; ENDLOOP; IF viewerInfo.charMarkList # NIL THEN { l _ viewerInfo.charMarkList; WHILE l # NIL AND l.rest # NIL DO --the dual WHILE condition is because we can delete the next entry and it could be the last SELECT l.rest.first FROM >= unchangedHead+deleteChars => {l.rest.first _ l.rest.first + insertChars - deleteChars; l _ l.rest}; >= unchangedHead => l.rest _ l.rest.rest; ENDCASE => l _ l.rest ENDLOOP }; IF soundInterval # NIL AND soundInterval.charMarkList # NIL THEN -- now put the character marks from the soundInterval into the list at the appropriate point { beforeInsert, afterInsert: LIST OF INT _ NIL; newSection, endNewSection: LIST OF INT; IF viewerInfo.charMarkList # NIL THEN { IF viewerInfo.charMarkList.first >= unchangedHead THEN afterInsert _ viewerInfo.charMarkList ELSE { l _ viewerInfo.charMarkList; WHILE l.rest # NIL AND l.rest.first < unchangedHead DO l _ l.rest ENDLOOP; beforeInsert _ l; afterInsert _ l.rest } }; { insertMarks: LIST OF INT _ soundInterval.charMarkList.rest; newSection _ CONS [soundInterval.charMarkList.first + unchangedHead, NIL]; endNewSection _ newSection; WHILE insertMarks # NIL DO endNewSection.rest _ CONS [soundInterval.charMarkList.first + unchangedHead, NIL]; endNewSection _ endNewSection.rest; insertMarks _ insertMarks.rest ENDLOOP }; endNewSection.rest _ afterInsert; IF beforeInsert = NIL THEN viewerInfo.charMarkList _ newSection ELSE beforeInsert.rest _ newSection } }; TextInput: PUBLIC PROC [viewer: ViewerClasses.Viewer, input: Rope.ROPE] = { node: TextNode.Ref; textEntry: TiogaVoicePrivate.TextMarkEntry; AddArtwork: PROC [root: TiogaOpsDefs.Ref] = { TextEdit.PutCharProp[node, textEntry.position, $Artwork, NARROW["VoiceMarker", Rope.ROPE]]; -- the NARROW prevents a REF TEXT being created TextEdit.PutCharProp[node, textEntry.position, $VoiceMark, textEntry.text.Substr[0, textEntry.displayChars]]; }; selectionViewer: ViewerClasses.Viewer; start, end, char: TiogaOpsDefs.Location; insertPosition: INT; caretBefore: BOOLEAN; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo; charsAlreadyDisplayed: INT _ 0; redraw: BOOLEAN _ FALSE; [viewer: selectionViewer, start: start, end: end, caretBefore: caretBefore] _ TiogaOps.GetSelection[]; IF selectionViewer # viewer THEN ERROR; viewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF ~TiogaVoicePrivate.GetVoiceLock[viewerInfo] THEN RETURN; char _ IF caretBefore THEN start ELSE end; node _ TiogaButtons.TextNodeRef[char.node]; insertPosition _ char.where; IF viewerInfo.textMarkList = NIL OR viewerInfo.textMarkList.first.position > insertPosition THEN { textEntry _ NEW [TiogaVoicePrivate.TextMarkRec _ [insertPosition, input, 0, 0]]; viewerInfo.textMarkList _ CONS [textEntry, viewerInfo.textMarkList]; LimitText[textEntry, IF viewerInfo.textMarkList.rest = NIL THEN LAST[INT] ELSE viewerInfo.textMarkList.rest.first.position - insertPosition]; TiogaOps.CallWithLocks[AddArtwork]; redraw _ TRUE } ELSE { IF viewerInfo.textMarkList.first.position = insertPosition THEN { textEntry _ viewerInfo.textMarkList.first; charsAlreadyDisplayed _ textEntry.displayChars; textEntry.text _ textEntry.text.Concat[input]; LimitText[textEntry, IF viewerInfo.textMarkList.rest = NIL THEN LAST[INT] ELSE viewerInfo.textMarkList.rest.first.position - insertPosition]; IF textEntry.displayChars # charsAlreadyDisplayed THEN {TiogaOps.CallWithLocks[AddArtwork]; redraw _ TRUE} } ELSE { l: LIST OF TiogaVoicePrivate.TextMarkEntry _ viewerInfo.textMarkList; WHILE l.rest # NIL AND l.rest.first.position < insertPosition DO l _ l.rest ENDLOOP; IF l.rest = NIL OR l.rest.first.position > insertPosition THEN -- insert a new entry: previous entry may need trimming { oldWidth: INT _ l.first.width; textEntry _ l.first; LimitText[textEntry, insertPosition - textEntry.position]; IF textEntry.width # oldWidth THEN {TiogaOps.CallWithLocks[AddArtwork]; redraw _ TRUE}; textEntry _ NEW [TiogaVoicePrivate.TextMarkRec _ [insertPosition, input, 0, 0]]; l.rest _ CONS [textEntry, l.rest] } ELSE -- appending text to an entry already there { textEntry _ l.rest.first; textEntry.text _ textEntry.text.Concat[input]; charsAlreadyDisplayed _ textEntry.displayChars }; l _ l.rest; -- point at the new/modified entry LimitText[textEntry, IF l.rest = NIL THEN LAST[INT] ELSE l.rest.first.position - insertPosition]; IF textEntry.displayChars # charsAlreadyDisplayed THEN {TiogaOps.CallWithLocks[AddArtwork]; redraw _ TRUE} } }; IF redraw THEN ViewerOps.PaintViewer[viewer: viewer, hint: client, clearClient: TRUE, whatChanged: NIL]; viewerInfo.edited _ TRUE; IF ~redraw THEN TiogaVoicePrivate.SetVoiceViewerEditStatus[viewer]; -- not needed if the redraw was performed, as viewer's displayed status will be `edited' already viewerInfo.editInProgress _ FALSE }; LimitText: PROC [entry: TiogaVoicePrivate.TextMarkEntry, maxLength: INT] = { maxEscapement: REAL _ REAL[maxLength]*TiogaVoicePrivate.voiceCharWidth; textLength: INT _ entry.text.Length; currPos: INT _ 0; currEscapement: REAL _ 0.0; WHILE currPos < textLength DO newEscapement: REAL _ currEscapement + ImagerFont.Escapement[voiceMarkerFont, [TiogaVoicePrivate.voiceCharSet, entry.text.Fetch[currPos]-'\000]].x; IF newEscapement > maxEscapement THEN EXIT; currPos _ currPos + 1; currEscapement _ newEscapement ENDLOOP; entry.width _ Real.Fix[(currEscapement+TiogaVoicePrivate.voiceCharWidth-1.0)/ TiogaVoicePrivate.voiceCharWidth]; entry.displayChars _ currPos; }; BackSpace: PUBLIC PROC [viewer: ViewerClasses.Viewer] = { node: TextNode.Ref; textEntry: TiogaVoicePrivate.TextMarkEntry; AddArtwork: PROC [root: TiogaOpsDefs.Ref] = { TextEdit.PutCharProp[node, textEntry.position, $Artwork, NARROW["VoiceMarker", Rope.ROPE]]; -- the NARROW prevents a REF TEXT being created TextEdit.PutCharProp[node, textEntry.position, $VoiceMark, textEntry.text.Substr[0, textEntry.displayChars]]; }; selectionViewer: ViewerClasses.Viewer; start, end, char: TiogaOpsDefs.Location; deletePosition, charsToNextPosition: INT; caretBefore: BOOLEAN; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo; [viewer: selectionViewer, start: start, end: end, caretBefore: caretBefore] _ TiogaOps.GetSelection[]; IF selectionViewer # viewer THEN ERROR; viewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF ~TiogaVoicePrivate.GetVoiceLock[viewerInfo] THEN RETURN; char _ IF caretBefore THEN start ELSE end; node _ TiogaButtons.TextNodeRef[char.node]; deletePosition _ char.where; textEntry _ NIL; FOR l: LIST OF TiogaVoicePrivate.TextMarkEntry _ viewerInfo.textMarkList, l.rest WHILE l # NIL DO IF l.first.position >= deletePosition THEN { IF l.first.position = deletePosition THEN { textEntry _ l.first; charsToNextPosition _ IF l.rest = NIL THEN LAST[INT] ELSE l.rest.first.position - deletePosition }; EXIT } ENDLOOP; IF textEntry = NIL THEN MessageWindow.Append["No textual annotation at point of selection", TRUE] ELSE { charsOnDisplay: INT _ textEntry.displayChars; textEntry.text _ textEntry.text.Substr[0, textEntry.text.Length -1]; LimitText[textEntry, charsToNextPosition]; viewerInfo.edited _ TRUE; IF charsOnDisplay # textEntry.displayChars THEN { TiogaOps.CallWithLocks[AddArtwork]; ViewerOps.PaintViewer[viewer: viewer, hint: client, clearClient: TRUE, whatChanged: NIL] } ELSE TiogaVoicePrivate.SetVoiceViewerEditStatus[selectionViewer] }; viewerInfo.editInProgress _ FALSE }; BackWord: PUBLIC PROC [viewer: ViewerClasses.Viewer] = { node: TextNode.Ref; textEntry: TiogaVoicePrivate.TextMarkEntry; AddArtwork: PROC [root: TiogaOpsDefs.Ref] = { TextEdit.PutCharProp[node, textEntry.position, $Artwork, IF textEntry.text = NIL THEN NIL ELSE NARROW["VoiceMarker", Rope.ROPE]]; -- the NARROW prevents a REF TEXT being created TextEdit.PutCharProp[node, textEntry.position, $VoiceMark, IF textEntry.text = NIL THEN NIL ELSE textEntry.text.Substr[0, textEntry.displayChars]]; }; selectionViewer: ViewerClasses.Viewer; start, end: TiogaOpsDefs.Location; textRemoved: BOOLEAN _ FALSE; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo; [viewer: selectionViewer, start: start, end: end] _ TiogaOps.GetSelection[]; IF selectionViewer # viewer THEN ERROR; IF start.node # end.node THEN ERROR; viewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; node _ TiogaButtons.TextNodeRef[start.node]; IF ~TiogaVoicePrivate.GetVoiceLock[viewerInfo] THEN RETURN; IF viewerInfo.textMarkList = NIL THEN { viewerInfo.editInProgress _ FALSE; RETURN }; IF viewerInfo.textMarkList.first.position >= start.where THEN -- any text to removed is at the head of the list WHILE viewerInfo.textMarkList # NIL AND viewerInfo.textMarkList.first.position <= end.where DO textEntry _ viewerInfo.textMarkList.first; textEntry.text _ NIL; TiogaOps.CallWithLocks[AddArtwork]; textRemoved _ TRUE; viewerInfo.textMarkList _ viewerInfo.textMarkList.rest ENDLOOP ELSE { l: LIST OF TiogaVoicePrivate.TextMarkEntry _ viewerInfo.textMarkList; WHILE l.rest # NIL AND l.rest.first.position < start.where DO l _ l.rest ENDLOOP; WHILE l.rest # NIL AND l.rest.first.position <= end.where DO textEntry _ l.rest.first; textEntry.text _ NIL; TiogaOps.CallWithLocks[AddArtwork]; textRemoved _ TRUE; l.rest _ l.rest.rest ENDLOOP; IF textRemoved THEN { currChars: INT; textEntry _ l.first; currChars _ textEntry.displayChars; LimitText[textEntry, IF l.rest = NIL THEN LAST[INT] ELSE l.rest.first.position - textEntry.position]; IF currChars # textEntry.displayChars THEN TiogaOps.CallWithLocks[AddArtwork] } }; IF textRemoved THEN { viewerInfo.edited _ TRUE; ViewerOps.PaintViewer[viewer: viewer, hint: client, clearClient: TRUE, whatChanged: NIL] }; viewerInfo.editInProgress _ FALSE }; RedrawTextMarkers: PUBLIC PROC [ viewer: ViewerClasses.Viewer, voiceCharNode: TiogaOpsDefs.Ref] = { AddArtwork: PROC [root: TiogaOpsDefs.Ref] = { textEntry: TiogaVoicePrivate.TextMarkEntry _ l.first; TextEdit.PutCharProp[node, textEntry.position, $Artwork, NARROW["VoiceMarker", Rope.ROPE]]; -- the NARROW prevents a REF TEXT being created TextEdit.PutCharProp[node, textEntry.position, $VoiceMark, textEntry.text.Substr[0, textEntry.displayChars]]; }; node: TextNode.Ref _ TiogaButtons.TextNodeRef[voiceCharNode]; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; l: LIST OF TiogaVoicePrivate.TextMarkEntry; FOR l _ viewerInfo.textMarkList, l.rest WHILE l # NIL DO TiogaOps.CallWithLocks[AddArtwork] ENDLOOP }; ExtractTextMarks: PUBLIC PROC [ viewerInfo: TiogaVoicePrivate.VoiceViewerInfo, soundInterval: TiogaVoicePrivate.SoundInterval] = { currLastInList: LIST OF TiogaVoicePrivate.TextMarkEntry; soughtStart: INT _ soundInterval.ropeInterval.start/TiogaVoicePrivate.soundRopeCharLength; soughtEnd: INT _ (soundInterval.ropeInterval.start + soundInterval.ropeInterval.length)/TiogaVoicePrivate.soundRopeCharLength; soundInterval.textMarkList _ NIL; -- should be already so, but . . . FOR l: LIST OF TiogaVoicePrivate.TextMarkEntry _ viewerInfo.textMarkList, l.rest WHILE l # NIL AND l.first.position < soughtEnd DO IF l.first.position >= soughtStart THEN { newEntry: TiogaVoicePrivate.TextMarkEntry _ NEW [TiogaVoicePrivate.TextMarkRec _ l.first^]; newEntry.position _ newEntry.position - soughtStart; IF soundInterval.textMarkList = NIL THEN { soundInterval.textMarkList _ CONS [newEntry, NIL]; currLastInList _ soundInterval.textMarkList; } ELSE { currLastInList.rest _ CONS [newEntry, NIL]; currLastInList _ currLastInList.rest } } ENDLOOP }; EditTextMarks: PUBLIC PROC [ viewerInfo: TiogaVoicePrivate.VoiceViewerInfo, unchangedHead, deleteChars, insertChars: INT, soundInterval: TiogaVoicePrivate.SoundInterval] = { atHead: BOOLEAN _ TRUE; l: LIST OF TiogaVoicePrivate.TextMarkEntry; WHILE atHead AND viewerInfo.textMarkList # NIL DO SELECT viewerInfo.textMarkList.first.position FROM >= unchangedHead+deleteChars => {viewerInfo.textMarkList.first.position _ viewerInfo.textMarkList.first.position + insertChars - deleteChars; atHead _ FALSE}; >= unchangedHead => viewerInfo.textMarkList _ viewerInfo.textMarkList.rest; ENDCASE => atHead _ FALSE; ENDLOOP; IF viewerInfo.textMarkList # NIL THEN { l _ viewerInfo.textMarkList; WHILE l # NIL AND l.rest # NIL DO --the dual WHILE condition is because we can delete the next entry and it could be the last SELECT l.rest.first.position FROM >= unchangedHead+deleteChars => {l.rest.first.position _ l.rest.first.position + insertChars - deleteChars; l _ l.rest}; >= unchangedHead => l.rest _ l.rest.rest; ENDCASE => l _ l.rest ENDLOOP }; IF soundInterval # NIL AND soundInterval.textMarkList # NIL THEN -- now put the textual marks from the soundInterval into the list at the appropriate point { beforeInsert, afterInsert: LIST OF TiogaVoicePrivate.TextMarkEntry _ NIL; newSection, endNewSection: LIST OF TiogaVoicePrivate.TextMarkEntry; IF viewerInfo.textMarkList # NIL THEN { IF viewerInfo.textMarkList.first.position >= unchangedHead THEN afterInsert _ viewerInfo.textMarkList ELSE { l _ viewerInfo.textMarkList; WHILE l.rest # NIL AND l.rest.first.position < unchangedHead DO l _ l.rest ENDLOOP; beforeInsert _ l; afterInsert _ l.rest } }; { insertMarks: LIST OF TiogaVoicePrivate.TextMarkEntry _ soundInterval.textMarkList.rest; newSection _ CONS [NEW [TiogaVoicePrivate.TextMarkRec _ soundInterval.textMarkList.first^], NIL]; newSection.first.position _ newSection.first.position + unchangedHead; endNewSection _ newSection; WHILE insertMarks # NIL DO endNewSection.rest _ CONS [NEW [TiogaVoicePrivate.TextMarkRec _ soundInterval.textMarkList.first^], NIL]; endNewSection _ endNewSection.rest; insertMarks _ insertMarks.rest; endNewSection.first.position _ endNewSection.first.position + unchangedHead ENDLOOP }; endNewSection.rest _ afterInsert; LimitText[endNewSection.first, IF endNewSection.rest = NIL THEN LAST[INT] ELSE endNewSection.rest.first.position - endNewSection.first.position]; IF beforeInsert = NIL THEN viewerInfo.textMarkList _ newSection ELSE { beforeInsert.rest _ newSection; LimitText[beforeInsert.first, newSection.first.position - beforeInsert.first.position] } } ELSE -- no new entries to be inserted, but we must LimitText on the entry previous to the cut {IF viewerInfo.textMarkList # NIL AND viewerInfo.textMarkList.first.position < unchangedHead THEN { beforeCut: LIST OF TiogaVoicePrivate.TextMarkEntry _ viewerInfo.textMarkList; WHILE beforeCut.rest # NIL AND beforeCut.rest.first.position < unchangedHead DO beforeCut _ beforeCut.rest ENDLOOP; LimitText[beforeCut.first, IF beforeCut.rest = NIL THEN LAST[INT] ELSE beforeCut.rest.first.position - beforeCut.first.position] } } }; RopeFromTextList: PUBLIC PROC [ l: LIST OF TiogaVoicePrivate.TextMarkEntry] RETURNS [r: Rope.ROPE _ NIL] = { WHILE l # NIL DO textEntry: Rope.ROPE _ l.first.text; positionInSamples: INT _ l.first.position*TiogaVoicePrivate.soundRopeCharLength; r _ r.Cat[Convert.RopeFromInt[positionInSamples], ":", Convert.RopeFromInt[textEntry.Length], ":"]; r _ r.Concat[textEntry]; l _ l.rest ENDLOOP }; TextListFromRope: PUBLIC PROC [r: Rope.ROPE] RETURNS [l: LIST OF TiogaVoicePrivate.TextMarkEntry _ NIL] = { endOfList: LIST OF TiogaVoicePrivate.TextMarkEntry; WHILE r.Length > 0 DO nextColon: INT _ r.Find[":"]; newEntry: TiogaVoicePrivate.TextMarkEntry _ NEW [TiogaVoicePrivate.TextMarkRec _ [Convert.IntFromRope[r.Substr[0, nextColon]]/TiogaVoicePrivate.soundRopeCharLength, NIL, 0, 0]]; textLength: INT; r _ r.Substr[nextColon+1]; nextColon _ r.Find[":"]; textLength _ Convert.IntFromRope[r.Substr[0, nextColon]]; r _ r.Substr[nextColon+1]; newEntry.text _ r.Substr[0, textLength]; r _ r.Substr[textLength]; IF l = NIL THEN { l _ CONS [newEntry, NIL]; endOfList _ l } ELSE { endOfList.rest _ CONS [newEntry, NIL]; endOfList _ endOfList.rest } ENDLOOP; FOR list: LIST OF TiogaVoicePrivate.TextMarkEntry _ l, list.rest WHILE list # NIL DO LimitText[list.first, IF list.rest = NIL THEN LAST[INT] ELSE list.rest.first.position - list.first.position] ENDLOOP }; voiceMarkerFont: PUBLIC ImagerFont.Font _ ImagerFont.Find["xerox/tiogafonts/tioga10"]; VoiceMarkerDataRep: TYPE ~ RECORD [ letter: CHAR, label: Rope.ROPE, charHeight: REAL, charWidth: REAL ]; VoiceMarkerPaint: PROC [ self: TEditFormat.CharacterArtwork, context: Imager.Context] ~ { data: REF VoiceMarkerDataRep ~ NARROW[self.data]; DrawArrow: ImagerPath.PathProc = { moveTo[[0.0, 2.0*data.charHeight]]; lineTo[[0.0, data.charHeight]]; lineTo[[3.0, 4.0*data.charHeight/3.0]]; lineTo[[-2.0, 4.0*data.charHeight/3.0]]; lineTo[[0.0, data.charHeight]] }; Imager.Move[context]; Imager.ShowChar[context, data.letter]; Imager.SetColor[context, Imager.black]; Imager.SetStrokeWidth[context, 1.0]; Imager.SetStrokeJoint[context, round]; Imager.MaskStroke[context, DrawArrow, TRUE]; Imager.SetXY[context, [2.0, 3.0*data.charHeight/2.0]]; Imager.SetFont[context, voiceMarkerFont]; Imager.ShowRope[context, data.label]; }; VoiceMarkerFormat: PROC [ class: TEditFormat.CharacterArtworkClass, loc: TextNode.Location, style: NodeStyle.Ref, kind: NodeStyleOps.OfStyle] RETURNS [TEditFormat.CharacterArtwork] ~ { voiceCharSet: TextEdit.CharSet _ TextEdit.FetchChar[loc.node, loc.where].charSet; letter: CHAR _ TextEdit.FetchChar[loc.node, loc.where].char; label: Rope.ROPE _ NARROW[TextEdit.GetCharProp[loc.node, loc.where, $VoiceMark], Rope.ROPE]; escapement: Vector2.VEC _ ImagerFont.Escapement[TEditFormat.GetFont[style], [set: TiogaVoicePrivate.voiceCharSet, code: letter-'\000]]; charWidth: REAL _ escapement.x; data: REF VoiceMarkerDataRep ~ NEW[VoiceMarkerDataRep _ [ letter: letter, label: label, charHeight: TiogaVoicePrivate.voiceCharHeight, charWidth: charWidth ]]; extents: ImagerFont.Extents _ [leftExtent: 0.0, rightExtent: 20.0, ascent: 5.0*data.charHeight/2.0, descent: TiogaVoicePrivate.voiceCharDescent]; RETURN [NEW[TEditFormat.CharacterArtworkRep _ [paint: VoiceMarkerPaint, extents: extents, escapement: escapement, data: data]]] }; talksBubbleClass: TEditFormat.CharacterArtworkClass ~ NEW[TEditFormat.CharacterArtworkClassRep _ [ name: $VoiceMarker, format: VoiceMarkerFormat, data: NIL ]]; oldest: PUBLIC INT _ 5; youngest: PUBLIC INT _ 1; ageColors: ARRAY [1..6] OF Rope.ROPE; ReColorViewer: PUBLIC PROC [ viewer: ViewerClasses.Viewer, voiceViewerInfo: TiogaVoicePrivate.VoiceViewerInfo _ NIL, repaint: BOOLEAN _ FALSE] = { node: TextNode.Ref _ TiogaButtons.TextNodeRef[TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]]; nodeLength: INT _ TiogaOps.GetRope[TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]].Length; IF voiceViewerInfo = NIL THEN voiceViewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; voiceViewerInfo.color _ viewer.column = color; IF voiceViewerInfo.ageList = NIL OR ~voiceViewerInfo.color THEN RETURN; TextEdit.PutCharProp[node, 0, $Postfix, NIL, nodeLength]; -- may not be necessary FOR l: LIST OF TiogaVoicePrivate.IntPair _ voiceViewerInfo.ageList, l.rest WHILE l # NIL DO TextEdit.PutCharProp[node, l.first.position, $Postfix, ageColors[l.first.age], IF l.rest # NIL THEN MIN[l.rest.first.position, nodeLength]-l.first.position ELSE nodeLength-l.first.position] ENDLOOP; TiogaVoicePrivate.SetVoiceViewerEditStatus[viewer]; ViewerOps.PaintViewer[viewer: viewer, hint: client, clearClient: TRUE, whatChanged: NIL] }; AgeAllViewers: PUBLIC PROC [youngestViewer: ViewerClasses.Viewer] = { FOR currInfo: TiogaVoicePrivate.VoiceViewerInfo _ TiogaVoicePrivate.voiceViewerInfoList, currInfo.nextInfoRec WHILE currInfo # NIL DO l: LIST OF TiogaVoicePrivate.IntPair _ currInfo.ageList; IF l # NIL THEN IF ~(l.rest = NIL AND l.first.age = oldest+1) THEN { IF (l.rest = NIL AND l.first.age = oldest) THEN { ReColorViewer[currInfo.viewer, currInfo, currInfo.viewer # youngestViewer]; l.first.age _ oldest+1 -- special treatment because this list may be the result of having cut out some newer voice in the most recent edit, in which case the colors need redrawing } ELSE { --in all other cases the ages are only allowed to be incremented to oldest WHILE l # NIL DO l.first.age _ MIN[l.first.age+1, oldest]; l _ l.rest ENDLOOP; l _ currInfo.ageList; WHILE l # NIL AND l.rest # NIL DO --the dual WHILE condition is because we can delete the next entry and it could be the last IF l.first.age = l.rest.first.age THEN l.rest _ l.rest.rest ELSE l _ l.rest ENDLOOP; ReColorViewer[currInfo.viewer, currInfo] } } ENDLOOP }; EditAges: PUBLIC PROC [ viewerInfo: TiogaVoicePrivate.VoiceViewerInfo, unchangedHead, deleteChars, insertChars: INT] = { ageAtEndOfCut: INT; atHead: BOOLEAN _ TRUE; l: LIST OF TiogaVoicePrivate.IntPair _ viewerInfo.ageList; IF l = NIL THEN ERROR; WHILE l.rest # NIL AND l.rest.first.position < unchangedHead + deleteChars DO l _ l.rest ENDLOOP; ageAtEndOfCut _ l.first.age; WHILE atHead AND viewerInfo.ageList # NIL DO SELECT viewerInfo.ageList.first.position FROM >= unchangedHead+deleteChars => {viewerInfo.ageList.first.position _ viewerInfo.ageList.first.position + insertChars - deleteChars; atHead _ FALSE}; >= unchangedHead => viewerInfo.ageList _ viewerInfo.ageList.rest; ENDCASE => atHead _ FALSE; ENDLOOP; IF viewerInfo.ageList # NIL THEN { l _ viewerInfo.ageList; WHILE l # NIL AND l.rest # NIL DO --the dual WHILE condition is because we can delete the next entry and it could be the last SELECT l.rest.first.position FROM >= unchangedHead+deleteChars => {l.rest.first.position _ l.rest.first.position + insertChars - deleteChars; l _ l.rest}; >= unchangedHead => l.rest _ l.rest.rest; ENDCASE => l _ l.rest ENDLOOP }; IF viewerInfo.ageList = NIL OR viewerInfo.ageList.first.position >= unchangedHead + insertChars THEN { IF viewerInfo.ageList = NIL OR viewerInfo.ageList.first.position # unchangedHead + insertChars THEN viewerInfo.ageList _ CONS [[unchangedHead + insertChars, ageAtEndOfCut], viewerInfo.ageList]; IF insertChars # 0 THEN viewerInfo.ageList _ CONS [[unchangedHead, youngest-1], viewerInfo.ageList] } ELSE { l _ viewerInfo.ageList; WHILE l.rest # NIL AND l.rest.first.position < unchangedHead + insertChars DO l _ l.rest ENDLOOP; IF l.rest = NIL OR l.rest.first.position # unchangedHead + insertChars THEN l.rest _ CONS [[unchangedHead + insertChars, ageAtEndOfCut], l.rest]; IF insertChars # 0 THEN l.rest _ CONS [[unchangedHead, youngest-1], l.rest] }; FOR l _ viewerInfo.ageList, l.rest WHILE l # NIL DO IF l.first.age > oldest THEN l.first.age _ oldest ENDLOOP; l _ viewerInfo.ageList; WHILE l # NIL AND l.rest # NIL DO --the dual WHILE condition is because we can delete the next entry and it could be the last IF l.first.age = l.rest.first.age THEN l.rest _ l.rest.rest ELSE l _ l.rest ENDLOOP }; DictationOps: PUBLIC Menus.MenuProc = { TiogaVoicePrivate.ChangeMenu[NARROW[parent], dictationMenu] }; ToggleDictationMenu: PUBLIC PROC [viewer: ViewerClasses.Viewer] = { TiogaVoicePrivate.ChangeMenu[viewer, dictationMenu] }; FindEnd: PROC [viewerInfo: TiogaVoicePrivate.VoiceViewerInfo] RETURNS [sample: INT] = { IF viewerInfo.ropeInterval.start # 0 THEN ERROR; IF ~viewerInfo.color THEN RETURN [viewerInfo.ropeInterval.length] ELSE { minAge: INT _ oldest+2; endOfAge: INT _ LAST[INT]; FOR l: LIST OF TiogaVoicePrivate.IntPair _ viewerInfo.ageList, l.rest WHILE l # NIL DO IF l.first.age <= minAge THEN { endOfAge _ IF l.rest=NIL THEN LAST[INT] ELSE l.rest.first.position; minAge _ l.first.age } ENDLOOP; RETURN [IF endOfAge = LAST[INT] THEN viewerInfo.ropeInterval.length ELSE endOfAge*TiogaVoicePrivate.soundRopeCharLength] } }; StillInVoiceViewerList: PROC [viewerInfo: TiogaVoicePrivate.VoiceViewerInfo] RETURNS [BOOLEAN] = { nextInfo: TiogaVoicePrivate.VoiceViewerInfo _ TiogaVoicePrivate.voiceViewerInfoList; DO IF nextInfo = NIL THEN { MessageWindow.Append["Voice viewer has been deleted (contents NIL): dictation operation therefore invalid", TRUE]; MessageWindow.Blink[]; RETURN[FALSE] }; IF nextInfo = viewerInfo THEN RETURN[TRUE]; nextInfo _ nextInfo.nextInfoRec ENDLOOP }; PlayFromSelection: Menus.MenuProc = { viewer: ViewerClasses.Viewer; start: TiogaOpsDefs.Location; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo; node: TiogaOpsDefs.Ref; from, to: INT; [viewer: viewer, start: start] _ TiogaOps.GetSelection[]; IF viewer = NIL THEN { MessageWindow.Append["Make a selection first", TRUE]; MessageWindow.Blink[]; RETURN }; viewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF viewerInfo = NIL THEN { MessageWindow.Append["Make a selection in a voice viewer first", TRUE]; MessageWindow.Blink[]; RETURN }; VoiceRope.Stop[TiogaVoicePrivate.thrushHandle]; TiogaVoicePrivate.CancelPlayBack[]; TiogaVoicePrivate.StopRecording[]; IF ~StillInVoiceViewerList[viewerInfo] THEN RETURN; node _ TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]; from _ start.where; to _ FindEnd[viewerInfo]/TiogaVoicePrivate.soundRopeCharLength; IF to >= from THEN TiogaVoicePrivate.PlaySlabSection[viewer, node, from, to] ELSE { MessageWindow.Append["Make a selection before the end of the most recent edit", TRUE]; MessageWindow.Blink[] }; }; ResumeFromSelection: Menus.MenuProc = { viewer: ViewerClasses.Viewer; start: TiogaOpsDefs.Location; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo; node: TiogaOpsDefs.Ref; replaceRopeInterval: VoiceRope.VoiceRopeInterval; startDelete, endDelete: INT; [viewer: viewer, start: start] _ TiogaOps.GetSelection[]; IF viewer = NIL THEN { MessageWindow.Append["Make a selection first", TRUE]; MessageWindow.Blink[]; RETURN }; viewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF viewerInfo = NIL THEN { MessageWindow.Append["Make a selection in a voice viewer first", TRUE]; MessageWindow.Blink[]; RETURN }; VoiceRope.Stop[TiogaVoicePrivate.thrushHandle]; TiogaVoicePrivate.CancelPlayBack[]; TiogaVoicePrivate.StopRecording[]; IF ~StillInVoiceViewerList[viewerInfo] THEN RETURN; IF ~TiogaVoicePrivate.GetVoiceLock[viewerInfo] THEN RETURN; node _ TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]; startDelete _ start.where*TiogaVoicePrivate.soundRopeCharLength; endDelete _ FindEnd[viewerInfo]; IF startDelete>endDelete THEN { MessageWindow.Append["Make a selection before the end of the most recent edit", TRUE]; MessageWindow.Blink[]; viewerInfo.editInProgress _ FALSE; RETURN }; replaceRopeInterval _ [ropeID: viewerInfo.ropeInterval.ropeID, start: startDelete, length: endDelete - startDelete]; TiogaVoicePrivate.RecordInPlaceOfSelection[NEW[TiogaVoicePrivate.SelectionRec _ [viewer: viewer, voiceViewerInfo: viewerInfo, ropeInterval: replaceRopeInterval, displayNode: node]]] }; ResumeFromEnd: Menus.MenuProc = { viewer: ViewerClasses.Viewer _ NARROW[parent, ViewerClasses.Viewer]; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; node: TiogaOpsDefs.Ref; replaceRopeInterval: VoiceRope.VoiceRopeInterval; VoiceRope.Stop[TiogaVoicePrivate.thrushHandle]; TiogaVoicePrivate.CancelPlayBack[]; TiogaVoicePrivate.StopRecording[]; IF ~StillInVoiceViewerList[viewerInfo] THEN RETURN; IF ~TiogaVoicePrivate.GetVoiceLock[viewerInfo] THEN RETURN; node _ TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]; replaceRopeInterval _ [ropeID: viewerInfo.ropeInterval.ropeID, start: FindEnd[viewerInfo], length: 0]; TiogaVoicePrivate.RecordInPlaceOfSelection[NEW[TiogaVoicePrivate.SelectionRec _ [viewer: viewer, voiceViewerInfo: viewerInfo, ropeInterval: replaceRopeInterval, displayNode: node]]] }; criticalSilenceLength: INT _ Jukebox.bytesPerChirp/2; SetCriticalSilenceLength: Commander.CommandProc = { argv: CommandTool.ArgumentVector _ CommandTool.Parse[cmd ! CommandTool.Failed => {msg _ errorMsg; GOTO Quit}]; IF argv.argc # 2 THEN { msg _ "Usage: SetCriticalSilenceLength lengthInMilliseconds"; GOTO Quit }; criticalSilenceLength _ Convert.IntFromRope[argv[1]]*8; RETURN EXITS Quit => RETURN [$Failure, msg] }; AdjustSilences: Menus.MenuProc = { viewer: ViewerClasses.Viewer; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo; selectionToRemove: TiogaVoicePrivate.Selection; startOfSilence, lengthOfSilence: INT; viewer _ NARROW [parent, ViewerClasses.Viewer]; viewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF ~TiogaVoicePrivate.GetVoiceLock[viewerInfo] THEN { MessageWindow.Append["Unable to examine silences in viewer - editing operation already in progress", TRUE]; MessageWindow.Blink[]; RETURN }; TiogaOps.SaveSelA[]; DO [startsAt: startOfSilence, lasts: lengthOfSilence] _ TiogaVoicePrivate.LastSilenceInSoundList[soundList: viewerInfo.soundList, lengthGreaterThan: criticalSilenceLength]; IF lengthOfSilence = -1 THEN { viewerInfo.editInProgress _ FALSE; TiogaOps.RestoreSelA[]; RETURN }; selectionToRemove _ NEW[TiogaVoicePrivate.SelectionRec _ [viewer: viewer, voiceViewerInfo: viewerInfo, ropeInterval: [ropeID: viewerInfo.ropeInterval.ropeID, start: startOfSilence+criticalSilenceLength, length: lengthOfSilence-criticalSilenceLength], displayNode: TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]]]; viewerInfo.edited _ TRUE; [] _ TiogaVoicePrivate.ReplaceSelectionWithSavedInterval[selection: selectionToRemove, soundInterval: NIL, leaveSelected: FALSE] ENDLOOP }; dictationMenu: Menus.MenuEntry; TEditFormat.RegisterCharacterArtwork[talksBubbleClass]; ageColors[1] _ "0.13 0.97 0.96 textColor"; ageColors[2] _ "0.06 0.94 1.00 textColor"; ageColors[3] _ "0.99 1.00 1.00 textColor"; ageColors[4] _ "0.99 1.00 0.65 textColor"; ageColors[5] _ ageColors[6] _ "0.97 1.00 0.28 textColor"; -- last two must be the same Menus.InsertMenuEntry[TiogaVoicePrivate.voiceViewerMenu, MBQueue.CreateMenuEntry[TiogaVoicePrivate.voiceButtonQueue, "AdjustSilences", AdjustSilences], 1]; Menus.InsertMenuEntry[TiogaVoicePrivate.voiceViewerMenu, MBQueue.CreateMenuEntry[TiogaVoicePrivate.voiceButtonQueue, "ResumeFromEnd", ResumeFromEnd], 1]; Menus.InsertMenuEntry[TiogaVoicePrivate.voiceViewerMenu, MBQueue.CreateMenuEntry[TiogaVoicePrivate.voiceButtonQueue, "ResumeFromSelection", ResumeFromSelection], 1]; Menus.InsertMenuEntry[TiogaVoicePrivate.voiceViewerMenu, MBQueue.CreateMenuEntry[TiogaVoicePrivate.voiceButtonQueue, "PlayFromSelection", PlayFromSelection], 1]; dictationMenu _ Menus.GetLine[TiogaVoicePrivate.voiceViewerMenu, 1]; Menus.SetLine[TiogaVoicePrivate.voiceViewerMenu, 1, NIL]; Menus.ChangeNumberOfLines[TiogaVoicePrivate.voiceViewerMenu, 1]; Commander.Register["SetCriticalSilenceLength", SetCriticalSilenceLength, "SetCriticalSilenceLength lengthInMilliseconds - when the AdjustSilences button is bugged for any voice viewer, all silences of more than this length will be reduced to this length"]; }. VoiceMarkersImpl.mesa Copyright Ó 1987 by Xerox Corporation. All rights reserved. Ades, May 1, 1986 10:39:24 am PDT Swinehart, April 9, 1987 10:32:10 am PDT TiogaVoicePrivate Handle markers in voice, of two types i) a special character which the user can place at a selection and remove using button-pushes ii) annotation of voice with text [if I get round to it] at present, when selections are used to delete/add markers, selections do not change - fix! [char marks manually before calling VoicePlayback.Redraw, text marks manually] Character markers: these are transitory and do not form part of the contents of the viewer adds marks to the selected voice viewer at each end of the selection if pending delete, otherwise at the position of the caret called when a viewer is known to be a voice viewer with at least position+1 characters in it: the caller should aready hold the VoiceLock this gets called by TiogaVoicePrivate.SoundChars, which has been called to build the viewer contents corresponding to some soundlist, the first skipChars omitted: takes that rope and replaces normal characters with marker characters as appropriate return as selection.charMarkList a copy [n.b.] of the section of the character mark list in viewerInfo falling in to the region of selection.ropeInterval this gets called when an edit is made to the contents of the viewer: it keeps the charMarkList in step with the edit it is not a very efficient implementation - better ones are an exercise for the reader Textual markers: these markers are really a part of the viewer contents. They are saved when the voice is saved and altering them makes the window status 'edited' **** a Plass-bug: shouldn't be necessary and he says he'll have a look at it entry potentially contains the supplied entry.text: this may only extend over maxLength voice characters, so it may not be possible to display all of it. Set entry.displayChars appropriately and set entry.width to say how much space is taken up by the text actually on display text limiting is currently accomplished by preserving all the text but only displaying that part which can be displayed: this means that when text obscuring other text is deleted the obscured text reappears for example. It could be arranged that the obscured text was actually eradicated: this would be done by inserting the following line and deleting the displayChars field of the TextMarkEntry and all the code that accesses it IF currPos # textLength THEN entry.text _ entry.text.Substr[0, currPos]; if there is a textual marker at the selection then remove its last character **** a Plass-bug: shouldn't be necessary and he says he'll have a look at it if there are textual markers within the selection then remove them completely - perhaps a rather odd interpretation of BackWord? **** a Plass-bug: shouldn't be necessary and he says he'll have a look at it called by voicePlayBack's redraw procedure [under a voice lock] to place all the textual markers back in a voice viewer return as selection.textMarkList a copy [n.b.] of the section of the textual mark list in viewerInfo falling in to the region of selection.ropeInterval this gets called when an edit is made to the contents of the viewer: it keeps the textMarkList in step with the edit it is not a very efficient implementation - better ones are an exercise for the reader first alter the old list to remove the text in the deleted area and move text after that area convert the list of text markers in a voice viewer into a rope suitable for saving in a textual document at the position of a talks bubble and go the other way TextInVoice it's all getting so recursive !!!! - routines which draw the artwork around voice viewers; that artwork is actually a textual display although the extents and positions of text in artworks are given in terms of constants from TiogaVoicePrivate, note that the line spacing is set up independently by VoiceProfile.Style VoiceAging Adds colors to and ages colors in voice viewers: the dictation ops buttons in voice viewers may use the ages of sections of a viewer to determine where to delete/playback/resume recording in each voice viewer is a list of ages: these are pairs of INTs and give the starting position for the age and its value. The age lasts until the next age entry's position or the end if last in the list. in each voice viewer is a list of ages: these are pairs of INTs and give the starting position for the age and its value. The age lasts until the next age entry's position or the end if last in the list. when age lists are edited, adjacent entries with the same age values can clearly be merged. The valid ages are [oldest..youngest] as given below. youngest-1 is a transitory value used before a call to AgeAllViewers when voice is inserted. oldest+1 is used to mean "this viewer didn't age at all in the last call to AgeAllViewers: oldest+1 may only appear as a single element age list with a position of 0 the following values may be changed to anything you care for, so long as oldest>youngest and provided you manually supply the correct number of colors required for this array -- see end of this module a viewer has been edited and the age list for it has been updated appropriately: redo the color looks so that the age marks are once more in the correct places **** I still haven't got right when to repaint and when not an editing event has occured which ages all the voice viewers this gets called when an edit is made to the contents of the viewer: it keeps the ageList in step with the edit. If insertChars # 0 then the new position is made of age youngest-1 so that it will become the youngest when the next call to AgeAllViewers is made DictationOps special ops provided to support the "stop, listen, [erase,] resume" model of a dictation machine, also to compress silence periods after such a dictation session all of the dictation operations stand apart from the normal rules for playback/recording, which are that one must be stopped manually before the other can be requested. A playback or record request in this module implicitly cancels any already in progress. the dictation machine works differently for color and monochrome viewers: the end point for playback/resume and delete to-/record from is the end of the viewer for monochrome and the end of the youngest sound for the color the button to toggle the dictation menu on and off the screen manual version of same thing procedure to interpret 'end' for monochrome/color viewers there should be a 'real rope' in this viewer!! if the supplied viewerInfo does not represent an existing voice viewer (having been deleted due to being empty) display and error message and return FALSE **** need to take the voice lock before performing the following! routines which allow the lengths of silent portions within a rope to be adjusted not interested in any of the returned values from this routine - in particular the viewer will not have been deleted by this call since we have only deleted on portion of one silent interval this action really ought not to age the voice viewers - this requires an extra parameter to ReplaceSelectionWithSavedInterval. See the implementation of that routine - the call to TiogaVoicePrivate.RedrawViewer can take a don't age option, which we currently don't exploit Initialization these are HSV values: colors can be generated experimentally using the ColorTool program Ê&'˜šœ™Icode™Jšœ œ˜Jšœ œ˜!Jšœ œV˜dJšœ œ˜#JšœœÒ˜éJšœœœ˜Jšœœ ˜Jšœ œ˜)Jšœ œ˜)Jšœ˜J™—Jš Ïnœœœœ¸œ˜üJ˜šÐbl™J™J™%J™]J™8J™«J˜JšœZ™ZJ˜šž œœ˜&J™~Jšœ˜Jšœ"˜"Jšœœ˜$J˜.J˜Jšœc˜cJ˜Jšœ œS˜fJšœœ˜Jš˜šœDœ˜JJšœ˜Jš˜—J˜J˜Jšœ,˜2šœ˜Jšœœ?œœ˜iJšœœ œ#˜GJšœœœ!˜Fšœœ6˜PJšœi˜iJšœ3˜3Jšœ˜!—J˜—Jšœ˜˜J˜——šžœœ˜Jšœ9œ˜AJšœ œœœ˜Jš œœœœ œ˜TJš˜šœœ+˜4Jšœœ*œœ&˜xJš˜˜š œ7œœœœ#˜‚˜Jšœ"œœ˜]Jš˜—J˜—Jšœ˜J˜Jšœœ œ˜&—J˜—J˜˜J˜——šžœœœ*œ˜PJšœ‰™‰Jšœ0œS˜‰Jšœœœœ˜Jšœ˜šœœ6˜PJšœi˜iJšœ3˜3—J˜šœ˜J™——šžœœ˜*Jšœ˜Jšœ"˜"J˜.J˜šžœœ œ˜)Jš œœœœ œ8œ˜–Jš!œœœœœœœ#œœœ œœœœ œœÏc\˜™—Jšœ˜J˜Jšœœ 3˜Nšœ œ˜1Jšœ œS˜fJšœ,˜2šœœ˜!šœœ6˜PJšœi˜iJšœ3˜3Jšœ˜!—J˜—J˜—J˜Jšœ 9˜>šœF˜FJšœ œS˜fJšœœ˜Jš˜šœDœ˜JJšœ˜Jš˜—J˜J˜Jšœ,˜2šœ˜Jšœœ?œœ˜iJšœ(˜(šœœ6˜PJšœi˜iJšœ3˜3Jšœ˜!—J˜—Jšœ˜—J˜šœ˜J˜——šžœœœ˜Jšœœœœœ œœœ˜gJšœ&ž œÇ™÷Jšœ œ˜'Jšœ œ˜Jšœ œ˜Jšœ œ˜Jš œœœ œ"œ˜iš˜Jš œ œœœ œ ˜VJšœU˜UJšœœœ˜&Jšœ% ˜Jš˜šœ-˜-Jšœ/˜/Jšœ.˜.Jš œœ œœœœœ?˜Jšœ0˜2Jšœ/œ˜9—J˜Jš˜šœœœ;˜HJš œ œœ(œ œ˜TJ˜Jšœ œœ'˜9Jšœ 7˜<šœ œ˜!Jšœ˜Jšœ:˜:Jšœœ/œ˜WJšœ œA˜PJšœ œ˜!—J˜Jšœ +˜0šœ˜Jšœ.˜.Jšœ.˜.—J˜J˜Jšœ  "˜.Jš œœ œœœœœ)˜aJšœ0˜2Jšœ/œ˜8—J˜—J˜J˜J™LJšœœBœœ˜hJ˜Jšœœ˜Jšœ œ5 `˜¤Jšœœ˜"˜J˜——šž œœ5œ˜LJšœ•™•Jšœœœ-˜GJšœ œ˜$Jšœ œ˜Jšœœ˜šœ˜Jšœœ€˜“Jšœœœ˜+Jšœ˜Jšœ˜—Jšœ˜J˜Jšœ®™®JšœH™HJ™Jšœp˜pJšœ˜˜J™——šž œœœ#˜9J™LJšœ˜J˜+J˜šž œœ˜-Jšœ9œœ /˜‹Jšœm˜m—J˜J˜Jšœ&˜&Jšœ(˜(Jšœ%œ˜)Jšœ œ˜J˜.Jšœf˜fJšœœœ˜'Jšœ œS˜fJšœ-œœ˜;J˜Jšœœ œœ˜*Jšœ+˜+Jšœ˜J˜Jšœ œ˜š œœœCœœœ˜bJšœ$˜*šœœ#œ˜-šœ˜Jš œœ œœœœœ'˜`—J˜Jš˜—J˜—šœ˜J˜—Jšœ œ˜JšœEœ˜NJš˜šœœ˜0JšœD˜DJšœ*˜*J˜Jšœœ˜Jšœ(˜*Jšœ˜šœ&˜&J™LJšœAœœ˜X—J˜Jšœ<˜@—J˜J˜Jšœœ˜"˜J˜——šžœœœ#˜8Jšœwžœ™€Jšœ˜J˜+J˜šž œœ˜-Jšœ9œœœœœœœ /˜±Jš œ;œœœœœ3˜“—J˜J˜Jšœ&˜&Jšœ"˜"Jšœ œœ˜J˜.JšœL˜LJšœœœ˜'Jšœœœ˜$Jšœ œS˜fJšœ,˜,J˜Jšœ-œœ˜;Jšœœ˜%šœœ˜%Jš˜—J˜J˜Jšœ7˜9šœ 1˜6šœœœ5˜^Jšœ*˜*Jšœœ˜Jšœ#˜#Jšœœ˜Jšœ6˜6—Jš˜—Jš˜šœœœ;˜HJš œ œœ%œ œ˜Qšœ œœ$˜Jšœ&˜*Jš˜šœ˜Jš œ œœ'œ œ˜SJšœ&˜&—J˜—J˜J˜šœœœC˜YJšœ œœFœ˜aJšœF˜FJšœ˜šœœ˜JšœœœFœ˜iJšœ#˜#Jšœ˜JšœK˜K—Jš˜—J˜J˜Jšœ!˜!Jš œœœœœœœC˜‘J˜Jšœœ˜Jšœ%˜)Jš˜šœ"˜"JšœV˜V—J˜—J˜Jšœ X˜]šœœœœ8˜ašœœœ;˜PJš œœœ/œœ˜sJš œœœœœœœ:˜€—J˜—Jšœ˜˜J˜——šžœœœ˜Jš œœœ"œ œœ˜LJ™Ššœœ˜Jšœœ˜$Jšœœ:˜PJšœc˜cJšœ˜J˜ —Jš˜J˜—šžœœœ œ˜,Jšœœœ#œ˜>J™Jšœ œœ!˜3J˜šœ˜Jšœ œ˜Jšœ,œvœ ˜±Jšœ œ˜J˜J˜Jšœ9˜9J˜Jšœ(˜(Jšœ˜J˜Jšœ˜ Jš˜šœœ œ˜Jšœ ˜ —J˜Jš˜šœœ œ˜(Jšœ˜—J˜—Jšœ˜J˜š œœœ0œœ˜TJš œœ œœœœœ0˜l—Jš˜˜J˜——Jšœœ?˜VJ˜—šŸ ™ J™J™…Jšœ\žœJ™·J™šœœœ˜#Kšœœ˜ Kšœ œ˜Kšœ œ˜Kšœ ˜Kšœ˜K˜—šžœœ˜Kšœ@˜@Kšœœœ ˜1K˜šž œ˜"Kšœ#˜#Kšœ˜Kšœ'˜'Kšœ(˜(Kšœ˜—K˜K˜Kšœ˜Kšœ&˜&Kšœ'˜'Kšœ$˜$Kšœ&˜&Kšœ&œ˜,Kšœ6˜6Kšœ)˜)Kšœ%˜%K˜K˜—šžœœ˜Kšœtœ#˜žKšœR˜RKšœœ0˜šœkœ œ˜…Kšœœœ.˜8šœœ˜Kšœ œœœ˜4šœœ œœ˜.Kšœ˜šœN˜NKšœ œ˜³—Kšœ˜Kšœ˜šœ K˜Mšœœ˜Kšœœ˜)K˜ —Kšœ˜K˜Kšœ˜š œœœ œœ [˜}Kšœ œœ ˜K—Kšœ˜Kšœ(˜(—K˜—K˜——š˜˜˜K˜————šžœœœ˜KšœXœ˜`Kšœîž œ™ƒKšœœ˜Kšœœœ˜Kšœœœ0˜:šœœœœ˜K˜—Kš œ œœ5œ œ˜bšœ˜K˜—šœœœ˜,šœ#œ˜.Jšœœ˜”JšœA˜A—Jšœ œ˜—šœ˜J˜—Jšœœœ˜!šœ˜š œœœ œœ [˜}šœœ˜"šœ ˜ JšœK˜KJ˜ —Jšœ)˜)—Jšœ˜—Jšœ˜—˜˜J˜——JšœœœB˜`Jš˜š œœœœAœœD˜ÄJšœœœ2˜c—Jšœ˜Jš˜šœ˜Jš œ œœ5œ œ˜aJš œ œœ5œ œ8˜‘Jšœœ œ&˜K—Jšœ˜J˜Jšœ œœœœœœ˜nJ˜Jšœ˜š œœœ œœ [˜}Kšœ œœ ˜K—Kš˜šœ˜K˜———šŸ ™ J™K™¡K™™ÞJ™—šž œœ˜(J™=Jšœœ˜>J™—šžœœœ$˜DJ™šœ6˜6J˜——šžœœ1œ œ˜WJšœ9™9Jšœ#œœ˜0J™.Jšœœœ!˜AJšœ˜šœ œ ˜Jšœ œœœ˜š œœœ8œœ˜VJšœœ˜š œœœœœœœ˜FJšœ˜—Jšœ˜—Jšœ˜Jš œœ œœœ œ0˜x—J˜˜J˜——šžœœ1œœ˜bJšœ•™šJšœT˜Tš˜Jšœ œ˜šœ˜Jšœlœ˜rJšœ˜Jšœœ˜ —J˜Jšœœœœ˜+Jšœ˜—Jš˜˜J˜——šžœ˜&Jšœ˜Jšœ˜J˜.Jšœ˜Jšœ œ˜J˜Jšœ9˜9Jšœ œ˜Jš˜šœ2œ˜8Jšœ˜Jš˜—J˜Jšœ œS˜fJšœœ˜Jš˜šœDœ˜JJšœ˜Jš˜—J˜J˜Jšœ/˜/Jšœ#˜#Jšœ"˜"Jšœ%œœ˜3J˜J™AJšœ7˜7J˜Jšœ?˜?Jšœ ˜Jšœ:˜>Jš˜šœSœ˜YJšœ˜—Jšœ˜šœ˜J˜——šžœ˜(Jšœ˜Jšœ˜J˜.Jšœ˜Jšœ1˜1Jšœœ˜J˜Jšœ9˜9Jšœ œ˜Jš˜šœ2œ˜8Jšœ˜Jš˜—J˜Jšœ œS˜fJšœœ˜Jš˜šœDœ˜JJšœ˜Jš˜—J˜J˜Jšœ/˜/Jšœ#˜#Jšœ"˜"Jšœ%œœ˜3Jšœ-œœ˜;J˜Jšœ7˜7Jšœ@˜@Jšœ ˜ Jšœ˜šœSœ˜YJšœ˜Jšœœ˜"Jš˜—Jšœ˜J˜Jšœt˜tJ˜Jšœ+œ‡˜µ˜J˜——šž œ˜!Jšœœ˜DJšœ0œS˜‰Jšœ˜Jšœ1˜1J˜Jšœ/˜/Jšœ#˜#Jšœ"˜"Jšœ%œœ˜3J˜Jšœ-œœ˜;J˜Jšœ7˜7Jšœf˜fJ˜Jšœ+œ‡˜µšœ˜J™——™PJ™—šœœ˜5J™—šžœ˜3Jšœbœ˜nšœœ˜Jšœ=˜=Jšœ˜ J˜—J˜Jšœ7˜7Jš˜J˜Jš˜Jšœœ˜˜J˜——šžœ˜"Jšœ˜J˜.Jšœ/˜/Jšœ!œ˜%J˜Jšœ œ ˜/Jšœ œS˜fJšœ-œ˜4šœ˜Jšœeœ˜kJšœ˜Jš˜—Jšœ˜J˜J˜š˜Jšœ©˜©Jšœœ˜šœœ˜%J˜Jš˜—Jšœ˜J˜Jšœœ¥˜¼—˜Jšœœ˜Jšœfœœ˜€J™¾J™Jšœ™—Jš˜˜J˜——Jšœ˜J˜—šŸ™J™Jšœ7˜7Kšœ*˜*Kšœ*˜*Kšœ*˜*Kšœ*˜*šœV˜VK™XK™—Jšœ›˜›Jšœ™˜™Jšœ¥˜¥Jšœ¡˜¡JšœD˜DJšœ4œ˜9Jšœ@˜@J˜Jšœ€˜€J˜—Jšœ˜—…—™tܵ