DIRECTORY Convert USING [RopeFromInt, IntFromRope], ImagerFont USING [Find, Font, Width], Menus USING [MenuProc], MessageWindow USING [Append, Blink], Real USING [Fix], Rope USING [ROPE, Length, Substr, Concat, Fetch, Cat, Find], SoundList USING [SoundChars], TextEdit USING [PutCharProp], TextNode USING [Ref], TiogaButtons USING [TextNodeRef], TiogaOps USING [GetSelection, FirstChild, ViewerDoc, CallWithLocks], TiogaOpsDefs USING [Location, Ref], ViewerClasses USING [Viewer], ViewerOps USING [FetchProp, PaintViewer], VoicePlayBack USING [RedrawViewer], VoiceViewers USING [VoiceViewerInfo, GetVoiceLock, SetVoiceViewerEditStatus, TextMarkEntry, TextMarkRec, SoundInterval, soundRopeCharLength], VoiceMarkers; VoiceMarkersImpl: CEDAR PROGRAM IMPORTS Convert, ImagerFont, MessageWindow, Real, Rope, SoundList, TextEdit, TiogaButtons, TiogaOps, ViewerOps, VoicePlayBack, VoiceViewers, VoiceMarkers EXPORTS VoiceMarkers = BEGIN AddCharMark: PUBLIC Menus.MenuProc = { viewer: ViewerClasses.Viewer; start, end: TiogaOpsDefs.Location; pendingDelete, caretBefore: BOOLEAN; viewerInfo: VoiceViewers.VoiceViewerInfo; [viewer: viewer, start: start, end: end, pendingDelete: pendingDelete, caretBefore: caretBefore] _ TiogaOps.GetSelection[]; viewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], VoiceViewers.VoiceViewerInfo]; IF viewerInfo = NIL THEN { MessageWindow.Append["Make a selection in a voice viewer first", TRUE]; MessageWindow.Blink[]; RETURN }; IF VoiceViewers.GetVoiceLock[viewerInfo] THEN { IF ~ (start.node = end.node AND start.node = TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]) THEN ERROR; IF pendingDelete OR caretBefore THEN MarkChar[viewerInfo, start.where]; IF pendingDelete OR ~caretBefore THEN MarkChar[viewerInfo, end.where]; { trueContents: Rope.ROPE _ SoundList.SoundChars[viewerInfo].soundRope; [] _ VoicePlayBack.RedrawViewer[viewer, trueContents, 0, 0, 0, viewerInfo.remnant, FALSE, unAltered]; VoiceViewers.SetVoiceViewerEditStatus[viewer]; viewerInfo.editInProgress _ FALSE } } }; MarkChar: PROC [viewerInfo: VoiceViewers.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: VoiceViewers.VoiceViewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], VoiceViewers.VoiceViewerInfo]; IF viewerInfo = NIL THEN ERROR; MarkChar[viewerInfo, position]; { trueContents: Rope.ROPE _ SoundList.SoundChars[viewerInfo].soundRope; [] _ VoicePlayBack.RedrawViewer[viewer, trueContents, 0, 0, 0, viewerInfo.remnant, FALSE, unAltered]; VoiceViewers.SetVoiceViewerEditStatus[viewer]; } }; DeleteCharMarks: PUBLIC Menus.MenuProc = { viewer: ViewerClasses.Viewer; start, end: TiogaOpsDefs.Location; viewerInfo: VoiceViewers.VoiceViewerInfo; RemoveMarkChars: PROC [from, to: INT] = { WHILE viewerInfo.charMarkList # NIL AND viewerInfo.charMarkList.first IN [from..to] DO viewerInfo.charMarkList _ viewerInfo.charMarkList.rest ENDLOOP; IF viewerInfo.charMarkList # NIL THEN FOR l: LIST OF INT _ viewerInfo.charMarkList, l.rest WHILE l # NIL AND l.rest # NIL DO IF l.rest.first IN [from..to] THEN l.rest _ l.rest.rest ENDLOOP -- the dual WHILE condition is because we can delete the next entry and it could be the last }; IF mouseButton = blue THEN -- right click means delete all marks in the viewer { viewer _ NARROW[parent, ViewerClasses.Viewer]; viewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], VoiceViewers.VoiceViewerInfo]; IF VoiceViewers.GetVoiceLock[viewerInfo] THEN { viewerInfo.charMarkList _ NIL; { trueContents: Rope.ROPE _ SoundList.SoundChars[viewerInfo].soundRope; [] _ VoicePlayBack.RedrawViewer[viewer, trueContents, 0, 0, 0, viewerInfo.remnant, FALSE, unAltered]; VoiceViewers.SetVoiceViewerEditStatus[viewer]; viewerInfo.editInProgress _ FALSE } } } ELSE -- both other clicks mean delete all marks from selection { [viewer: viewer, start: start, end: end] _ TiogaOps.GetSelection[]; viewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], VoiceViewers.VoiceViewerInfo]; IF viewerInfo = NIL THEN { MessageWindow.Append["Make a selection in a voice viewer first", TRUE]; MessageWindow.Blink[]; RETURN }; IF VoiceViewers.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 _ SoundList.SoundChars[viewerInfo].soundRope; [] _ VoicePlayBack.RedrawViewer[viewer, trueContents, 0, 0, 0, viewerInfo.remnant, FALSE, unAltered]; VoiceViewers.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: VoiceViewers.VoiceViewerInfo, soundInterval: VoiceViewers.SoundInterval] = { currLastInList: LIST OF INT; soughtStart: INT _ soundInterval.ropeInterval.start/VoiceViewers.soundRopeCharLength; soughtEnd: INT _ (soundInterval.ropeInterval.start + soundInterval.ropeInterval.length)/VoiceViewers.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: VoiceViewers.VoiceViewerInfo, unchangedHead, deleteChars, insertChars: INT, soundInterval: VoiceViewers.SoundInterval] = { atHead: BOOLEAN _ TRUE; l: LIST OF INT; WHILE atHead AND viewerInfo.charMarkList # NIL DO SELECT viewerInfo.charMarkList.first FROM >= unchangedHead+deleteChars => {viewerInfo.charMarkList.first _ viewerInfo.charMarkList.first + insertChars - deleteChars; atHead _ FALSE}; >= unchangedHead => viewerInfo.charMarkList _ viewerInfo.charMarkList.rest; ENDCASE => atHead _ FALSE; ENDLOOP; IF viewerInfo.charMarkList # NIL THEN { l _ viewerInfo.charMarkList; WHILE l # NIL AND l.rest # NIL DO --the dual WHILE condition is because we can delete the next entry and it could be the last SELECT l.rest.first FROM >= unchangedHead+deleteChars => {l.rest.first _ l.rest.first + insertChars - deleteChars; l _ l.rest}; >= unchangedHead => l.rest _ l.rest.rest; ENDCASE => l _ l.rest ENDLOOP }; IF soundInterval # NIL AND soundInterval.charMarkList # NIL THEN -- now put the character marks from the soundInterval into the list at the appropriate point { beforeInsert, afterInsert: LIST OF INT _ NIL; newSection, endNewSection: LIST OF INT; IF viewerInfo.charMarkList # NIL THEN { IF viewerInfo.charMarkList.first >= unchangedHead THEN afterInsert _ viewerInfo.charMarkList ELSE { l _ viewerInfo.charMarkList; WHILE l.rest # NIL AND l.rest.first < unchangedHead DO l _ l.rest ENDLOOP; beforeInsert _ l; afterInsert _ l.rest } }; { insertMarks: LIST OF INT _ soundInterval.charMarkList.rest; newSection _ CONS [soundInterval.charMarkList.first + unchangedHead, NIL]; endNewSection _ newSection; WHILE insertMarks # NIL DO endNewSection.rest _ CONS [soundInterval.charMarkList.first + unchangedHead, NIL]; endNewSection _ endNewSection.rest; insertMarks _ insertMarks.rest ENDLOOP }; endNewSection.rest _ afterInsert; IF beforeInsert = NIL THEN viewerInfo.charMarkList _ newSection ELSE beforeInsert.rest _ newSection } }; TextInput: PUBLIC PROC [viewer: ViewerClasses.Viewer, input: Rope.ROPE] = { node: TextNode.Ref; textEntry: VoiceViewers.TextMarkEntry; AddArtwork: PROC [root: TiogaOpsDefs.Ref] = { TextEdit.PutCharProp[node, textEntry.position, $Artwork, NARROW["VoiceMarker", Rope.ROPE]]; -- the NARROW prevents a REF TEXT being created TextEdit.PutCharProp[node, textEntry.position, $VoiceMark, textEntry.text.Substr[0, textEntry.displayChars]]; }; selectionViewer: ViewerClasses.Viewer; start, end, char: TiogaOpsDefs.Location; insertPosition: INT; caretBefore: BOOLEAN; viewerInfo: VoiceViewers.VoiceViewerInfo; charsAlreadyDisplayed: INT _ 0; redraw: BOOLEAN _ FALSE; [viewer: selectionViewer, start: start, end: end, caretBefore: caretBefore] _ TiogaOps.GetSelection[]; IF selectionViewer # viewer THEN ERROR; viewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], VoiceViewers.VoiceViewerInfo]; IF ~VoiceViewers.GetVoiceLock[viewerInfo] THEN RETURN; char _ IF caretBefore THEN start ELSE end; node _ TiogaButtons.TextNodeRef[char.node]; insertPosition _ char.where; IF viewerInfo.textMarkList = NIL OR viewerInfo.textMarkList.first.position > insertPosition THEN { textEntry _ NEW [VoiceViewers.TextMarkRec _ [insertPosition, input, 0, 0]]; viewerInfo.textMarkList _ CONS [textEntry, viewerInfo.textMarkList]; LimitText[textEntry, IF viewerInfo.textMarkList.rest = NIL THEN LAST[INT] ELSE viewerInfo.textMarkList.rest.first.position - insertPosition]; TiogaOps.CallWithLocks[AddArtwork]; redraw _ TRUE } ELSE { IF viewerInfo.textMarkList.first.position = insertPosition THEN { textEntry _ viewerInfo.textMarkList.first; charsAlreadyDisplayed _ textEntry.displayChars; textEntry.text _ textEntry.text.Concat[input]; LimitText[textEntry, IF viewerInfo.textMarkList.rest = NIL THEN LAST[INT] ELSE viewerInfo.textMarkList.rest.first.position - insertPosition]; IF textEntry.displayChars # charsAlreadyDisplayed THEN {TiogaOps.CallWithLocks[AddArtwork]; redraw _ TRUE} } ELSE { l: LIST OF VoiceViewers.TextMarkEntry _ viewerInfo.textMarkList; WHILE l.rest # NIL AND l.rest.first.position < insertPosition DO l _ l.rest ENDLOOP; IF l.rest = NIL OR l.rest.first.position > insertPosition THEN -- insert a new entry: previous entry may need trimming { oldWidth: INT _ l.first.width; textEntry _ l.first; LimitText[textEntry, insertPosition - textEntry.position]; IF textEntry.width # oldWidth THEN {TiogaOps.CallWithLocks[AddArtwork]; redraw _ TRUE}; textEntry _ NEW [VoiceViewers.TextMarkRec _ [insertPosition, input, 0, 0]]; l.rest _ CONS [textEntry, l.rest] } ELSE -- appending text to an entry already there { textEntry _ l.rest.first; textEntry.text _ textEntry.text.Concat[input]; charsAlreadyDisplayed _ textEntry.displayChars }; l _ l.rest; -- point at the new/modified entry LimitText[textEntry, IF l.rest = NIL THEN LAST[INT] ELSE l.rest.first.position - insertPosition]; IF textEntry.displayChars # charsAlreadyDisplayed THEN {TiogaOps.CallWithLocks[AddArtwork]; redraw _ TRUE} } }; IF redraw THEN ViewerOps.PaintViewer[viewer: viewer, hint: client, clearClient: TRUE, whatChanged: NIL]; viewerInfo.edited _ TRUE; IF ~redraw THEN VoiceViewers.SetVoiceViewerEditStatus[viewer]; -- not needed if the redraw was performed, as viewer's displayed status will be `edited' already viewerInfo.editInProgress _ FALSE }; LimitText: PROC [entry: VoiceViewers.TextMarkEntry, maxLength: INT] = { maxEscapement: REAL _ REAL[maxLength]*VoiceMarkers.voiceCharWidth; textLength: INT _ entry.text.Length; currPos: INT _ 0; currEscapement: REAL _ 0.0; WHILE currPos < textLength DO newEscapement: REAL _ currEscapement + ImagerFont.Width[VoiceMarkers.voiceMarkerFont, [VoiceMarkers.voiceCharSet, entry.text.Fetch[currPos]-'\000]].x; IF newEscapement > maxEscapement THEN EXIT; currPos _ currPos + 1; currEscapement _ newEscapement ENDLOOP; entry.width _ Real.Fix[(currEscapement+VoiceMarkers.voiceCharWidth-1.0)/ VoiceMarkers.voiceCharWidth]; entry.displayChars _ currPos; }; BackSpace: PUBLIC PROC [viewer: ViewerClasses.Viewer] = { node: TextNode.Ref; textEntry: VoiceViewers.TextMarkEntry; AddArtwork: PROC [root: TiogaOpsDefs.Ref] = { TextEdit.PutCharProp[node, textEntry.position, $Artwork, NARROW["VoiceMarker", Rope.ROPE]]; -- the NARROW prevents a REF TEXT being created TextEdit.PutCharProp[node, textEntry.position, $VoiceMark, textEntry.text.Substr[0, textEntry.displayChars]]; }; selectionViewer: ViewerClasses.Viewer; start, end, char: TiogaOpsDefs.Location; deletePosition, charsToNextPosition: INT; caretBefore: BOOLEAN; viewerInfo: VoiceViewers.VoiceViewerInfo; [viewer: selectionViewer, start: start, end: end, caretBefore: caretBefore] _ TiogaOps.GetSelection[]; IF selectionViewer # viewer THEN ERROR; viewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], VoiceViewers.VoiceViewerInfo]; IF ~VoiceViewers.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 VoiceViewers.TextMarkEntry _ viewerInfo.textMarkList, l.rest WHILE l # NIL DO IF l.first.position >= deletePosition THEN { IF l.first.position = deletePosition THEN { textEntry _ l.first; charsToNextPosition _ IF l.rest = NIL THEN LAST[INT] ELSE l.rest.first.position - deletePosition }; EXIT } ENDLOOP; IF textEntry = NIL THEN MessageWindow.Append["No textual annotation at point of selection", TRUE] ELSE { charsOnDisplay: INT _ textEntry.displayChars; textEntry.text _ textEntry.text.Substr[0, textEntry.text.Length -1]; LimitText[textEntry, charsToNextPosition]; viewerInfo.edited _ TRUE; IF charsOnDisplay # textEntry.displayChars THEN { TiogaOps.CallWithLocks[AddArtwork]; ViewerOps.PaintViewer[viewer: viewer, hint: client, clearClient: TRUE, whatChanged: NIL] } ELSE VoiceViewers.SetVoiceViewerEditStatus[selectionViewer] }; viewerInfo.editInProgress _ FALSE }; BackWord: PUBLIC PROC [viewer: ViewerClasses.Viewer] = { node: TextNode.Ref; textEntry: VoiceViewers.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: VoiceViewers.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], VoiceViewers.VoiceViewerInfo]; node _ TiogaButtons.TextNodeRef[start.node]; IF ~VoiceViewers.GetVoiceLock[viewerInfo] THEN RETURN; IF viewerInfo.textMarkList = NIL THEN { viewerInfo.editInProgress _ FALSE; RETURN }; IF viewerInfo.textMarkList.first.position >= start.where THEN -- any text to removed is at the head of the list WHILE viewerInfo.textMarkList # NIL AND viewerInfo.textMarkList.first.position <= end.where DO textEntry _ viewerInfo.textMarkList.first; textEntry.text _ NIL; TiogaOps.CallWithLocks[AddArtwork]; textRemoved _ TRUE; viewerInfo.textMarkList _ viewerInfo.textMarkList.rest ENDLOOP ELSE { l: LIST OF VoiceViewers.TextMarkEntry _ viewerInfo.textMarkList; WHILE l.rest # NIL AND l.rest.first.position < start.where DO l _ l.rest ENDLOOP; WHILE l.rest # NIL AND l.rest.first.position <= end.where DO textEntry _ l.rest.first; textEntry.text _ NIL; TiogaOps.CallWithLocks[AddArtwork]; textRemoved _ TRUE; l.rest _ l.rest.rest ENDLOOP; IF textRemoved THEN { currChars: INT; textEntry _ l.first; currChars _ textEntry.displayChars; LimitText[textEntry, IF l.rest = NIL THEN LAST[INT] ELSE l.rest.first.position - textEntry.position]; IF currChars # textEntry.displayChars THEN TiogaOps.CallWithLocks[AddArtwork] } }; IF textRemoved THEN { viewerInfo.edited _ TRUE; ViewerOps.PaintViewer[viewer: viewer, hint: client, clearClient: TRUE, whatChanged: NIL] }; viewerInfo.editInProgress _ FALSE }; RedrawTextMarkers: PUBLIC PROC [viewer: ViewerClasses.Viewer, voiceCharNode: TiogaOpsDefs.Ref] = { AddArtwork: PROC [root: TiogaOpsDefs.Ref] = { textEntry: VoiceViewers.TextMarkEntry _ l.first; TextEdit.PutCharProp[node, textEntry.position, $Artwork, NARROW["VoiceMarker", Rope.ROPE]]; -- the NARROW prevents a REF TEXT being created TextEdit.PutCharProp[node, textEntry.position, $VoiceMark, textEntry.text.Substr[0, textEntry.displayChars]]; }; node: TextNode.Ref _ TiogaButtons.TextNodeRef[voiceCharNode]; viewerInfo: VoiceViewers.VoiceViewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], VoiceViewers.VoiceViewerInfo]; l: LIST OF VoiceViewers.TextMarkEntry; FOR l _ viewerInfo.textMarkList, l.rest WHILE l # NIL DO TiogaOps.CallWithLocks[AddArtwork] ENDLOOP }; ExtractTextMarks: PUBLIC PROC [viewerInfo: VoiceViewers.VoiceViewerInfo, soundInterval: VoiceViewers.SoundInterval] = { currLastInList: LIST OF VoiceViewers.TextMarkEntry; soughtStart: INT _ soundInterval.ropeInterval.start/VoiceViewers.soundRopeCharLength; soughtEnd: INT _ (soundInterval.ropeInterval.start + soundInterval.ropeInterval.length)/VoiceViewers.soundRopeCharLength; soundInterval.textMarkList _ NIL; -- should be already so, but . . . FOR l: LIST OF VoiceViewers.TextMarkEntry _ viewerInfo.textMarkList, l.rest WHILE l # NIL AND l.first.position < soughtEnd DO IF l.first.position >= soughtStart THEN { newEntry: VoiceViewers.TextMarkEntry _ NEW [VoiceViewers.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: VoiceViewers.VoiceViewerInfo, unchangedHead, deleteChars, insertChars: INT, soundInterval: VoiceViewers.SoundInterval] = { atHead: BOOLEAN _ TRUE; l: LIST OF VoiceViewers.TextMarkEntry; WHILE atHead AND viewerInfo.textMarkList # NIL DO SELECT viewerInfo.textMarkList.first.position FROM >= unchangedHead+deleteChars => {viewerInfo.textMarkList.first.position _ viewerInfo.textMarkList.first.position + insertChars - deleteChars; atHead _ FALSE}; >= unchangedHead => viewerInfo.textMarkList _ viewerInfo.textMarkList.rest; ENDCASE => atHead _ FALSE; ENDLOOP; IF viewerInfo.textMarkList # NIL THEN { l _ viewerInfo.textMarkList; WHILE l # NIL AND l.rest # NIL DO --the dual WHILE condition is because we can delete the next entry and it could be the last SELECT l.rest.first.position FROM >= unchangedHead+deleteChars => {l.rest.first.position _ l.rest.first.position + insertChars - deleteChars; l _ l.rest}; >= unchangedHead => l.rest _ l.rest.rest; ENDCASE => l _ l.rest ENDLOOP }; IF soundInterval # NIL AND soundInterval.textMarkList # NIL THEN -- now put the textual marks from the soundInterval into the list at the appropriate point { beforeInsert, afterInsert: LIST OF VoiceViewers.TextMarkEntry _ NIL; newSection, endNewSection: LIST OF VoiceViewers.TextMarkEntry; IF viewerInfo.textMarkList # NIL THEN { IF viewerInfo.textMarkList.first.position >= unchangedHead THEN afterInsert _ viewerInfo.textMarkList ELSE { l _ viewerInfo.textMarkList; WHILE l.rest # NIL AND l.rest.first.position < unchangedHead DO l _ l.rest ENDLOOP; beforeInsert _ l; afterInsert _ l.rest } }; { insertMarks: LIST OF VoiceViewers.TextMarkEntry _ soundInterval.textMarkList.rest; newSection _ CONS [NEW [VoiceViewers.TextMarkRec _ soundInterval.textMarkList.first^], NIL]; newSection.first.position _ newSection.first.position + unchangedHead; endNewSection _ newSection; WHILE insertMarks # NIL DO endNewSection.rest _ CONS [NEW [VoiceViewers.TextMarkRec _ soundInterval.textMarkList.first^], NIL]; endNewSection _ endNewSection.rest; insertMarks _ insertMarks.rest; endNewSection.first.position _ endNewSection.first.position + unchangedHead ENDLOOP }; endNewSection.rest _ afterInsert; LimitText[endNewSection.first, IF endNewSection.rest = NIL THEN LAST[INT] ELSE endNewSection.rest.first.position - endNewSection.first.position]; IF beforeInsert = NIL THEN viewerInfo.textMarkList _ newSection ELSE { beforeInsert.rest _ newSection; LimitText[beforeInsert.first, newSection.first.position - beforeInsert.first.position] } } ELSE -- no new entries to be inserted, but we must LimitText on the entry previous to the cut {IF viewerInfo.textMarkList # NIL AND viewerInfo.textMarkList.first.position < unchangedHead THEN { beforeCut: LIST OF VoiceViewers.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 VoiceViewers.TextMarkEntry] RETURNS [r: Rope.ROPE _ NIL] = { WHILE l # NIL DO textEntry: Rope.ROPE _ l.first.text; positionInSamples: INT _ l.first.position*VoiceViewers.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 VoiceViewers.TextMarkEntry _ NIL] = { endOfList: LIST OF VoiceViewers.TextMarkEntry; WHILE r.Length > 0 DO nextColon: INT _ r.Find[":"]; newEntry: VoiceViewers.TextMarkEntry _ NEW [VoiceViewers.TextMarkRec _ [Convert.IntFromRope[r.Substr[0, nextColon]]/VoiceViewers.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 VoiceViewers.TextMarkEntry _ l, list.rest WHILE list # NIL DO LimitText[list.first, IF list.rest = NIL THEN LAST[INT] ELSE list.rest.first.position - list.first.position] ENDLOOP }; voiceMarkerFont: PUBLIC ImagerFont.Font _ ImagerFont.Find["xerox/tiogafonts/tioga10"]; END. èVoiceMarkersImpl.mesa module to handle markers in voice, of two types i) a special character which the user can place at a selection and remove using button-pushes ii) annotation of voice with text [if I get round to it] Ades, May 1, 1986 10:39:24 am PDT 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] ---- routines to do with character markers: these are transitory and do not form part of the contents of the viewer adds marks to the selected voice viewer at each end of the selection if pending delete, otherwise at the position of the caret called when a viewer is known to be a voice viewer with at least position+1 characters in it: the caller should aready hold the VoiceLock this gets called by SoundList.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 ---- routines to do with textual markers: these markers are really a part of the viewer contents. They are saved when the voice is saved and altering them makes the window status 'edited' **** a Plass-bug: shouldn't be necessary and he says he'll have a look at it entry potentially contains the supplied entry.text: this may only extend over maxLength voice characters, so it may not be possible to display all of it. Set entry.displayChars appropriately and set entry.width to say how much space is taken up by the text actually on display text limiting is currently accomplished by preserving all the text but only displaying that part which can be displayed: this means that when text obscuring other text is deleted the obscured text reappears for example. It could be arranged that the obscured text was actually eradicated: this would be done by inserting the following line and deleting the displayChars field of the TextMarkEntry and all the code that accesses it IF currPos # textLength THEN entry.text _ entry.text.Substr[0, currPos]; if there is a textual marker at the selection then remove its last character **** a Plass-bug: shouldn't be necessary and he says he'll have a look at it if there are textual markers within the selection then remove them completely - perhaps a rather odd interpretation of BackWord? **** a Plass-bug: shouldn't be necessary and he says he'll have a look at it called by voicePlayBack's redraw procedure [under a voice lock] to place all the textual markers back in a voice viewer return as selection.textMarkList a copy [n.b.] of the section of the textual mark list in viewerInfo falling in to the region of selection.ropeInterval this gets called when an edit is made to the contents of the viewer: it keeps the textMarkList in step with the edit it is not a very efficient implementation - better ones are an exercise for the reader first alter the old list to remove the text in the deleted area and move text after that area convert the list of text markers in a voice viewer into a rope suitable for saving in a textual document at the position of a talks bubble and go the other way ʺ˜šœ™J™/J™]™8Icode™!K™——šÏk ˜ Kšœœ˜)Kšœ œ˜%Jšœœ ˜Jšœœ˜$Jšœœ˜Jšœœœ,˜šœF˜FJšœ œN˜aJšœœ˜Jš˜šœDœ˜JJšœ˜Jš˜—J˜J˜Jšœ'˜-šœ˜Jšœœ?œœ˜iJšœ(˜(šœœ.˜HJšœe˜eJšœ.˜.Jšœ˜!—J˜—Jšœ˜—J˜—Jšœ˜šžœœœœœœœ œœœ˜†Jšœž œÇ™ïJšœ œ˜'Jšœ œ˜Jšœ œ˜Jšœ œ˜Jš œœœ œ"œ˜iš˜Jš œ œœœ œ ˜VJšœU˜UJšœœœ˜&Jšœ%Ÿ˜Jš˜šœ-˜-Jšœ/˜/Jšœ.˜.Jš œœ œœœœœ?˜Jšœ0˜2Jšœ/œ˜9—J˜Jš˜šœœœ6˜CJš œ œœ(œ œ˜TJ˜Jšœ œœ'˜9JšœŸ7˜<šœ œ˜!Jšœ˜Jšœ:˜:Jšœœ/œ˜WJšœ œ<˜KJšœ œ˜!—J˜JšœŸ+˜0šœ˜Jšœ.˜.Jšœ.˜.—J˜J˜Jšœ Ÿ"˜.Jš œœ œœœœœ)˜aJšœ0˜2Jšœ/œ˜8—J˜—J˜J˜J™LJšœœBœœ˜hJ˜Jšœœ˜Jšœ œ0Ÿ`˜ŸJšœœ˜"—J˜J˜Jšž œœ0œ˜Gšœ•™•Jšœœœ(˜BJšœ œ˜$Jšœ œ˜Jšœœ˜šœ˜Jšœœƒ˜–Jšœœœ˜+Jšœ˜Jšœ˜—Jšœ˜J˜Jšœ®™®JšœH™HJ™Jšœf˜fJšœ˜—˜J™—J˜šž œœœ#˜9J™LJšœ˜J˜&J˜šž œœ˜-Jšœ9œœŸ/˜‹Jšœm˜m—J˜—˜Jšœ&˜&Jšœ(˜(Jšœ%œ˜)Jšœ œ˜J˜)Jšœf˜fJšœœœ˜'Jšœ œN˜aJšœ(œœ˜6J˜Jšœœ œœ˜*Jšœ+˜+Jšœ˜J˜Jšœ œ˜š œœœ>œœœ˜]Jšœ$˜*šœœ#œ˜-šœ˜Jš œœ œœœœœ'˜`—J˜Jš˜—J˜—Jšœ˜J˜Jšœ œ˜JšœEœ˜NJš˜šœœ˜0JšœD˜DJšœ*˜*J˜Jšœœ˜Jšœ(˜*Jšœ˜šœ&˜&J™LJšœAœœ˜X—J˜Jšœ7˜;—J˜J˜Jšœœ˜"—J˜J˜šžœœœ#˜8Jšœwžœ™€Jšœ˜J˜&J˜šž œœ˜-Jšœ9œœœœœœœŸ/˜±Jš œ;œœœœœ3˜“—J˜—˜Jšœ&˜&Jšœ"˜"Jšœ œœ˜J˜)JšœL˜LJšœœœ˜'Jšœœœ˜$Jšœ œN˜aJšœ,˜,J˜Jšœ(œœ˜6Jšœœ˜%šœœ˜%Jš˜—J˜J˜Jšœ7˜9šœŸ1˜6šœœœ5˜^Jšœ*˜*Jšœœ˜Jšœ#˜#Jšœœ˜Jšœ6˜6—Jš˜—Jš˜šœœœ6˜CJš œ œœ%œ œ˜Qšœ œœ$˜œœœ˜}Jšœ!˜'šœ*œ'˜TJšœ4˜4J˜Jšœœ˜$Jš˜šœ œ œ˜5Jšœ,˜,—J˜Jš˜šœœ œ˜.Jšœ$˜$—Jšœ˜—J˜—Jš˜—J˜J˜šž œœœUœ0˜¢J™tJ™VJšœœœ˜Jšœœœ˜&J˜J™^šœœœ˜1šœ(œ˜3Jšœ˜Jšœwœ˜~JšœK˜K—Jšœ œ˜—Jšœ˜J˜Jšœœœ˜&šœ˜š œœœ œœŸ[˜}šœœ˜"šœ ˜ JšœK˜KJ˜ —Jšœ)˜)—Jšœ˜—Jšœ˜—J˜J˜Jšœœœœ˜Jšœœ˜%šœœ9˜>Jšœ&˜*Jš˜šœ˜Jš œ œœ'œ œ˜SJšœ&˜&—J˜—J˜J˜šœœœ>˜TJšœ œœAœ˜\JšœF˜FJšœ˜šœœ˜JšœœœAœ˜dJšœ#˜#Jšœ˜JšœK˜K—Jš˜—J˜J˜Jšœ!˜!Jš œœœœœœœC˜‘J˜Jšœœ˜Jšœ%˜)Jš˜šœ"˜"JšœV˜V—J˜—J˜JšœŸX˜]šœœœœ8˜ašœœœ6˜KJš œœœ/œœ˜sJš œœœœœœœ:˜€—J˜—Jšœ˜—J˜J˜šžœœœœœœ œœ˜fJ™Ššœœ˜Jšœœ˜$Jšœœ5˜KJšœc˜cJšœ˜J˜ —Jš˜—J˜J˜šžœœœ œœœœœ˜fJ™Jšœ œœ˜.J˜šœ˜Jšœ œ˜Jšœ'œlœ ˜¢Jšœ œ˜J˜J˜Jšœ9˜9J˜Jšœ(˜(Jšœ˜J˜Jšœ˜ Jš˜šœœ œ˜Jšœ ˜ —J˜Jš˜šœœ œ˜(Jšœ˜—J˜—Jšœ˜J˜š œœœ+œœ˜OJš œœ œœœœœ0˜l—Jš˜—˜˜J˜J˜J˜——J˜Jšœœ?˜VJ˜Jšœ˜—…—ZÀ€b