VoiceEditOpsImpl.mesa:
routines to edit voice viewers, plus some routines to intercept operations deemed inapplicable for voice or voice/text viewer combinations
Ades, September 23, 1986 7:34:07 pm PDT
DIRECTORY
Rope USING [ROPE, Concat, Substr],
SoundList USING [ExtractSoundList, ReplaceSoundList, SoundChars],
TEditDocument USING [SelectionId],
TextEdit USING [Size],
TiogaButtons USING [TextNodeRef],
TiogaOps USING [GetSelection, Location, GetRope, SelectPoint, CancelSelection],
TiogaOpsDefs USING [Ref, WhichSelection, SelectionGrain],
IO USING [PutFR, rope],
MessageWindow USING [Append, Blink],
ViewerClasses USING [Viewer],
ViewerOps USING [FetchProp, DestroyViewer],
VoiceAging USING [EditAges],
VoiceInText USING [thrushHandle, DebugRope],
VoiceMarkers USING [ExtractCharMarks, EditCharMarks, ExtractTextMarks, EditTextMarks],
VoicePlayBack USING [RedrawViewer],
VoiceViewers USING [VoiceViewerInfo, SoundList, soundRopeCharLength, Selection, SelectionRec, SoundInterval, SoundIntervalRec, GetVoiceLock],
VoiceRope USING [Replace, VoiceRopeInterval],
VoiceEditOps;
VoiceEditOpsImpl: CEDAR PROGRAM IMPORTS IO, MessageWindow, Rope, SoundList, TextEdit, TiogaButtons, TiogaOps, ViewerOps, VoiceAging, VoiceInText, VoiceMarkers, VoicePlayBack, VoiceRope, VoiceViewers EXPORTS VoiceEditOps = BEGIN
BadVoiceViewerDisplay: ERROR [viewer: ViewerClasses.Viewer];
Mumble: PUBLIC PROC [opAttempted: Rope.ROPE, viewerTypes: Rope.ROPE] = {
MessageWindow.Append[IO.PutFR["%g is not a valid operation between %g viewers", IO.rope[opAttempted], IO.rope[viewerTypes]], TRUE];
MessageWindow.Blink[]
};
DescribeSelection: PUBLIC PROC [which: TiogaOpsDefs.WhichSelection, forceDelete: BOOLEAN, returnSoundInterval: BOOLEAN, viewerAlreadyLocked: ViewerClasses.Viewer ← NIL] RETURNS [failed: BOOLEAN ← FALSE, selection: VoiceViewers.Selection, soundInterval: VoiceViewers.SoundInterval ← NIL] = {
this routine will characterise the voice corresponding to a selection: if the selection is pendingDelete [or if forceDelete] then the selection.ropeInterval will have non-zero length; if returnSoundInterval then the sound interval corresponding to the whole selection will be characterised - to be saved and inserted somewhere else
selections can only be described if the voiceLock can be taken out on them: this routine returns failed if it cannot get the lock; otherwise it returns with the lock still held. However the caller may pass a viewer in viewerAlreadyLocked to say that he already has that one locked and if this
selection is in the same viewer then we may proceed
selStart, selEnd: TiogaOps.Location;
caretBefore, pendingDelete: BOOL;
level: TiogaOpsDefs.SelectionGrain;
selection ← NEW[VoiceViewers.SelectionRec];
[viewer: selection.viewer, start: selStart, end: selEnd, level: level, caretBefore: caretBefore, pendingDelete: pendingDelete] ← TiogaOps.GetSelection[which];
selection.voiceViewerInfo ← NARROW[ViewerOps.FetchProp[selection.viewer, $voiceViewerInfo], VoiceViewers.VoiceViewerInfo];
IF ~( viewerAlreadyLocked = selection.viewer OR VoiceViewers.GetVoiceLock[selection.voiceViewerInfo] ) THEN {failed ← TRUE; RETURN};
IF selStart.node # selEnd.node THEN ERROR BadVoiceViewerDisplay[selection.viewer];
selection.displayNode ← selStart.node;
work out the VoiceRopeInterval corresponding to the whole selection
it is here that policy about trimming the sound selection e.g. to silence/sound boundaries could be implemented
selection.ropeInterval ← [selection.voiceViewerInfo.ropeInterval.ropeID, selStart.where*VoiceViewers.soundRopeCharLength, (selEnd.where+(IF level=point THEN 0 ELSE 1))*VoiceViewers.soundRopeCharLength -selStart.where*VoiceViewers.soundRopeCharLength];
IF selection.ropeInterval.start + selection.ropeInterval.length > selection.voiceViewerInfo.ropeInterval.length THEN ERROR BadVoiceViewerDisplay[selection.viewer];
IF returnSoundInterval THEN
{ soundInterval ← NEW [VoiceViewers.SoundIntervalRec ← [ropeInterval: selection.ropeInterval]];
SoundList.ExtractSoundList[selection.voiceViewerInfo, soundInterval];
VoiceMarkers.ExtractCharMarks[selection.voiceViewerInfo, soundInterval];
VoiceMarkers.ExtractTextMarks[selection.voiceViewerInfo, soundInterval] -- the width/displayChars fields in this will not necessarily be valid at the end of the list
};
IF ~(pendingDelete OR forceDelete)
THEN
{
IF ~caretBefore THEN selection.ropeInterval.start ← selection.ropeInterval.start + selection.ropeInterval.length;
selection.ropeInterval.length ← 0
}
};
ReplaceSelectionWithSavedInterval: PUBLIC PROC [selection: VoiceViewers.Selection, soundInterval: VoiceViewers.SoundInterval, leaveSelected: BOOLEAN] RETURNS [viewerDeleted: BOOLEAN, insertChars, positionAfterInsert: INT] = {
this routine actually does all the editing work for voice: it is passed a selection specification and a sound specification: if the selection is of non-zero length it is deleted; then if the sound is non-nil it is inserted
this routine (i) works out what the updated voice viewer sound list and contents will be (ii) calls VoicePlayBack to redraw the viewer [done there since the viewer may be somewhere in the queue of sounds to be played back] (iii) interacts with the voice rope data base to update the rope specification corresponding to the viewer
it may turn out that there is nothing left in the viewer at the end of this action, in which case the viewer will simply be deleted. However this is not done until after VoicePlayBack. RedrawViewer has been called since a viewer cannot be destroyed whilst the playback caret is in it: after a call to RedrawViewer the playback caret cannot be in it!!
there is no way to stop this destruction in this implementation: in a Copy/Transpose both selections could be in the same viewer but if so the result of the first call to this procedure cannot be an empty viewer
newViewerContents: Rope.ROPE;
displayCutPoint, deleteChars: INT;
IF selection.ropeInterval.length = 0 AND soundInterval = NIL THEN RETURN;
SoundList.ReplaceSoundList[voiceViewerInfo: selection.voiceViewerInfo, cutStart: selection.ropeInterval.start, cutLength: selection.ropeInterval.length, replacement: IF soundInterval = NIL THEN NIL ELSE soundInterval.soundList];
displayCutPoint ← selection.ropeInterval.start/VoiceViewers.soundRopeCharLength;
deleteChars ← selection.ropeInterval.length/VoiceViewers.soundRopeCharLength;
insertChars ← IF soundInterval = NIL THEN 0 ELSE soundInterval.ropeInterval.length/VoiceViewers.soundRopeCharLength;
those three need only be [and are only] approximate
positionAfterInsert ← displayCutPoint + insertChars + 1;
VoiceMarkers.EditCharMarks[selection.voiceViewerInfo, displayCutPoint, deleteChars, insertChars, soundInterval];
VoiceMarkers.EditTextMarks[selection.voiceViewerInfo, displayCutPoint, deleteChars, insertChars, soundInterval];
VoiceAging.EditAges[selection.voiceViewerInfo, displayCutPoint, deleteChars, insertChars];
[newViewerContents, selection.voiceViewerInfo.remnant] ← SoundList.SoundChars[selection.voiceViewerInfo, displayCutPoint];
newViewerContents ← Rope.Concat[(TiogaOps.GetRope[selection.displayNode]).Substr[0, displayCutPoint], newViewerContents];
done for efficiency - no need to recalculate the first characters again
selection.displayNode ← VoicePlayBack.RedrawViewer[selection.viewer, newViewerContents, displayCutPoint, deleteChars, insertChars, selection.voiceViewerInfo.remnant, TRUE, IF leaveSelected THEN primaryOnInsert ELSE deSelected];
redrawing isdone in VoicePlayBack for sync with any playback cues
the last but one parameter governs whether this edit will cause all previous edits to age [only if insertChars # 0]. It is simply set TRUE here: if there were any use for such a facility a parameter could be passed by caller of ReplaceSelectionWithSavedInterval to pass on here
viewerDeleted ← TextEdit.Size[TiogaButtons.TextNodeRef[selection.displayNode]] = 0;
IF viewerDeleted
THEN
{ MessageWindow.Append["voice viewer now completely empty - destroying", TRUE];
ViewerOps.DestroyViewer[selection.viewer]
this will cause the event proc which clears down the voiceViewerInfo etc. to be called
}
ELSE
{
selection.voiceViewerInfo.ropeInterval ← VoiceRope.Replace[handle: VoiceInText.thrushHandle, vr: NEW [VoiceRope.VoiceRopeInterval ← selection.voiceViewerInfo.ropeInterval], start: selection.ropeInterval.start, len: selection.ropeInterval.length, with: IF soundInterval = NIL THEN NIL ELSE NEW [VoiceRope.VoiceRopeInterval ← soundInterval.ropeInterval]]^
}
};
Delete: PUBLIC PROC = {
delete the primary selection
charAfterDelete: INT;
viewerDeleted: BOOLEAN;
selectionLocked: BOOLEAN;
selection: VoiceViewers.Selection;
[selection: selection, failed: selectionLocked] ← DescribeSelection[which: primary, forceDelete: TRUE, returnSoundInterval: FALSE];
IF selectionLocked THEN RETURN;
selection.voiceViewerInfo.edited ← TRUE;
[positionAfterInsert: charAfterDelete, viewerDeleted: viewerDeleted] ← ReplaceSelectionWithSavedInterval[selection: selection, soundInterval: NIL, leaveSelected: TRUE];
IF ~viewerDeleted THEN
{
TiogaOps.SelectPoint[viewer: selection.viewer, caret: [selection.displayNode, MIN [charAfterDelete, TextEdit.Size[TiogaButtons.TextNodeRef[selection.displayNode]]-1]], which: primary];
selection.voiceViewerInfo.editInProgress ← FALSE
}
};
Copy: PUBLIC PROC [target: TEditDocument.SelectionId ← primary] = {
sourceSelection, destSelection: VoiceViewers.Selection;
sourceInterval: VoiceViewers.SoundInterval;
selectionLocked: BOOLEAN;
destMovesForward: INT ← 0;
if source selection is pending delete and ahead of the destination selection in the same viewer, then after the first action of deleting source the destination will be nearer the start of the sound
[selection: sourceSelection, soundInterval: sourceInterval, failed: selectionLocked] ← DescribeSelection[which: (IF target=primary THEN secondary ELSE primary), forceDelete: FALSE, returnSoundInterval: TRUE];
IF ~selectionLocked THEN
{
[selection: destSelection, failed: selectionLocked] ← DescribeSelection[which: (IF target=primary THEN primary ELSE secondary), forceDelete: FALSE, returnSoundInterval: FALSE, viewerAlreadyLocked: sourceSelection.viewer];
IF selectionLocked THEN sourceSelection.voiceViewerInfo.editInProgress ← FALSE
};
TiogaOps.CancelSelection[primary];
TiogaOps.CancelSelection[secondary];
we drop the selections here because after a ReplaceSelectionWithSavedInterval call a viewer may not exist! If a selection above failed we should also drop the selections here as well - if selections are left around after an aborted tioga editing operation unpleasant thing tend to happen
IF selectionLocked THEN RETURN;
destSelection.voiceViewerInfo.edited ← TRUE;
IF sourceSelection.ropeInterval.length # 0
if true then source is pending delete: this means two edit operations
THEN
{ sourceSelection.voiceViewerInfo.edited ← TRUE;
IF sourceSelection.viewer = destSelection.viewer -- special case of same viewer
THEN
{
IF (sourceSelection.ropeInterval.start< destSelection.ropeInterval.start+destSelection.ropeInterval.length) = (destSelection.ropeInterval.start< sourceSelection.ropeInterval.start+sourceSelection.ropeInterval.length)
that is the test for overlapping selections: if source is pending delete the copy operation then reduces to "delete the region(s) lying in the destination but not in the source selection" - done here as a special case
done by substituting first and second regions for source and destination selections
and setting soundInterval to NIL: note that either of these regions may now have zero length
THEN
{ firstStart: INT ← destSelection.ropeInterval.start;
firstLength: INTMAX [sourceSelection.ropeInterval.start -destSelection.ropeInterval.start, 0];
secondStart: INT ← sourceSelection.ropeInterval.start + sourceSelection.ropeInterval.length;
secondLength: INTMAX [(destSelection.ropeInterval.start+destSelection.ropeInterval.length) - (sourceSelection.ropeInterval.start+sourceSelection.ropeInterval.length), 0];
sourceSelection.ropeInterval.start ← firstStart;
sourceSelection.ropeInterval.length ← firstLength;
destSelection.ropeInterval.start ← secondStart;
destSelection.ropeInterval.length ← secondLength;
sourceInterval ← NIL
};
IF sourceSelection.ropeInterval.start<destSelection.ropeInterval.start THEN destMovesForward ← sourceSelection.ropeInterval.length
};
IF ~ReplaceSelectionWithSavedInterval[sourceSelection, NIL, FALSE].viewerDeleted THEN
{ IF sourceSelection.viewer # destSelection.viewer THEN sourceSelection.voiceViewerInfo.editInProgress ← FALSE }
}
ELSE
{ IF sourceSelection.viewer # destSelection.viewer THEN sourceSelection.voiceViewerInfo.editInProgress ← FALSE
};
destSelection.ropeInterval.start ← destSelection.ropeInterval.start - destMovesForward;
[] ← ReplaceSelectionWithSavedInterval[destSelection, sourceInterval, TRUE];
destSelection.voiceViewerInfo.editInProgress ← FALSE
};
Transpose: PUBLIC PROC [target: TEditDocument.SelectionId ← primary] = {
IF target = feedback THEN ERROR;
VoiceInText.DebugRope["Voice transpose:\n"];
SelectionStates[];
VoiceInText.DebugRope["not yet implemented.\n"];
};
SelectionStates: PROC = {
VoiceInText.DebugRope[" Primary selection is"];
IF ~TiogaOps.GetSelection[primary].pendingDelete THEN VoiceInText.DebugRope[" not"];
VoiceInText.DebugRope[" pending delete,\n secondary selection is"];
IF ~TiogaOps.GetSelection[secondary].pendingDelete THEN VoiceInText.DebugRope[" not"];
VoiceInText.DebugRope[" pending delete\n"]
};
END.