DIRECTORY Char USING [XCHAR, CharSet, Code, Make, Set], CharOps USING [AlphaNumeric], Commander USING [CommandProc, Register], CommanderOps USING [ArgumentVector, Parse, Failed], Convert USING [RopeFromInt, IntFromRope], Imager USING [Context, Move, ShowChar, SetStrokeWidth, SetStrokeJoint, MaskStroke, SetXY, ShowRope, SetFont, black, SetColor], ImagerFont USING [Escapement, Extents, FindScaled, Font, FontBoundingBox, MapRope, RopeEscapement, XCharProc], ImagerPath USING [PathProc], MessageWindow USING [Append, Blink], NodeStyle USING [Style, GetFont], Real USING [Fix], Rope USING [Cat, Concat, Fetch, Find, FromChar, Length, ROPE, Substr], TEditFormat USING [CharacterArtwork, CharacterArtworkRep, CharacterArtworkClass, CharacterArtworkClassRep, RegisterCharacterArtwork], TextEdit USING [FetchChar, GetCharProp, PutCharProp, ReplaceByRope, Size], TextNode USING [Location, Ref], TiogaOps USING [CallWithLocks, CancelSelection, CommandProc, FirstChild, GetRope, GetSelection, RestoreSelA, SaveSelA, StepForward, ViewerDoc ], TiogaOpsDefs USING [Location, Ref, WhichSelection], TiogaVoicePrivate USING [ ButtonParams, bytesPerChirp, CancelPlayBack, GetCurrentPlayBackPos, GetVoiceLock, GetVoiceViewerInfoList, IntPair, LastSilenceInSoundList, MakeVoiceEdited, PlayBackInProgress, PlaySlabSection, RecordInPlaceOfSelection, RedrawViewer, ReplaceSelectionWithSavedInterval, Selection, SelectionRec, SetVoiceViewerEditStatus, SoundChars, SoundInterval, soundRopeCharLength, StopRecording, TextMarkEntry, TextMarkRec, thrushHandle, voiceCharAscent, voiceCharDescent, voiceCharHeight, voiceCharSet, voiceCharWidth, VoiceViewerInfo, VoiceViewerInfoList ], Vector2 USING [VEC], ViewerClasses USING [MouseButton, Viewer], ViewerForkers USING [ForkPaint], ViewerOps USING [FetchProp], VoiceRope USING [Stop, VoiceRopeInterval] ; VoiceMarkersImpl: CEDAR PROGRAM IMPORTS Char, CharOps, Commander, CommanderOps, Convert, Imager, ImagerFont, MessageWindow, NodeStyle, Real, Rope, TEditFormat, TextEdit, TiogaOps, TiogaVoicePrivate, ViewerForkers, ViewerOps, VoiceRope EXPORTS TiogaVoicePrivate = { AddCharMark: PUBLIC TiogaOps.CommandProc = { buttonParams: TiogaVoicePrivate.ButtonParams ¬ NARROW[ViewerOps.FetchProp[viewer, $ButtonParams]]; mouseButton: ViewerClasses.MouseButton ¬ IF buttonParams#NIL THEN buttonParams.mouseButton ELSE $red; SELECT mouseButton FROM $red, $blue => AddMarksAtSelection[viewer]; $yellow => AddMarkAtPlayBackLocation[]; ENDCASE; }; AddMarkAtPlayBackLocation: PROC = { viewer: ViewerClasses.Viewer ¬ NIL; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo; display: BOOLEAN ¬ FALSE; currentPos: INT ¬ -1; IF TiogaVoicePrivate.PlayBackInProgress[] THEN { [display, viewer, currentPos] ¬ TiogaVoicePrivate.GetCurrentPlayBackPos[]; }; IF currentPos = -1 THEN { MessageWindow.Append["No current playback to mark (possibly abort in progress)", TRUE]; MessageWindow.Blink[]; RETURN; }; viewerInfo ¬ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF (NOT display) OR (viewerInfo = NIL) THEN { MessageWindow.Append["No voice viewer to mark for current playback", TRUE]; MessageWindow.Blink[]; RETURN; }; IF TiogaVoicePrivate.GetVoiceLock[viewerInfo] THEN { MarkChar[viewerInfo, currentPos]; { trueContents: Rope.ROPE ¬ TiogaVoicePrivate.SoundChars[viewerInfo].soundRope; [] ¬ TiogaVoicePrivate.RedrawViewer[viewer, trueContents, 0, 0, 0, viewerInfo.remnant, FALSE, unAltered]; TiogaVoicePrivate.SetVoiceViewerEditStatus[viewer]; viewerInfo.editInProgress ¬ FALSE } }; }; AddMarksAtSelection: PROC [parent: ViewerClasses.Viewer] = { 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 viewer = NIL OR viewer # parent OR viewerInfo = NIL THEN { MessageWindow.Append["Make a selection in this voice viewer first", TRUE]; MessageWindow.Blink[]; RETURN }; IF TiogaVoicePrivate.GetVoiceLock[viewerInfo] THEN { IF NOT ((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 TiogaOps.CommandProc = { 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 }; -- end of RemoveMarkChars buttonParams: TiogaVoicePrivate.ButtonParams ¬ NARROW[ViewerOps.FetchProp[viewer, $ButtonParams]]; mouseButton: ViewerClasses.MouseButton ¬ IF buttonParams#NIL THEN buttonParams.mouseButton ELSE $red; IF mouseButton = $blue THEN { -- right click means delete all marks in the 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 { v: ViewerClasses.Viewer; [viewer: v, start: start, end: end] ¬ TiogaOps.GetSelection[]; viewerInfo ¬ NARROW[ViewerOps.FetchProp[v, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF v = NIL OR v # viewer OR viewerInfo = NIL THEN { MessageWindow.Append["Make a selection in this 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, target: TiogaOpsDefs.WhichSelection¬primary] = { 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]]; }; voiceViewer: ViewerClasses.Viewer; voiceRoot: TiogaOpsDefs.Ref; start, end, char: TiogaOpsDefs.Location; insertPosition: INT; caretBefore: BOOLEAN; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo; charsAlreadyDisplayed: INT ¬ 0; redraw: BOOLEAN ¬ FALSE; [viewer: voiceViewer, start: start, end: end, caretBefore: caretBefore] ¬ TiogaOps.GetSelection[target]; IF voiceViewer # viewer THEN ERROR; voiceRoot ¬ TiogaOps.ViewerDoc[voiceViewer]; char ¬ IF caretBefore THEN start ELSE end; node ¬ char.node; insertPosition ¬ char.where; IF TextEdit.Size[node] <= insertPosition THEN { MessageWindow.Append["Select a character in a voice viewer before entering text marker", TRUE]; MessageWindow.Blink[]; RETURN; }; viewerInfo ¬ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF NOT TiogaVoicePrivate.GetVoiceLock[viewerInfo] THEN RETURN; 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, voiceRoot]; 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, voiceRoot]; 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, voiceRoot]; 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, voiceRoot]; redraw ¬ TRUE} } }; TiogaVoicePrivate.MakeVoiceEdited[viewer]; 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 ¬ ImagerFont.RopeEscapement[voiceMarkerFont, entry.text.Substr[len: currPos+1]].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] = { Back[viewer, BackSpaceProc]; }; BackWord: PUBLIC PROC [viewer: ViewerClasses.Viewer] = { Back[viewer, BackWordProc]; }; BackProc: TYPE = PROC [textEntry: TiogaVoicePrivate.TextMarkEntry] RETURNS [len: INT]; BackSpaceProc: BackProc ~ { len ¬ textEntry.text.Length[] - 1; }; BackWordProc: BackProc ~ { len ¬ textEntry.text.Length[]; WHILE len>0 AND NOT CharOps.AlphaNumeric[textEntry.text.Fetch[len-1]] DO len ¬ len-1; ENDLOOP; WHILE len>0 AND CharOps.AlphaNumeric[textEntry.text.Fetch[len-1]] DO len ¬ len-1; ENDLOOP; }; Back: PROC [viewer: ViewerClasses.Viewer, findNewLen: BackProc] = { 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, char: TiogaOpsDefs.Location; deletePosition, charsToNextPosition: INT; prevptr: LIST OF TiogaVoicePrivate.TextMarkEntry ¬ NIL; 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 ¬ 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 }; prevptr ¬ l; ENDLOOP; IF textEntry = NIL THEN MessageWindow.Append["No textual annotation at point of selection", TRUE] ELSE { charsOnDisplay: INT ¬ textEntry.displayChars; len: INT ¬ findNewLen[textEntry]; textEntry.text ¬ IF len<=0 THEN NIL ELSE textEntry.text.Substr[0, len]; DO LimitText[textEntry, charsToNextPosition]; IF charsOnDisplay # textEntry.displayChars THEN { TiogaOps.CallWithLocks[AddArtwork]; }; IF textEntry.text=NIL THEN -- remove this marker from textMarkList IF prevptr=NIL THEN viewerInfo.textMarkList ¬ viewerInfo.textMarkList.rest ELSE { -- also need to repaint previous text marker prevptr.rest ¬ prevptr.rest.rest; IF charsToNextPosition # LAST[INT] THEN charsToNextPosition ¬ charsToNextPosition + textEntry.position - prevptr.first.position; textEntry ¬ prevptr.first; LOOP; }; EXIT; ENDLOOP; TiogaVoicePrivate.MakeVoiceEdited[selectionViewer]; }; viewerInfo.editInProgress ¬ FALSE }; start16Bits: Rope.ROPE ¬ "ÿÿ"; -- 377B 377B 0B stop16Bits: Rope.ROPE ¬ "ÿ"; -- 377B CopyTextViewerToTextMarker: PUBLIC PROC [target: TiogaOpsDefs.WhichSelection¬primary] ~ { use16Bits: BOOL ¬ FALSE; text: Rope.ROPE ¬ NIL; ToXeroxStringEncoding: PROC [loc: TiogaOpsDefs.Location] ~ { charSet: Char.CharSet¬0; char: CHAR; xc: Char.XCHAR ¬ TextEdit.FetchChar[loc.node, loc.where]; charSet ¬ xc.Set; char ¬ VAL[xc.Code]; IF charSet # 0 THEN { IF NOT use16Bits THEN { use16Bits ¬ TRUE; text ¬ Rope.Concat[text, start16Bits]; }; text ¬ Rope.Concat[text, Rope.FromChar[VAL[charSet]]]; } ELSE IF use16Bits THEN { -- back to charSet 0 use16Bits ¬ FALSE; text ¬ Rope.Concat[text, stop16Bits]; }; text ¬ Rope.Concat[text, Rope.FromChar[char]]; }; start, end: TiogaOpsDefs.Location; voiceViewer: ViewerClasses.Viewer ¬ TiogaOps.GetSelection[target].viewer; [start: start, end: end] ¬ TiogaOps.GetSelection[SourceSel[target]]; -- in text viewer ApplyToChars[start, end, ToXeroxStringEncoding]; IF use16Bits THEN text ¬ Rope.Concat[text, stop16Bits]; TextInput[voiceViewer, text, target]; TiogaOps.CancelSelection[secondary]; }; CopyTextMarkerToTextViewer: PUBLIC PROC [target: TiogaOpsDefs.WhichSelection¬primary] ~ { textRoot, insertNode: TextNode.Ref; insertPosition: INT; InsertMarks: PROC [root: TiogaOpsDefs.Ref] ~ { ImagerFont.MapRope[rope: text, charAction: TiogaXCharProc]; }; TiogaXCharProc: ImagerFont.XCharProc ~ { [] ¬ TextEdit.ReplaceByRope[root: textRoot, dest: insertNode, start: insertPosition, len: 0, rope: Rope.FromChar[VAL[char.Code]], charSet: char.Set]; insertPosition ¬ insertPosition+1; }; text: Rope.ROPE; start, end: TiogaOpsDefs.Location; caretBefore: BOOL; textViewer: ViewerClasses.Viewer; [viewer: textViewer, start: start, end: end, caretBefore: caretBefore] ¬ TiogaOps.GetSelection[target]; IF start.node = NIL OR end.node = NIL THEN RETURN; textRoot ¬ TiogaOps.ViewerDoc[textViewer]; IF caretBefore THEN { insertNode ¬ start.node; insertPosition ¬ start.where; } ELSE { insertNode ¬ end.node; insertPosition ¬ end.where+1; }; text ¬ GetTextMarksFromSelection[SourceSel[target]]; TiogaOps.CallWithLocks[InsertMarks, textRoot]; TiogaOps.CancelSelection[secondary]; }; SourceSel: PROC [target: TiogaOpsDefs.WhichSelection] RETURNS [source: TiogaOpsDefs.WhichSelection] ~ INLINE { source ¬ IF target=primary THEN secondary ELSE primary; }; ApplyToChars: PROC [start, end: TiogaOpsDefs.Location, actionProc: PROC [loc: TiogaOpsDefs.Location]] = { current: TiogaOpsDefs.Location ¬ start; nodeSize: INT ¬ TextEdit.Size[current.node]; IF start.node = NIL OR end.node = NIL THEN RETURN; IF current.node = end.node THEN FOR i: INT IN [current.where..end.where] WHILE i < nodeSize DO actionProc[[current.node, i]] ENDLOOP ELSE { FOR i: INT IN [current.where..nodeSize) DO actionProc[[current.node, i]] ENDLOOP; DO current.node ¬ TiogaOps.StepForward[current.node]; nodeSize ¬ TextEdit.Size[current.node]; IF current.node = end.node THEN { FOR i: INT IN [0..end.where] WHILE i < nodeSize DO actionProc[[current.node, i]] ENDLOOP; RETURN } ELSE FOR i: INT IN [0..nodeSize) DO actionProc[[current.node, i]] ENDLOOP; ENDLOOP; }; }; GetTextMarksFromSelection: PROC [source: TiogaOpsDefs.WhichSelection¬primary] RETURNS [textMarks: Rope.ROPE ¬ NIL] ~ { voiceViewer: ViewerClasses.Viewer; start, end: TiogaOpsDefs.Location; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo; [viewer: voiceViewer, start: start, end: end] ¬ TiogaOps.GetSelection[source]; IF voiceViewer=NIL OR start.node=NIL OR end.node=NIL THEN RETURN; IF start.node # end.node THEN ERROR; -- BadVoiceViewerDisplay viewerInfo ¬ NARROW[ViewerOps.FetchProp[voiceViewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF viewerInfo=NIL THEN RETURN; -- not a voice viewer; selection has moved FOR l: LIST OF TiogaVoicePrivate.TextMarkEntry ¬ viewerInfo.textMarkList, l.rest WHILE l # NIL AND l.first.position <= end.where DO IF l.first.position >= start.where THEN textMarks ¬ Rope.Concat[textMarks, l.first.text]; ENDLOOP; }; RemoveTextMarkers: 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 ¬ 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 remove 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 { TiogaVoicePrivate.MakeVoiceEdited[selectionViewer]; }; viewerInfo.editInProgress ¬ FALSE }; RedrawTextMarkers: PUBLIC PROC [ viewer: ViewerClasses.Viewer, voiceCharNode: TiogaOpsDefs.Ref] = { node: TextNode.Ref ¬ voiceCharNode; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo ¬ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; AddArtwork: PROC [root: TiogaOpsDefs.Ref] = { l: LIST OF TiogaVoicePrivate.TextMarkEntry; FOR l ¬ viewerInfo.textMarkList, l.rest WHILE l # NIL DO 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]]; ENDLOOP; }; TiogaOps.CallWithLocks[AddArtwork, TiogaOps.ViewerDoc[viewer]]; }; 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 { beforeInsert, afterInsert: LIST OF TiogaVoicePrivate.TextMarkEntry ¬ NIL; newSection, endNewSectionPtr: 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 } }; { insertMarkPtr: LIST OF TiogaVoicePrivate.TextMarkEntry ¬ soundInterval.textMarkList; newSection ¬ CONS [NEW [TiogaVoicePrivate.TextMarkRec ¬ insertMarkPtr.first­], NIL]; newSection.first.position ¬ newSection.first.position + unchangedHead; endNewSectionPtr ¬ newSection; insertMarkPtr ¬ insertMarkPtr.rest; WHILE insertMarkPtr # NIL DO endNewSectionPtr.rest ¬ CONS [NEW [TiogaVoicePrivate.TextMarkRec ¬ insertMarkPtr.first­], NIL]; endNewSectionPtr ¬ endNewSectionPtr.rest; endNewSectionPtr.first.position ¬ endNewSectionPtr.first.position + unchangedHead; insertMarkPtr ¬ insertMarkPtr.rest; ENDLOOP; }; endNewSectionPtr.rest ¬ afterInsert; LimitText[endNewSectionPtr.first, IF endNewSectionPtr.rest = NIL THEN LAST[INT] ELSE afterInsert.first.position - endNewSectionPtr.first.position]; IF beforeInsert = NIL THEN viewerInfo.textMarkList ¬ newSection ELSE { beforeInsert.rest ¬ newSection; LimitText[beforeInsert.first, newSection.first.position - beforeInsert.first.position] }; } ELSE { 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.FindScaled["Xerox/XC1/Tioga-Classic-12",12.0]; voiceMarkerExtents: ImagerFont.Extents ¬ ImagerFont.FontBoundingBox[voiceMarkerFont]; voiceMarkerBaseline: REAL ¬ TiogaVoicePrivate.voiceCharAscent + 2.0 + voiceMarkerExtents.descent; -- 15.0 totalAscent: REAL ¬ voiceMarkerBaseline + voiceMarkerExtents.ascent; -- 27.0 arrowTop: REAL ¬ TiogaVoicePrivate.voiceCharHeight*2; arrowBottom: REAL ¬ TiogaVoicePrivate.voiceCharHeight; arrowTipHeight: REAL ¬ 4.0*TiogaVoicePrivate.voiceCharHeight/3.0; 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, arrowTop]]; lineTo[[0.0, arrowBottom]]; lineTo[[2.5, arrowTipHeight]]; lineTo[[-1.5, arrowTipHeight]]; lineTo[[0.0, arrowBottom]] }; 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, voiceMarkerBaseline]]; Imager.SetFont[context, voiceMarkerFont]; Imager.ShowRope[context, data.label]; }; VoiceMarkerFormat: PROC [ class: TEditFormat.CharacterArtworkClass, loc: TextNode.Location, style: NodeStyle.Style] RETURNS [TEditFormat.CharacterArtwork] ~ { letterX: Char.XCHAR ¬ TextEdit.FetchChar[loc.node, loc.where]; letterE: Char.XCHAR ¬ Char.Make[letterX.Code, TiogaVoicePrivate.voiceCharSet]; label: Rope.ROPE ¬ NARROW[TextEdit.GetCharProp[loc.node, loc.where, $VoiceMark], Rope.ROPE]; escapement: Vector2.VEC ¬ ImagerFont.Escapement[NodeStyle.GetFont[style], letterE]; charWidth: REAL ¬ escapement.x; data: REF VoiceMarkerDataRep ~ NEW[VoiceMarkerDataRep ¬ [ letter: VAL[letterX.Code], label: label, charHeight: TiogaVoicePrivate.voiceCharHeight, charWidth: charWidth ]]; extents: ImagerFont.Extents ¬ [leftExtent: 0.0, rightExtent: 20.0, ascent: totalAscent, descent: TiogaVoicePrivate.voiceCharDescent]; RETURN [NEW[TEditFormat.CharacterArtworkRep ¬ [paint: VoiceMarkerPaint, extents: extents, escapement: escapement, data: data]]] }; voiceMarkerClass: 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; nodeLength: INT; IF voiceViewerInfo = NIL THEN voiceViewerInfo ¬ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF voiceViewerInfo = NIL THEN RETURN; -- not really a voice viewer node ¬ TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]; nodeLength ¬ TiogaOps.GetRope[node].Length; 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]; IF NOT viewer.parent.iconic THEN ViewerForkers.ForkPaint[viewer: viewer.parent, hint: all, tryShortCuts: TRUE]; }; AgeAllViewers: PUBLIC PROC [youngestViewer: ViewerClasses.Viewer] = { FOR vList: TiogaVoicePrivate.VoiceViewerInfoList ¬ TiogaVoicePrivate.GetVoiceViewerInfoList[], vList.rest WHILE vList # NIL DO currInfo: TiogaVoicePrivate.VoiceViewerInfo ¬ vList.first; 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 }; FindEnd: PROC [viewerInfo: TiogaVoicePrivate.VoiceViewerInfo] RETURNS [sample: INT] = { IF viewerInfo.ropeInterval.start # 0 THEN ERROR; { 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] = { FOR vList: TiogaVoicePrivate.VoiceViewerInfoList ¬ TiogaVoicePrivate.GetVoiceViewerInfoList[], vList.rest WHILE vList # NIL DO IF vList.first = viewerInfo THEN RETURN[TRUE]; ENDLOOP; MessageWindow.Append["Voice viewer has been deleted (contents NIL): dictation operation therefore invalid", TRUE]; MessageWindow.Blink[]; RETURN[FALSE]; }; PlayFromSelection: PUBLIC TiogaOps.CommandProc = { v: ViewerClasses.Viewer; start: TiogaOpsDefs.Location; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo; node: TiogaOpsDefs.Ref; from, to: INT; [viewer: v, start: start] ¬ TiogaOps.GetSelection[]; IF v = NIL OR v # viewer OR (viewerInfo ¬ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]) = NIL THEN { MessageWindow.Append["Make a selection in this 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: PUBLIC TiogaOps.CommandProc = { v: ViewerClasses.Viewer; start: TiogaOpsDefs.Location; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo; node: TiogaOpsDefs.Ref; replaceRopeInterval: VoiceRope.VoiceRopeInterval; startDelete, endDelete: INT; [viewer: v, start: start] ¬ TiogaOps.GetSelection[]; IF v = NIL OR v # viewer OR (viewerInfo ¬ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]) = NIL THEN { MessageWindow.Append["Make a selection in this 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: PUBLIC TiogaOps.CommandProc = { 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 ¬ TiogaVoicePrivate.bytesPerChirp/2; SetCriticalSilenceLength: Commander.CommandProc = { argv: CommanderOps.ArgumentVector ¬ CommanderOps.Parse[cmd ! CommanderOps.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: PUBLIC TiogaOps.CommandProc = { viewerInfo: TiogaVoicePrivate.VoiceViewerInfo; selectionToRemove: TiogaVoicePrivate.Selection; startOfSilence, lengthOfSilence: INT; 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]]]]; [] ¬ TiogaVoicePrivate.ReplaceSelectionWithSavedInterval[selection: selectionToRemove, soundInterval: NIL, leaveSelected: FALSE]; TiogaVoicePrivate.MakeVoiceEdited[viewer]; ENDLOOP }; TEditFormat.RegisterCharacterArtwork[voiceMarkerClass]; ageColors[1] ¬ "0.130 1.00 1.00 textColor"; -- vivid orange yellow (vivid yellow is too light to see well) ageColors[2] ¬ "0.060 0.90 1.00 textColor"; -- lightish vivid yellow orange ageColors[3] ¬ "0.000 1.00 1.00 textColor"; -- vivid red ageColors[4] ¬ "0.000 1.00 0.50 textColor"; -- darkish vivid red ageColors[5] ¬ ageColors[6] ¬ "0.000 1.00 0.00 textColor"; -- black; last two must be the same 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, 1988, 1990, 1992 by Xerox Corporation. All rights reserved. Ades, May 1, 1986 10:39:24 am PDT Swinehart, June 7, 1992 11:30 am PDT Polle Zellweger (PTZ) April 23, 1990 6:37:43 pm PDT TiogaVoicePrivate Handle markers in voice, of two types i) a special character which the user can add/delete at a selection using button-pushes ii) annotation of voice with text 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 PROC [viewer: Viewer _ NIL] RETURNS [recordAtom: BOOL _ TRUE, quit: BOOL _ FALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams adds a mark at the position of the current playback cursor 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 PROC [viewer: Viewer _ NIL] RETURNS [recordAtom: BOOL _ TRUE, quit: BOOL _ FALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams 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' Textual markers can be expressed in the Xerox String Encoding, to permit multi-lingual or mathematical annotations via 16-bit Xerox Character Codes. Note that this still does not permit the full power of Tioga (looks, etc.) within textual markers. Xerox String Encoding summary - A string is a sequence of stringlets, each of which has one of the following 3 forms: If no character sets are specified, character set 0 is assumed. Thus an ordinary rope with 8-bit characters has its original interpretation (character code 377B is reserved). Sticky character set form: 377B CharSet1 CharCode1 CharCode2 ... CharCoden 377B CharSet2 CharCode1 ... 16-bit form: 377B 377B 0B CharSet1 CharCode1 CharSet2 CharCode2 CharSet3 CharCode3 ... **** a Plass-bug: shouldn't be necessary and he says he'll have a look at it IF redraw THEN ViewerOps.PaintViewer[viewer: viewer, hint: client, clearClient: TRUE, whatChanged: NIL]; 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 count rope prefix strings rather than individual chars because some chars may be Xerox String Encoding escape chars and may thus not count. PTZ April 17, 1990 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 if there is a textual marker at the selection then remove its last word if there is a textual marker at the selection, do the work to remove its last character or its last word **** a Plass-bug: shouldn't be necessary and he says he'll have a look at it ViewerOps.PaintViewer[viewer: viewer, hint: client, clearClient: TRUE, whatChanged: NIL] Copy text from a text viewer into a voice viewer as an annotation at the insert point. Looks, properties, etc of the characters are discarded. 16-bit Xerox extended charaters are translated into the Xerox string encoding. Copy text from a voice annotation into a text viewer. The Xerox string encoding is translated back into Tioga's CharSets node property notation. XCharProc: TYPE ~ PROC [char: XChar]; A convenience: looks after all the tree walking and calls the given actionProc once for each character in the given range. Returns all text marks in Tioga source selection, concatenated together as a single rope (NOT in Tioga voiceInText property format). Need to acquire voice viewer lock here? if there are textual markers within the selection then remove them completely This is the last text BEFORE the first deleted text. We are guaranteed that this text exists because of the special case made above for the head of the textMarkList. **** a Plass-bug: shouldn't be necessary and he says he'll have a look at it ViewerOps.PaintViewer[viewer: viewer, hint: client, clearClient: TRUE, whatChanged: NIL] 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 now put the textual marks from the soundInterval into the list at the appropriate point no new entries to be inserted, but we must LimitText on the entry previous to the cut 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 Use viewer.parent to try to coalesce this paint with some other; might be better as viewer. 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 -- no longer true: the end point is always the end of the youngest sound in the viewer. PTZ, January 15, 1988 procedure to interpret 'end' for monochrome/color viewers there should be a 'real rope' in this viewer!! IF ~viewerInfo.color THEN RETURN [viewerInfo.ropeInterval.length] ELSE -- This seems like a bad idea to me. PTZ, January 15, 1988 if the supplied viewerInfo does not represent an existing voice viewer (having been deleted due to being empty) display an error message and return FALSE PROC [viewer: Viewer _ NIL] RETURNS [recordAtom: BOOL _ TRUE, quit: BOOL _ FALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams **** need to take the voice lock before performing the following! PROC [viewer: Viewer _ NIL] RETURNS [recordAtom: BOOL _ TRUE, quit: BOOL _ FALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams PROC [viewer: Viewer _ NIL] RETURNS [recordAtom: BOOL _ TRUE, quit: BOOL _ FALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams routines that allow the lengths of silent portions within a rope to be adjusted PROC [viewer: Viewer _ NIL] RETURNS [recordAtom: BOOL _ TRUE, quit: BOOL _ FALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams 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 one 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 Polle Zellweger (PTZ) May 28, 1987 4:37:10 pm PDT Red button `Mark' => mark the current playback spot; otherwise mark the selection as before. changes to: AddCharMark, AddMarkAtPlaybackLocation (new), AddMarksAtSelection (new) Polle Zellweger (PTZ) June 4, 1987 11:18:46 am PDT Make BackSpace and BackWord have their normal Tioga functions for text markers, removing the text marker completely whenever its length goes to 0. [BackWord (^W) was being used to remove text markers completely rather than for its usual purpose.] The RemoveTextMarkers function is available to remove all text markers quickly from a voice selection (no current user interface access). changes to: DIRECTORY, VoiceMarkersImpl, Back, AddArtwork (local of Back), BackProc, BackSpaceProc, BackWordProc, BackSpace, BackWord, RemoveTextMarkers Polle Zellweger (PTZ) June 4, 1987 11:27:47 am PDT Try going without ViewerOps.PaintViewer (maybe the Plass-bug is fixed now). changes to: TextInput, Back, RemoveTextMarkers Polle Zellweger (PTZ) June 7, 1987 7:40:05 pm PDT Always call TiogaVoicePrivate.SetVoiceViewerEditStatus now that ViewerOps.PaintViewer has been removed. Sets the icon to dirty. changes to: TextInput, Back, RemoveTextMarkers Polle Zellweger (PTZ) June 17, 1987 3:28:06 pm PDT Call TiogaVoicePrivate.MakeVoiceEdited to set the icon to dirty, etc. whenever voice viewer contents have been edited. changes to: DIRECTORY, TextInput, Back, RemoveTextMarkers, AdjustSilences Polle Zellweger (PTZ) July 10, 1987 5:48:48 pm PDT Make voiceViewerInfoList into a real list; make private to VoiceViewersImpl. changes to: DIRECTORY, AgeAllViewers Polle Zellweger (PTZ) July 20, 1987 5:09:26 pm PDT Change UI: yellow button Mark = add at playback loc; red, blue = add at selection; minor logic improvement. changes to: AddCharMark, AddMarkAtPlayBackLocation Polle Zellweger (PTZ) December 29, 1987 3:42:25 pm PST Copy a voice segment with >1 annotations => only 1st annotation copied (1st one was copied repeatedly to the same location). changes to: EditTextMarks Polle Zellweger (PTZ) December 30, 1987 1:25:13 pm PST Tune colors a bit: make final color black, make previous color an in-between brown. changes to: ageColors Polle Zellweger (PTZ) January 11, 1988 2:43:41 pm PST Changes to support move to PopUpButtons. Removed DictationOps menu line, moved all voice viewer UI to VoiceViewersImpl => dictation fns must be PUBLIC. changes to: PlayFromSelection, ResumeFromSelection, ResumeFromEnd, AdjustSilences, initialization (deleted DictationOps menu line creation) Polle Zellweger (PTZ) January 13, 1988 3:45:16 pm PST Inhibit PlayFromSelection and ResumeFromSelection if selection is not in this viewer. Also MarkAtSelection and DeleteSelectionMarks. changes to: PlayFromSelection, ResumeFromSelection, AddCharMark, AddMarksAtSelection, DeleteCharMarks Polle Zellweger (PTZ) January 19, 1988 10:04:10 am PST Trying to remove extra painting. changes to: DIRECTORY, ReColorViewer Polle Zellweger (PTZ) January 22, 1988 11:45:43 am PST Protect against calling TextEdit.PutCharProp with nonexistent node or character. changes to: TextInput Swinehart, April 30, 1988 4:03:01 pm PDT Get bytesPerChirp from TiogaVoicePrivate, not Bluejay changes to: DIRECTORY, criticalSilenceLength Polle Zellweger (PTZ) September 13, 1988 9:14:12 pm PDT Changes to allow clients to alter button behaviors via Tioga registry (for WalnutTiogaVoice). changes to: AddCharMark, DeleteCharMarks, PlayFromSelection, ResumeFromSelection, ResumeFromEnd, AdjustSilences Polle Zellweger (PTZ) April 16, 1990 4:32:52 pm PDT Make it possible to display Xerox extended character sets as markers in voice viewers, using Xerox string encoding. Permits multi-lingual or mathematical textual annotations, but not the full power of Tioga (looks, character properties, etc.) in annotations. changes to: DIRECTORY, LimitText, voiceMarkerFont, voiceMarkerExtents, voiceMarkerBaseline, totalAscent, arrowTop, arrowBottom, arrowTipHeight, VoiceMarkerDataRep, DrawArrow, VoiceMarkerPaint, VoiceMarkerFormat Polle Zellweger (PTZ) April 21, 1990 6:22:36 pm PDT Allow copying text between voice viewers and text viewers. changes to: DIRECTORY, TextInput new: start16Bits, stop16Bits, CopyTextViewerToTextMarker, CopyTextMarkerToTextViewer, SourceSel, ApplyToChars, GetTextMarksFromSelection Ê/q•NewlineDelimiter –(cedarcode) style™codešœ™Kšœ ÏeœC™NKšœÏk™!K™$Kšœžœ™3K™—šž ˜ Kšœžœžœ˜-Kšœžœ˜Kšœ žœ˜(Kšœ žœ!˜3Kšœžœ˜)Kšœžœr˜~Kšœ žœ^˜nKšœ žœ ˜Kšœžœ˜$Kšœ žœ˜!Kšœžœ˜Kšœžœ.žœ ˜FKšœ žœu˜†Kšœ žœ<˜JKšœ žœ˜Kšœ žœ‚˜Kšœ žœ!˜3Kšœžœ0Ïrœß˜»Kšœžœžœ˜Kšœžœ˜*Kšœžœ ˜ Kšœ žœ ˜Kšœ žœ˜)Kšœ˜K™—Kš ÐlnœžœžœžœÄžœ˜ˆK˜šÐbl™K™™%K™WK™!K™«—K˜šœZ™ZK™—šÏn œžœ˜,Kšžœžœžœžœžœžœžœ=™Kšœ/žœ-˜bKš œ)žœžœžœžœ˜ešžœ ž˜Kšœ+˜+Kšœ'˜'Kšžœ˜—˜K˜——š¢œžœ˜#K™:Kšœžœ˜#K˜.Kšœ žœžœ˜Kšœ žœ˜K˜šžœ(žœ˜0K˜JK˜—šžœžœ˜KšœQžœ˜WKšœ˜Kšžœ˜K˜—Kšœ žœS˜fš žœžœ žœžœžœ˜-KšœEžœ˜KKšœ˜Kšžœ˜K˜—šžœ,žœ˜4Kšœ!˜!šœžœ6˜PKšœWžœ ˜iKšœ3˜3Kšœž˜!—K˜Kšœ˜—˜K˜——š¢œžœ#˜šœ˜K˜>Kšœ žœN˜aš žœžœžœ žœž œ˜3KšœDžœ˜JKšœ˜Kšž˜K˜—K˜Kšžœ,ž˜2šœ˜Kšžœžœ?žœžœ˜iKšœ(˜(šœžœ6˜PKšœWžœ ˜iKšœ3˜3Kšœž˜!—K˜—Kšœ˜—K˜šœ˜K˜——š¢œžœžœ˜Kšœžœžœžœžœ žœžœžœ˜gKšœ÷™÷Kšœ žœ˜'Kšœ žœ˜Kšœ žœ˜Kšœ žœ˜Kš žœžœžœ žœ"žœ˜išž˜Kš œ žœžœžœ žœ ˜VK˜UKšžœžœžœ˜&Kšœ%£˜K˜—Kšžœžœžœ9˜\Kšž˜šœžœA˜SKšœžœ&˜DKš œžœ žœžœžœžœžœ?˜Kšœ.˜.Kšœ ž˜ —Kšœ˜Kšž˜šœžœ9˜>Kšž˜˜-K˜/K˜.Kš œžœ žœžœžœžœžœ?˜Kšžœ0žœ:žœ˜v—K˜Kšž˜šœžœžœ;˜HKš žœ žœžœ(žœ žœ˜TK˜Kšžœ žœžœ'˜9Kšžœ£7˜<šœ žœ˜!K˜Kšœ:˜:Kšžœžœ:žœ˜bKšœ žœA˜PKšœ žœ˜!—K˜Kšžœ£+˜0˜K˜.K˜.—K˜K˜Kšœ £"˜.Kš œžœ žœžœžœžœžœ)˜aKšžœ0žœ:žœ˜u—K˜—K˜K˜K™LKšžœžœBžœžœ™hK˜Kšœ*˜*Kšœžœ˜"˜K˜——š¢ œžœ5žœ˜LKšœ•™•Kšœžœžœ-˜GKšœ žœ˜&Kšœ žœ˜Kšœžœ˜šžœž˜šœžœS˜fK™ž—Kšžœžœžœ˜+K˜K˜—Kšžœ˜K˜Kšœ®™®Kšžœžœ,™HK™K˜pK˜˜K™——š¢ œžœžœ#˜9K™LK˜˜K˜——š¢œžœžœ#˜8K™GK˜˜K˜——š œ žœžœ.žœžœ˜VK˜—š¢ œ˜K˜"K˜K˜—š¢ œ˜K˜šžœžœžœ3ž˜HK˜ Kšžœ˜—šžœžœ3ž˜DK˜ Kšžœ˜—K˜K˜—š¢œžœ9˜CK™hKšœ˜K˜+K˜š¢ œžœ˜-Kšœ9žœžœžœžœžœžœžœ£/˜±Kš œ;žœžœžœžœžœ3˜“K˜—K˜Kšœ&˜&Kšœ(˜(Kšœ%žœ˜)Kšœ žœžœ#žœ˜7Kšœ žœ˜K˜.K˜fKšžœžœžœ˜'Kšœ žœS˜fKšžœ-žœžœ˜;K˜Kšœžœ žœžœ˜*K˜K˜K˜Kšœ žœ˜š žœžœžœCžœžœžœ˜bKšžœ$ž˜*šœžœ#žœ˜-˜Kš œžœ žœžœžœžœžœ'˜`—K˜Kšž˜—K˜K˜ —šžœ˜K˜—Kšžœ žœ˜KšžœEžœ˜Nšžœ˜Kšœžœ˜-Kšœžœ˜!Kš œžœžœžœžœ˜Gšž˜Kšœ*˜*šžœ)žœ˜1Kšœ#˜#K™LKšœAžœžœ™XK˜—šžœžœžœ£'˜Cšžœ žœž˜K˜6—šžœ£,˜4K˜!šžœžœžœž˜'K˜X—K˜Kšžœ˜K˜——Kšžœ˜Kšžœ˜—Kšœ3˜3K˜—˜Kšœžœ˜"—˜K˜——Kšœžœ Ïi˜0šœžœ Ðci¥˜&K˜—š¢œž œ2˜YK™ßKšœ žœžœ˜Kšœ žœžœ˜K˜š¢œžœ!˜Kšœž˜%——šžœ˜Kš žœžœžœžœžœ˜Qšž˜K˜2K˜'šžœžœ˜!š žœžœžœžœž˜2Kšœžœ˜&—Kšž˜K˜—šž˜Kš žœžœžœžœžœ˜E—Kšžœ˜—K˜—K˜K˜—š ¢œžœ/žœžœžœ˜vK™„Kšœ"˜"Kšœ"˜"K˜.K˜NKšžœ žœžœ žœžœ žœžœžœ˜AKšžœžœžœ£˜>Kšœ žœX˜kK™'Kš žœ žœžœžœ£*˜Jš žœžœžœCžœžœžœž˜ƒšžœ!ž˜'K˜1—Kšžœ˜—K˜K˜—š¢œžœžœ#˜AKšœM™MKšœ˜K˜+K˜š¢ œžœ˜-Kšœ9žœžœžœžœžœžœžœ£/˜±Kš œ;žœžœžœžœžœ3˜“—K˜K˜Kšœ&˜&Kšœ"˜"Kšœ žœžœ˜K˜.K˜LKšžœžœžœ˜'Kšžœžœžœ˜$Kšœ žœS˜fK˜K˜Kšžœ-žœžœ˜;Kšžœžœž˜%šœžœ˜%Kšž˜—K˜K˜Kšžœ7˜9šžœ£0˜5šžœžœžœ5ž˜^K˜*Kšœžœ˜Kšœ#˜#Kšœžœ˜K˜6—Kšž˜—Kšž˜šœžœžœ;˜HKš žœ žœžœ%žœ žœ˜Qšžœ žœžœ$ž˜Kšžœ&˜*Kšž˜˜Kš žœ žœžœ'žœ žœ˜SK˜&K˜—K˜—K˜šœžœžœ>˜VKšœ žœžœ9žœ˜TK˜FK˜K˜#šžœžœž˜Kšœžœžœ9žœ˜_K˜)K˜RK˜#—Kšžœ˜K˜—K˜K˜$Kš œ"žœžœžœžœžœžœ?˜“K˜Kšžœžœžœ%˜?šžœ˜K˜KšœV˜VK˜—K˜—šžœ˜KšœU™Ušžœžœžœ8žœ˜bKšœ žœžœ;˜MKš žœžœžœ/žœžœ˜sKš œžœžœžœžœžœžœ:˜€K˜—Kšœ˜—˜K˜——š¢œžœžœ˜Kš œžœžœ"žœ žœžœ˜LK™Ššžœžœž˜Kšœžœ˜$Kšœžœ:˜PK˜cK˜K˜ —Kšž˜K˜—š¢œžœžœ žœ˜,Kšžœžœžœ#žœ˜>K™Kšœ žœžœ!˜3K˜šžœž˜Kšœ žœ˜Kšœ,žœvžœ ˜±Kšœ žœ˜K˜K˜K˜9K˜K˜(K˜K˜Kšžœž˜ Kšž˜šœžœ žœ˜K˜ —K˜Kšž˜šœžœ žœ˜(K˜—K˜—Kšžœ˜K˜š žœžœžœ0žœžœž˜TKš œžœ žœžœžœžœžœ0˜l—Kšž˜K˜—K˜—š¡ ™ K™K™…Kšœ·™·K™šœžœL˜cK˜—K˜UKšœžœJ£˜jKšœ žœ5£˜MKšœ žœ'˜5Kšœ žœ%˜6šœžœ-˜AK˜—šœžœžœ˜#Kšœžœ˜ Kšœ žœ˜Kšœ žœ˜Kšœ ž˜Kšœ˜K˜—š¢œžœ˜Kšœ@˜@Kšœžœžœ ˜1K˜š¢ œ˜"Kšœ˜Kšœ˜Kšœ˜Kšœ˜Kšœ˜—K˜K˜Kšœ˜Kšœ&˜&Kšœ'˜'Kšœ$˜$Kšœ&˜&Kšœ&žœ˜,Kšœ2˜2Kšœ)˜)Kšœ%˜%K˜K˜—š¢œžœ˜KšœZžœ#˜„Kšœžœ+˜>K˜NKšœ žœžœ=žœ˜\Kšœžœ<˜TKšœ žœ˜K˜šœžœžœ˜9Kšœžœ˜K˜ Kšœ.˜.Kšœ˜—Kšœ˜K˜…Kšžœžœt˜Kšœ˜K˜—šœ5˜5Kšžœ)˜,Kšœ˜Kšœ˜Kšœž˜ Kšœ˜—K™—š¡ ™ K™K™»K™ÌK™ËKšœ”™”KšœX™XK™Kšœžœžœ˜šœ žœžœ˜KšœV£™o—Kšœ žœžœžœ˜%K˜š¢ œžœžœ˜KšœSžœ žœžœ˜uK™ŸKšœ˜Kšœ žœ˜šžœžœž˜KšœžœS˜k—Kš žœžœžœžœ£˜CK˜K˜7Kšœ žœ ˜+K˜K˜.Kš žœžœžœžœžœ˜GK˜Kšœ(žœ£˜Qš žœžœžœ=žœžœž˜[Kš œPžœ žœžœžœ6žœ˜¿Kšžœ˜—K˜Kšœ3˜3K˜K™;šžœžœžœ˜!KšœHžœ˜NK™[—˜K˜K˜——š¢ œžœžœ+˜EK™>šžœgžœ žœž˜~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˜—šžœžœžœž˜,šžœ#žœ˜.Kšœžœ˜”K˜A—Kšžœ žœ˜—šžœ˜K˜—Kšžœžœžœ˜!˜š žœžœžœ žœžœ£[˜}šžœžœ˜"šœ ˜ K˜KK˜ —K˜)—Kšžœ˜—Kšžœ˜—˜˜K˜——KšžœžœžœB˜`Kšž˜š œžœžœžœAžœžœD˜ÄKšžœžœžœ2˜c—Kšœ˜Kšž˜˜Kš žœ žœžœ5žœ žœ˜aKš žœ žœžœ5žœ žœ8˜‘Kšžœžœ žœ&˜K—Kšœ˜K˜Kšžœ žœžœžœžœžœžœ˜nK˜K˜š žœžœžœ žœžœ£[˜}Kšžœ žœžœ ˜K—Kšž˜Kšœ˜—K˜—š¡ ™ K™K™¡K™šÏyÞœp™ÎK™—š¢œžœ1žœ žœ˜WKšœ9™9Kšžœ#žœžœ˜0K™.Kšžœžœžœ!™AKšžœ<™@šœ žœ ˜Kšœ žœžœžœ˜š žœžœžœ8žœžœž˜VKšžœžœ˜š œžœžœžœžœžœžœ˜FK˜—Kšœ˜—Kšžœ˜Kš žœžœ žœžœžœ žœ0˜x—K˜˜K˜——š¢œžœ1žœžœ˜bKšœ”ž™™šžœgžœ žœž˜~Kšžœžœžœžœ˜.Kšž˜—Kšœ>Ïsœ+žœ˜rKšœ˜Kšžœžœ˜˜K˜——š¢œžœ˜3Kšžœžœžœžœžœžœžœ=™Kšœ˜Kšœ˜K˜.Kšœ˜Kšœ žœ˜K˜K˜4šžœžœžœ žœžœVžœžœ˜KšœDžœ˜JKšœ˜Kšž˜K˜—K˜Kšœ/˜/Kšœ#˜#Kšœ"˜"Kšžœ%žœžœ˜3K˜K™AK˜7K˜K˜?Kšžœ ˜Kšžœ:˜>Kšž˜šœSžœ˜YKšœ˜—Kšœ˜šœ˜K˜——š¢œžœ˜5Kšžœžœžœžœžœžœžœ=™Kšœ˜Kšœ˜K˜.Kšœ˜Kšœ1˜1Kšœžœ˜K˜K˜4šžœžœžœ žœžœVžœžœ˜KšœDžœ˜JKšœ˜Kšž˜K˜—K˜Kšœ/˜/Kšœ#˜#Kšœ"˜"Kšžœ%žœžœ˜3Kšžœ-žœžœ˜;K˜K˜7K˜@K˜ Kšžœž˜šœSžœ˜YKšœ˜Kšœžœ˜"Kšž˜—Kšœ˜K˜K˜tK˜Kšœ+žœ‡˜µ˜K˜——š¢ œžœ˜.Kšžœžœžœžœžœžœžœ=™Kšœ0žœS˜‰Kšœ˜Kšœ1˜1K˜Kšœ/˜/Kšœ#˜#Kšœ"˜"Kšžœ%žœžœ˜3K˜Kšžœ-žœžœ˜;K˜K˜7K˜fK˜Kšœ+žœ‡˜µšœ˜K™——™OK™—šœžœ%˜?K™—š¢œ˜3Kšœežœ˜qšžœžœ˜K˜=Kšžœ˜ K˜—K˜K˜7Kšž˜K˜Kšž˜Kšœžœ˜˜K˜——š¢œžœ˜/Kšžœžœžœžœžœžœžœ=™K˜.Kšœ/˜/Kšœ!žœ˜%K˜Kšœ žœS˜fKšžœ-žœ˜4šœ˜Kšœežœ˜kKšœ˜Kšž˜—Kšœ˜K˜K˜šž˜K˜©Kšžœžœ˜šœžœ˜%K˜Kšž˜—Kšœ˜K˜Kšœžœ¥˜¼—˜Kšœfžœžœ˜K™¿Kšœ™Kšœ*˜*—Kšž˜˜K˜———š¡™K™Kšœ7˜7Kšœ-£>˜kKšœ-£˜LKšœ-£ ˜9Kšœ-£˜Ašœ;£#˜^Kšœ žœK™XK™—Kšœ€˜€K˜—Kšœ˜K˜šœžœž™1K™\Kšœ Ÿ&œŸœ™S—™2KšœýŸœt™‚Kšœ Ðrs Ÿ$œŸO™˜—™2KšœK™KKšœ Ÿ"™.—™1Kšœ€™€Kšœ Ÿ"™.—™2Kšœv™vKšœ ª Ÿ5™J—™2KšœL™LKšœ Ðkr Ÿ™%—™2K™kKšœ Ÿ&™2—™6Kšœ|™|Kšœ Ÿ ™—™6K™SKšœ Ÿ ™—™5K™˜Kšœ ŸVœ)™‹—™5Kšœ…™…Kšœ ŸY™e—™6K™ Kšœ ª Ÿ™$—™6K™PKšœ Ÿ ™—™(K™5Kšœ ª Ÿ™,—™7K™]Kšœ Ÿc™o—™3K™ƒKšœ ª Ÿ½™Ò—™3K™:Kšœ ª Ÿ ™ KšœŸƒ™ˆ——…—°Q