<> <> <> <> <> <<>> 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 [Escapement, Extents, FindScaled, Font, FontBoundingBox, MapRope, RopeEscapement, XCharProc], ImagerPath USING [PathProc], MessageWindow USING [Append, Blink], NodeStyle USING [Ref], NodeStyleOps USING [OfStyle], Real USING [Fix], Rope USING [Cat, Concat, Fetch, Find, FromChar, Length, ROPE, Substr], RopeEdit USING [AlphaNumericChar], TEditFormat USING [CharacterArtwork, CharacterArtworkRep, CharacterArtworkClass, CharacterArtworkClassRep, RegisterCharacterArtwork, GetFont], TextEdit USING [CharSet, FetchChar, GetCharProp, InsertChar, PutCharProp, Size], TextNode USING [Location, Ref], TiogaButtons USING [TextNodeRef, TiogaOpsRef], 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 Commander, CommandTool, Convert, Imager, ImagerFont, MessageWindow, Real, Rope, RopeEdit, TEditFormat, TextEdit, TiogaButtons, 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 } }; <> <<>> <> <> <> <> <> <<16-bit form: 377B 377B 0B CharSet1 CharCode1 CharSet2 CharCode2 CharSet3 CharCode3 ...>> 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 _ TiogaButtons.TextNodeRef[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} } }; <<**** a Plass-bug: shouldn't be necessary and he says he'll have a look at it>> <> 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 RopeEdit.AlphaNumericChar[textEntry.text.Fetch[len-1]] DO len _ len-1; ENDLOOP; WHILE len>0 AND RopeEdit.AlphaNumericChar[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 _ 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 }; 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]; <<**** a Plass-bug: shouldn't be necessary and he says he'll have a look at it>> <> }; 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 _ "ÿÿ stop16Bits: Rope.ROPE _ "ÿ CopyTextViewerToTextMarker: PUBLIC PROC [target: TiogaOpsDefs.WhichSelection_primary] ~ { <> use16Bits: BOOL _ FALSE; text: Rope.ROPE _ NIL; ToXeroxStringEncoding: PROC [loc: TiogaOpsDefs.Location] ~ { charSet: TextEdit.CharSet; char: CHAR; [charSet, char] _ TextEdit.FetchChar[TiogaButtons.TextNodeRef[loc.node], loc.where]; 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.InsertChar[root: textRoot, dest: insertNode, destLoc: insertPosition, char: 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 _ TiogaButtons.TextNodeRef[TiogaOps.ViewerDoc[textViewer]]; IF caretBefore THEN { insertNode _ TiogaButtons.TextNodeRef[start.node]; insertPosition _ start.where; } ELSE { insertNode _ TiogaButtons.TextNodeRef[end.node]; insertPosition _ end.where+1; }; text _ GetTextMarksFromSelection[SourceSel[target]]; TiogaOps.CallWithLocks[InsertMarks, TiogaButtons.TiogaOpsRef[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[TiogaButtons.TextNodeRef[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[TiogaButtons.TextNodeRef[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 _ 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 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]; <<**** a Plass-bug: shouldn't be necessary and he says he'll have a look at it>> <> }; viewerInfo.editInProgress _ FALSE }; RedrawTextMarkers: PUBLIC PROC [ viewer: ViewerClasses.Viewer, voiceCharNode: TiogaOpsDefs.Ref] = { <> node: TextNode.Ref _ TiogaButtons.TextNodeRef[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.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: 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 ]]; <<>> <> <<>> <> <> <> <> <youngest>> <<>> 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 _ TiogaButtons.TextNodeRef[TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]]; nodeLength _ TiogaOps.GetRope[TiogaButtons.TiogaOpsRef[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]; <<**** I still haven't got right when to repaint and when not>> 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; <<**** need to take the voice lock before performing the following!>> 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: 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: 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"]; }. <> < mark the current playback spot; otherwise mark the selection as before.>> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <1 annotations => only 1st annotation copied (1st one was copied repeatedly to the same location).>> <> <> <> <> <> < dictation fns must be PUBLIC.>> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <>