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]
;
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
AddCharMark:
PUBLIC TiogaOps.CommandProc = {
PROC [viewer: Viewer ← NIL] RETURNS [recordAtom: BOOL ← TRUE, quit: BOOL ← FALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams
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 = {
adds a mark at the position of the current playback cursor
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] = {
adds marks to the selected voice viewer at each end of the selection if pending delete, otherwise at the position of the caret
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] = {
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
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 = {
PROC [viewer: Viewer ← NIL] RETURNS [recordAtom: BOOL ← TRUE, quit: BOOL ← FALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams
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] = {
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
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] = {
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
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] = {
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
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
}
};
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 ...
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
IF redraw THEN ViewerOps.PaintViewer[viewer: viewer, hint: client, clearClient: TRUE, whatChanged: NIL];
TiogaVoicePrivate.MakeVoiceEdited[viewer];
viewerInfo.editInProgress ← FALSE
LimitText:
PROC [entry: TiogaVoicePrivate.TextMarkEntry, maxLength:
INT] = {
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
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;
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
IF newEscapement > maxEscapement THEN EXIT;
currPos ← currPos + 1;
currEscapement ← newEscapement
ENDLOOP;
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];
entry.width ← Real.Fix[(currEscapement+TiogaVoicePrivate.voiceCharWidth-1.0)/ TiogaVoicePrivate.voiceCharWidth];
entry.displayChars ← currPos;
BackSpace:
PUBLIC
PROC [viewer: ViewerClasses.Viewer] = {
if there is a textual marker at the selection then remove its last character
Back[viewer, BackSpaceProc];
BackWord:
PUBLIC
PROC [viewer: ViewerClasses.Viewer] = {
if there is a textual marker at the selection then remove its last word
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] = {
if there is a textual marker at the selection, do the work to remove its last character or its last word
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;
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
ViewerOps.PaintViewer[viewer: viewer, hint: client, clearClient: TRUE, whatChanged: NIL]
};
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] ~ {
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.
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] ~ {
Copy text from a voice annotation into a text viewer. The Xerox string encoding is translated back into Tioga's CharSets node property notation.
textRoot, insertNode: TextNode.Ref;
insertPosition: INT;
InsertMarks:
PROC [root: TiogaOpsDefs.Ref] ~ {
ImagerFont.MapRope[rope: text, charAction: TiogaXCharProc];
};
TiogaXCharProc: ImagerFont.XCharProc ~ {
XCharProc: TYPE ~ PROC [char: XChar];
[] ← 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]] = {
A convenience: looks after all the tree walking and calls the given actionProc once for each character in the given range.
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] ~ {
Returns all text marks in Tioga source selection, concatenated together as a single rope (NOT in Tioga voiceInText property format).
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];
Need to acquire voice viewer lock here?
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] = {
if there are textual markers within the selection then remove them completely
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;
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.
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
ViewerOps.PaintViewer[viewer: viewer, hint: client, clearClient: TRUE, whatChanged: NIL]
viewerInfo.editInProgress ← FALSE
RedrawTextMarkers:
PUBLIC
PROC [
viewer: ViewerClasses.Viewer, voiceCharNode: TiogaOpsDefs.Ref] = {
called by voicePlayBack's redraw procedure [under a voice lock] to place all the textual markers back in a voice viewer
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] = {
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
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] = {
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
atHead: BOOLEAN ← TRUE;
l: LIST OF TiogaVoicePrivate.TextMarkEntry;
first alter the old list to remove the text in the deleted area and move text after that area
WHILE atHead
AND viewerInfo.textMarkList #
NIL
DO
SELECT viewerInfo.textMarkList.first.position
FROM
>= unchangedHead+deleteChars => {
viewerInfo.textMarkList.first.position ← viewerInfo.textMarkList.first.position + insertChars - deleteChars;
atHead ← FALSE};
>= unchangedHead => viewerInfo.textMarkList ← viewerInfo.textMarkList.rest;
ENDCASE => atHead ← FALSE;
ENDLOOP;
IF viewerInfo.textMarkList #
NIL
THEN {
l ← viewerInfo.textMarkList;
WHILE l #
NIL
AND l.rest #
NIL
DO
--the dual WHILE condition is because we can delete the next entry and it could be the last
SELECT l.rest.first.position
FROM
>= unchangedHead+deleteChars => {
l.rest.first.position ← l.rest.first.position + insertChars - deleteChars;
l ← l.rest};
>= unchangedHead => l.rest ← l.rest.rest;
ENDCASE => l ← l.rest
ENDLOOP
};
IF soundInterval #
NIL
AND soundInterval.textMarkList #
NIL
THEN {
now put the textual marks from the soundInterval into the list at the appropriate point
beforeInsert, afterInsert: LIST OF TiogaVoicePrivate.TextMarkEntry ← NIL;
newSection, 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 {
no new entries to be inserted, but we must LimitText on the entry previous to the cut
IF viewerInfo.textMarkList #
NIL
AND viewerInfo.textMarkList.first.position < unchangedHead
THEN {
beforeCut: LIST OF TiogaVoicePrivate.TextMarkEntry ← viewerInfo.textMarkList;
WHILE beforeCut.rest # NIL AND beforeCut.rest.first.position < unchangedHead DO beforeCut ← beforeCut.rest ENDLOOP;
LimitText[beforeCut.first, IF beforeCut.rest = NIL THEN LAST[INT] ELSE beforeCut.rest.first.position - beforeCut.first.position]
}
}
RopeFromTextList:
PUBLIC
PROC [
l: LIST OF TiogaVoicePrivate.TextMarkEntry] RETURNS [r: Rope.ROPE ← NIL] = {
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
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] = {
and go the other way
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
};
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
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
]];
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
FindEnd:
PROC [viewerInfo: TiogaVoicePrivate.VoiceViewerInfo]
RETURNS [sample:
INT] = {
procedure to interpret 'end' for monochrome/color viewers
IF viewerInfo.ropeInterval.start # 0 THEN ERROR;
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
{ 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] = {
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
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 = {
PROC [viewer: Viewer ← NIL] RETURNS [recordAtom: BOOL ← TRUE, quit: BOOL ← FALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams
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 = {
PROC [viewer: Viewer ← NIL] RETURNS [recordAtom: BOOL ← TRUE, quit: BOOL ← FALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams
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 = {
PROC [viewer: Viewer ← NIL] RETURNS [recordAtom: BOOL ← TRUE, quit: BOOL ← FALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams
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]]]
routines that allow the lengths of silent portions within a rope to be adjusted
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 = {
PROC [viewer: Viewer ← NIL] RETURNS [recordAtom: BOOL ← TRUE, quit: BOOL ← FALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams
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];
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
TiogaVoicePrivate.MakeVoiceEdited[viewer];
ENDLOOP
}.