<> <> << Ades, September 24, 1986 5:52:47 pm PDT>> <> <> <<>> DIRECTORY Atom USING [PropList], Commander USING [CommandProc, Register], Convert USING [IntFromRope, RopeFromInt ], Imager USING [Color, Context, DoSave, MaskStroke, Move, SetAmplifySpace, SetColor, SetStrokeJoint, SetStrokeWidth, ShowRope, ShowXChar ], ImagerColor USING [ColorFromStipple, ConstantColor], ImagerFont USING [Escapement, Extents, Font, FontBoundingBox, RopeEscapement, XChar ], ImagerPath USING [PathProc], IO USING [int, PutF, PutFR, STREAM ], MBQueue USING [Create, CreateMenuEntry, Queue ], Menus USING [AppendMenuEntry, CreateEntry, CreateMenu, FindEntry, GetLine, GetNumberOfLines, Menu, MenuEntry, MenuLine, MenuProc, ReplaceMenuEntry, SetLine ], MessageWindow USING [Append, Blink, Clear, Confirm], NodeStyle USING [GetReal, Ref], NodeStyleOps USING [FlushCaches, OfStyle], NodeStyleWorks USING [styledict], Rope USING [Cat, Concat, Equal, Find, MaxLen, ROPE, Substr ], TEditFormat USING [CharacterArtwork, CharacterArtworkClass, CharacterArtworkClassRep, CharacterArtworkRep, GetFont, RegisterCharacterArtwork ], TextEdit USING [FetchChar, GetCharProp, PutCharProp, Size ], TextNode USING [Location, Ref, Root, StepForward], TiogaAccess USING [EndOf, FromSelection, Get, Reader, TiogaChar ], TiogaButtons USING [TextNodeRef, TiogaOpsRef], TiogaMenuOps USING [tiogaMenu], TiogaOps USING [CallWithLocks, CommandProc, GetProp, GetSelection, Interpret, NoSelection, PutProp, Ref, RegisterCommand, SelectionGrain, SetSelection, StepForward, ViewerDoc ], TiogaOpsDefs USING [Location, Ref], TiogaVoicePrivate USING [ AddVoiceProc, BadRope, BuildVoiceViewer, ButtonParamsBody, CancelProc, DictationMachine, FindAttachedVoiceViewers, GetVoiceViewerInfo, PauseProc, PlayBackMenuProc, PlayRopeWithoutCue, RegisterViewerInterests, RemoveParentPointersTo, RemoveParentViewer, RopeFromTextList, SetParentViewer, VoiceViewerInfo, VoiceViewerInfoList, VoiceWindowRec, VoiceWindowRef ], TJaM USING [Put], Vector2 USING [VEC], ViewerBLT USING [ChangeNumberOfLines], ViewerClasses USING [Viewer], ViewerEvents USING [EventProc, EventRegistration, RegisterEventProc, UnRegisterEventProc ], ViewerForkers USING [ CallBack, ForkCall], ViewerOps USING [AddProp, FetchProp, PaintViewer], VoiceRope USING [Handle, Length, Open, VoiceRope, VoiceRopeInterval ] ; VoiceInTextImpl: CEDAR PROGRAM IMPORTS Commander, Convert, Imager, ImagerColor, ImagerFont, IO, MBQueue, Menus, MessageWindow, NodeStyle, NodeStyleOps, NodeStyleWorks, Rope, TEditFormat, TextEdit, TextNode, TiogaAccess, TiogaButtons, TiogaMenuOps, TiogaOps, TiogaVoicePrivate, TJaM, ViewerBLT, ViewerEvents, ViewerForkers, ViewerOps, VoiceRope EXPORTS TiogaVoicePrivate SHARES ViewerClasses = { <> thrushHandle: PUBLIC VoiceRope.Handle _ VoiceRope.Open[]; voiceRopePrefix: Rope.ROPE _ "Sound Viewer #"; TiogaVoiceCmdProc: Commander.CommandProc = { NULL; }; <> VoiceMenu: TiogaOps.CommandProc = { ChangeMenu[NARROW[viewer], voiceMenu] }; <> <> <> ChangeMenu: PUBLIC PROC [ viewer: ViewerClasses.Viewer, subMenu: Menus.MenuEntry] = { <> menu: Menus.Menu _ viewer.menu; found: BOOL _ FALSE; numLines: Menus.MenuLine = Menus.GetNumberOfLines[menu]; newLines: Menus.MenuLine _ numLines; FOR i: Menus.MenuLine IN [1..numLines) DO <> IF Rope.Equal[Menus.GetLine[menu,i].name, subMenu.name] THEN { -- yes, so remove it FOR j: Menus.MenuLine IN (i..numLines) DO Menus.SetLine[menu, j-1, Menus.GetLine[menu, j]]; ENDLOOP; newLines _ newLines-1; found _ TRUE; EXIT }; ENDLOOP; IF ~found THEN { <> GoesBefore: PROC [m1, m2: Menus.MenuEntry] RETURNS [BOOL] = { Priority: PROC [m: Menus.MenuEntry] RETURNS [INTEGER] = { <> <> RETURN [SELECT TRUE FROM Rope.Equal[m.name, "Find"] => 1, Rope.Equal[m.name, "FirstLevelOnly"] => 0, ENDCASE => -1 -- unknown menu goes at bottom -- ] }; RETURN [Priority[m1] > Priority[m2]] }; newLast: Menus.MenuLine = MIN[numLines, LAST[Menus.MenuLine]]; newLines _ newLines+1; FOR i: Menus.MenuLine IN [1..numLines) DO IF GoesBefore[subMenu, Menus.GetLine[menu, i]] THEN { <> FOR j: Menus.MenuLine DECREASING IN (i..newLast] DO Menus.SetLine[menu, j, Menus.GetLine[menu, j-1]]; ENDLOOP; Menus.SetLine[menu, i, subMenu]; found _ TRUE; EXIT }; ENDLOOP; IF ~found THEN Menus.SetLine[menu, newLast, subMenu]; }; ViewerBLT.ChangeNumberOfLines[viewer, newLines]; }; ApplyToCharsInPrimarySelection: PUBLIC PROC [actionProc: PROC [TiogaOpsDefs.Location]] = { <> ScanLocked: PROC [root: TiogaOps.Ref] = { current, end: TiogaOpsDefs.Location; level: TiogaOps.SelectionGrain; [start: current, end: end, level: level] _ TiogaOps.GetSelection[]; IF level=point THEN RETURN; -- nothing in the selection anyhow IF current.node = end.node THEN FOR i: INT IN [current.where..end.where] DO actionProc[[current.node, i]] ENDLOOP ELSE { FOR i: INT IN [current.where..TextEdit.Size[TiogaButtons.TextNodeRef[current.node]]) DO actionProc[[current.node, i]] ENDLOOP; DO current.node _ TiogaOps.StepForward[current.node]; IF current.node = end.node THEN { FOR i: INT IN [0..end.where) DO actionProc[[current.node, i]] ENDLOOP; RETURN } ELSE FOR i: INT IN [0..TextEdit.Size[TiogaButtons.TextNodeRef[current.node]]) DO actionProc[[current.node, i]] ENDLOOP; ENDLOOP; }; }; TiogaOps.CallWithLocks[ScanLocked ! TiogaOps.NoSelection => CONTINUE] }; ApplyToLockedChars: PUBLIC PROC [actionProc: PROC [TiogaOpsDefs.Location]] = { <> current, end: TiogaOpsDefs.Location; level: TiogaOps.SelectionGrain; [start: current, end: end, level: level] _ TiogaOps.GetSelection[]; IF level=point THEN RETURN; -- nothing in the selection anyhow IF current.node = end.node THEN FOR i: INT IN [current.where..end.where] DO actionProc[[current.node, i]] ENDLOOP ELSE { FOR i: INT IN [current.where..TextEdit.Size[TiogaButtons.TextNodeRef[current.node]]) DO actionProc[[current.node, i]] ENDLOOP; DO current.node _ TiogaOps.StepForward[current.node]; IF current.node = end.node THEN { FOR i: INT IN [0..end.where) DO actionProc[[current.node, i]] ENDLOOP; RETURN } ELSE FOR i: INT IN [0..TextEdit.Size[TiogaButtons.TextNodeRef[current.node]]) DO actionProc[[current.node, i]] ENDLOOP; ENDLOOP; } }; <<>> StoreVoiceAtSelection: PUBLIC PROC [voiceViewerInfo: TiogaVoicePrivate.VoiceViewerInfo] RETURNS [succeeded: BOOLEAN _ FALSE] = { <> selectedViewer: ViewerClasses.Viewer; suitableViewer: BOOLEAN _ FALSE; alreadyVoiceThere: BOOLEAN _ FALSE; SuitableViewer: PROC RETURNS [BOOLEAN] = { RETURN [selectedViewer.class.flavor = $Text AND ViewerOps.FetchProp[selectedViewer, $voiceViewerInfo] = NIL] }; AddVoiceMarkerAtPrimarySelection: PROC [root: TiogaOps.Ref] = { startChar, endChar, targetChar: TiogaOpsDefs.Location; node: TextNode.Ref; caretBefore: BOOLEAN; pendingDelete: BOOLEAN; level: TiogaOps.SelectionGrain; [viewer: selectedViewer, start: startChar, end: endChar, caretBefore: caretBefore, pendingDelete: pendingDelete, level: level] _ TiogaOps.GetSelection[]; suitableViewer _ SuitableViewer[]; IF pendingDelete AND suitableViewer THEN { ApplyToLockedChars[DeleteVoiceFromChar]; TiogaOps.SetSelection[viewer: selectedViewer, start: startChar, end: endChar, level: level, caretBefore: caretBefore, pendingDelete: FALSE, which: primary] -- simply makes not pending delete }; IF suitableViewer THEN { targetChar _ IF caretBefore THEN startChar ELSE endChar; node _ TiogaButtons.TextNodeRef[targetChar.node]; -- just a type convertor IF TextEdit.Size[node] <= targetChar.where THEN <> targetChar.where _ MAX[targetChar.where-1, 0]; IF TextEdit.Size[node] <= targetChar.where THEN suitableViewer _ FALSE; alreadyVoiceThere _ TextEdit.GetCharProp[node, targetChar.where, $voice] # NIL; IF NOT suitableViewer OR alreadyVoiceThere THEN RETURN; TextEdit.PutCharProp[node, targetChar.where, $voice, voiceViewerInfo.ropeInterval.ropeID]; TextEdit.PutCharProp[node, targetChar.where, $textInVoice, TiogaVoicePrivate.RopeFromTextList[voiceViewerInfo.textMarkList]]; <> TextEdit.PutCharProp[node, targetChar.where, $Artwork, NARROW["TalksBubble", Rope.ROPE]]; -- the NARROW prevents a REF TEXT being created TiogaVoicePrivate.SetParentViewer[voiceViewerInfo, selectedViewer, targetChar]; <> TextEdit.PutCharProp[node, targetChar.where, $voiceWindow, NEW[TiogaVoicePrivate.VoiceWindowRec _ [label: Rope.Concat[voiceRopePrefix, Convert.RopeFromInt[voiceViewerInfo.viewerNumber]]]]]; } }; -- end of AddVoiceMarkerAtPrimarySelection IF voiceViewerInfo.ropeInterval.ropeID = NIL THEN RETURN; TiogaOps.CallWithLocks[AddVoiceMarkerAtPrimarySelection ! TiogaOps.NoSelection => CONTINUE]; <> SELECT TRUE FROM NOT suitableViewer => BlinkMessageWindow["Make a character selection in a Tioga viewer first"]; alreadyVoiceThere => BlinkMessageWindow["Cannot add sound on top of another sound; delete old sound if desired"]; ENDCASE => {MessageWindow.Clear[]; succeeded _ TRUE}; }; -- end of StoreVoiceAtSelection DeleteVoiceProc: TiogaOps.CommandProc = { <> voiceThere _ FALSE; ApplyToCharsInPrimarySelection[DeleteVoiceFromChar]; IF NOT voiceThere THEN MessageWindow.Append["No sounds in selection", TRUE] ELSE MessageWindow.Clear[]; }; voiceThere: BOOL _ FALSE; -- this relies upon the MBQueue underneath menu buttons to keep other processes from resetting it during a single invocation. Pretty hacky, but it only controls the user message. DeleteVoiceFromChar: PUBLIC PROC [position: TiogaOpsDefs.Location] = { <> node: TextNode.Ref _ TiogaButtons.TextNodeRef[position.node]; offset: INT _ position.where; IF TextEdit.GetCharProp[node, offset, $voice]#NIL THEN { viewerInfo: TiogaVoicePrivate.VoiceViewerInfo _ ValidateSourceMarker[node, offset]; voiceThere _ TRUE; TextEdit.PutCharProp[node, offset, $Artwork, NIL]; TextEdit.PutCharProp[node, offset, $voice, NIL]; TextEdit.PutCharProp[node, offset, $textInVoice, NIL]; IF viewerInfo # NIL THEN { TextEdit.PutCharProp[node, offset, $voiceWindow, NIL]; <<Ideally, the [edited] bit in a voice viewer might show whether its contents are stored anywhere. But checking this in DeleteVoice only accomplishes part of this, because deleting an annotated character also deletes the voice (and the user might hit Reset in the doc anyway). Should also see whether there are any more copies of this source marker/voice rope in this doc before breaking links or indicating edited.>> <> <> }; }; }; PlaySelection: PUBLIC PROC = { selectStream: TiogaAccess.Reader _ TiogaAccess.FromSelection[]; charsInSelection: INT _ 0; soundsInSelection: INT _ 0; heavyChar: TiogaAccess.TiogaChar; props: Atom.PropList; badRopes, badRope: TiogaVoicePrivate.BadRope _ okay; IF NOT TiogaAccess.EndOf[selectStream] THEN DO heavyChar _ TiogaAccess.Get[selectStream]; IF TiogaAccess.EndOf[selectStream] THEN EXIT; charsInSelection _ charsInSelection + 1; FOR props _ heavyChar.propList, props.rest WHILE props # NIL DO IF props.first.key = $voice THEN { badRope _ TiogaVoicePrivate.PlayRopeWithoutCue[NARROW[props.first.val, Rope.ROPE]]; badRopes _ MIN[badRopes, badRope]; soundsInSelection _ soundsInSelection + 1; }; ENDLOOP; ENDLOOP; IF soundsInSelection = 0 THEN MessageWindow.Append["No sounds in selection", TRUE] ELSE IF badRopes < okay THEN MessageWindow.Append["Non-existent or zero length voice utterance(s) found in selection", TRUE] ELSE MessageWindow.Clear[]; DebugRope[IO.PutFR["%d characters in selection\n", IO.int[charsInSelection]]] }; EditVoiceProc: TiogaOps.CommandProc = { <> <> voiceList: LIST OF Rope.ROPE _ NIL; textInVoiceList: LIST OF Rope.ROPE _ NIL; newViewerInfoList: TiogaVoicePrivate.VoiceViewerInfoList _ NIL; nonRopes: BOOL _ FALSE; <<>> SearchForVoice: PROC [targetChar: TiogaOpsDefs.Location] = { node: TextNode.Ref _ TiogaButtons.TextNodeRef[targetChar.node]; voiceRopeID: Rope.ROPE _ NARROW[TextEdit.GetCharProp[node, targetChar.where, $voice], Rope.ROPE]; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo _ ValidateSourceMarker[node, targetChar.where]; -- take this opportunity to fix up if needed IF voiceRopeID # NIL THEN { fullRope: VoiceRope.VoiceRope _ NEW [VoiceRope.VoiceRopeInterval _ [voiceRopeID, 0, 0]]; fullRope.length _ VoiceRope.Length[handle: thrushHandle, vr: fullRope]; IF fullRope.length <= 0 THEN nonRopes _ TRUE ELSE { IF viewerInfo = NIL THEN { voiceList _ AppendRope[voiceList, voiceRopeID]; textInVoiceList _ AppendRope[textInVoiceList, NARROW[TextEdit.GetCharProp[node, targetChar.where, $textInVoice], Rope.ROPE]] } ELSE DebugRope["Voice already has an associated viewer\n"] } } }; AddVoiceWindowProps: PROC [targetChar: TiogaOpsDefs.Location] = { <> <> node: TextNode.Ref _ TiogaButtons.TextNodeRef[targetChar.node]; voiceRopeID: Rope.ROPE _ NARROW[TextEdit.GetCharProp[node, targetChar.where, $voice], Rope.ROPE]; IF voiceList = NIL THEN RETURN; IF voiceRopeID = NIL THEN RETURN; IF TextEdit.GetCharProp[node, targetChar.where, $voiceWindow] # NIL THEN RETURN; <> IF voiceRopeID.Equal[voiceList.first] THEN { parentViewer: ViewerClasses.Viewer _ TiogaOps.GetSelection[].viewer; alreadyEdited: BOOLEAN _ parentViewer.newVersion; TiogaVoicePrivate.SetParentViewer[newViewerInfoList.first, parentViewer, targetChar]; <> TextEdit.PutCharProp[node, targetChar.where, $voiceWindow, NEW[TiogaVoicePrivate.VoiceWindowRec _ [label: Rope.Concat[voiceRopePrefix, Convert.RopeFromInt[newViewerInfoList.first.viewerNumber]]]]]; IF ~alreadyEdited THEN { parentViewer.newVersion _ FALSE; ViewerOps.PaintViewer[parentViewer, caption, FALSE] }; voiceList _ voiceList.rest; newViewerInfoList _ newViewerInfoList.rest; <> }; }; ApplyToCharsInPrimarySelection[SearchForVoice ! TiogaOps.NoSelection => GOTO Quit]; IF voiceList = NIL THEN { MessageWindow.Append[ IF nonRopes THEN "Non-existent or zero length voice utterance(s) found in selection" ELSE "No undisplayed sounds in selection", TRUE]; RETURN; } ELSE MessageWindow.Clear[]; FOR l: LIST OF Rope.ROPE _ voiceList, l.rest WHILE l # NIL DO newInfo: TiogaVoicePrivate.VoiceViewerInfo; [viewerInfo: newInfo] _ TiogaVoicePrivate.BuildVoiceViewer[voiceID: l.first, textInVoice: textInVoiceList.first, youngVoice: FALSE]; newViewerInfoList _ AppendViewerInfo[newViewerInfoList, newInfo]; textInVoiceList _ textInVoiceList.rest ENDLOOP; ApplyToCharsInPrimarySelection[AddVoiceWindowProps ! TiogaOps.NoSelection => CONTINUE] EXITS Quit => NULL }; DeleteSourceMarkers: PUBLIC PROC [viewer: ViewerClasses.Viewer, voiceViewerNumber: INT, exceptionLoc: TiogaOpsDefs.Location] = { <> DeleteSourceMarker: LocActionProc = { <> IF CheckVoiceWindowProp[propVal: voiceWindowRope, charProp: charProp] AND NOT (node=TiogaButtons.TextNodeRef[exceptionLoc.node] AND offset=exceptionLoc.where) THEN { foundOne _ TRUE; TextEdit.PutCharProp[node, offset, $voiceWindow, NIL]; <> IF TextEdit.GetCharProp[node, offset, $voice] = NIL THEN TextEdit.PutCharProp[node, offset, $Artwork, NIL]; }; }; DoIt: PROC [root: TiogaOps.Ref] = { MapCharProps[viewer: viewer, propName: $voiceWindow, actionProc: DeleteSourceMarker]; }; rootNode: TiogaOps.Ref _ TiogaOps.ViewerDoc[viewer]; alreadyEdited: BOOL _ viewer.newVersion; <> foundOne: BOOL _ FALSE; voiceWindowRope: Rope.ROPE _ Rope.Concat[voiceRopePrefix, Convert.RopeFromInt[voiceViewerNumber]]; TiogaOps.CallWithLocks[DoIt, rootNode]; IF NOT foundOne AND exceptionLoc.node=NIL THEN { <> DebugRope["Source marker not found in expected document!\n"]; TiogaVoicePrivate.RemoveParentViewer[TiogaVoicePrivate.GetVoiceViewerInfo[voiceViewerNumber]]; } ELSE IF ~alreadyEdited THEN { viewer.newVersion _ FALSE; ViewerOps.PaintViewer[viewer, caption, FALSE]; }; }; SaveRopeAtSourceMarkers: PUBLIC PROC [viewer: ViewerClasses.Viewer, voiceViewerNumber: INT, voiceRopeID: Rope.ROPE, textInVoice: Rope.ROPE] RETURNS [succeeded: BOOL] = { <> SaveRopeAtSourceMarker: LocActionProc = { <> IF CheckVoiceWindowProp[propVal: voiceWindowRope, charProp: charProp] THEN { TextEdit.PutCharProp[node, offset, $voice, voiceRopeID]; TextEdit.PutCharProp[node, offset, $textInVoice, textInVoice]; succeeded _ TRUE; }; }; DoIt: PROC [root: TiogaOps.Ref] = { MapCharProps[viewer: viewer, propName: $voiceWindow, actionProc: SaveRopeAtSourceMarker]; }; rootNode: TiogaOps.Ref _ TiogaOps.ViewerDoc[viewer]; voiceWindowRope: Rope.ROPE _ Rope.Concat[voiceRopePrefix, Convert.RopeFromInt[voiceViewerNumber]]; succeeded _ FALSE; TiogaOps.CallWithLocks[DoIt, rootNode]; IF NOT succeeded THEN -- since it wasn't there, don't bother to look again TiogaVoicePrivate.RemoveParentViewer[TiogaVoicePrivate.GetVoiceViewerInfo[voiceViewerNumber]]; }; ScanForVoice: TiogaOps.CommandProc = { IF viewer.newFile OR viewer.newVersion THEN RETURN[quit: RecordVoiceInstancesAtRoot[viewer]]; }; RecordVoiceInstancesAtRoot: PROC [viewer: ViewerClasses.Viewer] RETURNS [quit: BOOL _ FALSE] = { rootRef: TiogaOps.Ref _ TiogaOps.ViewerDoc[viewer]; voiceList: Rope.ROPE _ NIL; postfixProp: Rope.ROPE _ NARROW[TiogaOps.GetProp[rootRef, $Postfix]]; DoIt: PROC [root: TiogaOps.Ref] = { TiogaOps.PutProp[root, $voicelist, voiceList]; TiogaOps.PutProp[root, $Postfix, postfixProp]; }; soundsInDocument: INT _ 0; userConfirmed: BOOL _ FALSE; DebugRope["Voice messages in document:"]; FOR n: TextNode.Ref _ TiogaButtons.TextNodeRef[rootRef], TextNode.StepForward[n] UNTIL n = NIL DO FOR i: INT IN [0..TextEdit.Size[n]) DO voiceRope: Rope.ROPE; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo _ ValidateSourceMarker[n, i]; IF viewerInfo#NIL THEN { <> IF NOT userConfirmed AND viewerInfo.edited THEN { IF NOT MessageWindow.Confirm["Confirm discard of voice edits . . . "] THEN { MessageWindow.Append["Save aborted."]; RETURN[TRUE]; } ELSE userConfirmed _ TRUE; }; }; voiceRope _ NARROW[TextEdit.GetCharProp[n, i, $voice]]; IF voiceRope#NIL THEN { DebugRope["\n"]; DebugRope[voiceRope]; soundsInDocument _ soundsInDocument + 1; voiceList _ Rope.Cat[voiceList, "&", voiceRope]; }; ENDLOOP; ENDLOOP; IF soundsInDocument # 0 THEN { IF Rope.Find[postfixProp, "(TiogaVoiceLoaded)"] = -1 THEN postfixProp _ Rope.Concat[checkTiogaVoiceLoaded, postfixProp]; TiogaOps.CallWithLocks[DoIt, rootRef]; DebugRope["\n"]; TiogaVoicePrivate.RegisterViewerInterests[viewer, voiceList]; } ELSE DebugRope[" none\n"]; }; DeleteLinks: TiogaOps.CommandProc = { <> <> RemoveAnySourceMarker: LocActionProc = { <> TextEdit.PutCharProp[node, offset, $voiceWindow, NIL]; }; DoIt: PROC [root: TiogaOps.Ref] = { MapCharProps[viewer: viewer, propName: $voiceWindow, actionProc: RemoveAnySourceMarker]; }; rootNode: TiogaOps.Ref _ TiogaOps.ViewerDoc[viewer]; alreadyEdited: BOOL _ viewer.newVersion; <> TiogaOps.CallWithLocks[DoIt, rootNode]; IF ~alreadyEdited THEN { viewer.newVersion _ FALSE; ViewerOps.PaintViewer[viewer, caption, FALSE]; }; TiogaVoicePrivate.RemoveParentPointersTo[viewer: viewer, findSplits: FALSE]; }; checkTiogaVoiceLoaded: Rope.ROPE _ "IF (TiogaVoiceLoaded) .where {.pop .false}.cvx {.true}.cvx .ifelse THEN {\"Document contains voice; load TiogaVoice to see annotations\" 1 ReportStyleError} FI "; InstallMenuButton: PROC [name: Rope.ROPE, proc: Menus.MenuProc, clientData: REF ANY _ NIL] = { old: Menus.MenuEntry = Menus.FindEntry[menu: TiogaMenuOps.tiogaMenu, entryName: name]; new: Menus.MenuEntry = Menus.CreateEntry[name: name, proc: proc, clientData: clientData]; IF old = NIL THEN Menus.AppendMenuEntry[TiogaMenuOps.tiogaMenu, new] ELSE Menus.ReplaceMenuEntry[TiogaMenuOps.tiogaMenu, old, new]; }; voiceMenu: Menus.MenuEntry; voiceButtonQueue: PUBLIC MBQueue.Queue _ MBQueue.Create[]; <> <<>> CreateTiogaViewerMenu: PROC ~ { initVoiceMenu: Menus.Menu _ Menus.CreateMenu[]; <<**** voiceButtonQueue should be deleted, or used for ALL button activities>> Menus.AppendMenuEntry[initVoiceMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "AddVoice", TiogaInterpret, $TVAddVoice]]; Menus.AppendMenuEntry[initVoiceMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "PlayVoice", TiogaInterpret, $TVPlayVoice]]; Menus.AppendMenuEntry[initVoiceMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "STOP", TiogaInterpret, $TVSTOP]]; Menus.AppendMenuEntry[initVoiceMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "Pause/Resume", TiogaInterpret, $TVPause]]; Menus.AppendMenuEntry[initVoiceMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "EditVoice", TiogaInterpret, $TVEditVoice]]; Menus.AppendMenuEntry[initVoiceMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "DeleteVoice", TiogaInterpret, $TVDeleteVoice]]; Menus.AppendMenuEntry[initVoiceMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "DictationViewer", TiogaInterpret, $TVDictationViewer]]; Menus.AppendMenuEntry[initVoiceMenu, MBQueue.CreateMenuEntry[ q: voiceButtonQueue, name: "DeleteLinks", proc: TiogaInterpret, clientData: $TVDeleteLinks, guarded: TRUE, documentation: "Confirm orphaning all voice viewers created from this document"]]; voiceMenu _ Menus.GetLine[initVoiceMenu, 0]; InstallMenuButton["Voice", TiogaInterpret, $TVVoiceMenu]; }; RegisterButtonBehaviors: PROC ~ { <> TiogaOps.RegisterCommand[name: $RedSave, proc: ScanForVoice]; TiogaOps.RegisterCommand[name: $YellowSave, proc: ScanForVoice]; TiogaOps.RegisterCommand[name: $BlueSave, proc: ScanForVoice]; TiogaOps.RegisterCommand[name: $RedStore, proc: ScanForVoice]; TiogaOps.RegisterCommand[name: $YellowStore, proc: ScanForVoice]; TiogaOps.RegisterCommand[name: $BlueStore, proc: ScanForVoice]; <> TiogaOps.RegisterCommand[name: $TVVoiceMenu, proc: VoiceMenu]; TiogaOps.RegisterCommand[name: $TVAddVoice, proc: TiogaVoicePrivate.AddVoiceProc]; TiogaOps.RegisterCommand[name: $TVPlayVoice, proc: TiogaVoicePrivate.PlayBackMenuProc]; TiogaOps.RegisterCommand[name: $TVSTOP, proc: TiogaVoicePrivate.CancelProc]; TiogaOps.RegisterCommand[name: $TVPause, proc: TiogaVoicePrivate.PauseProc]; TiogaOps.RegisterCommand[name: $TVEditVoice, proc: EditVoiceProc]; TiogaOps.RegisterCommand[name: $TVDeleteVoice, proc: DeleteVoiceProc]; TiogaOps.RegisterCommand[name: $TVDictationViewer, proc: TiogaVoicePrivate.DictationMachine]; TiogaOps.RegisterCommand[name: $TVDeleteLinks, proc: DeleteLinks]; }; <<>> TiogaInterpret: PUBLIC Menus.MenuProc ~ { <> ViewerOps.AddProp[parent, $ButtonParams, NEW[TiogaVoicePrivate.ButtonParamsBody _ [mouseButton, shift, control]]]; TiogaOps.Interpret[parent, LIST[clientData]]; }; <> <<>> BlinkMessageWindow: PROC [r: Rope.ROPE] ~ { MessageWindow.Append[r, TRUE]; MessageWindow.Blink[]; }; AppendRope: PROC [list: LIST OF Rope.ROPE, entry: Rope.ROPE] RETURNS [LIST OF Rope.ROPE] = { oneElementList: LIST OF Rope.ROPE _ CONS[entry, NIL]; hangOffPoint: LIST OF Rope.ROPE _ list; IF list = NIL THEN RETURN [oneElementList]; WHILE hangOffPoint.rest # NIL DO hangOffPoint _ hangOffPoint.rest ENDLOOP; hangOffPoint.rest _ oneElementList; RETURN [list] }; AppendViewerInfo: PROC [list: LIST OF TiogaVoicePrivate.VoiceViewerInfo, entry: TiogaVoicePrivate.VoiceViewerInfo] RETURNS [LIST OF TiogaVoicePrivate.VoiceViewerInfo] = { oneElementList: LIST OF TiogaVoicePrivate.VoiceViewerInfo _ CONS[entry, NIL]; hangOffPoint: LIST OF TiogaVoicePrivate.VoiceViewerInfo _ list; IF list = NIL THEN RETURN [oneElementList]; WHILE hangOffPoint.rest # NIL DO hangOffPoint _ hangOffPoint.rest ENDLOOP; hangOffPoint.rest _ oneElementList; RETURN [list] }; debugStream: IO.STREAM _ NIL; DebugRope: PUBLIC PROC [rope: Rope.ROPE] = { IF debugStream # NIL THEN debugStream.PutF[rope] }; DebugStreamInit: Commander.CommandProc = { debugStream _ cmd.out }; <> <<>> <> <<>> TalksBubbleDataRep: TYPE ~ RECORD [ letter: ImagerFont.XChar, label: Rope.ROPE, ascent: REAL, descent: REAL, width: REAL, bearoff: REAL, firstCharWidth: REAL ]; TalksBubblePaint: PROC [self: TEditFormat.CharacterArtwork, context: Imager.Context] ~ { data: REF TalksBubbleDataRep ~ NARROW[self.data]; DrawBubble: ImagerPath.PathProc = { height: REAL _ data.ascent + data.descent; moveTo[[data.bearoff, -data.descent+(height/4)]]; lineTo[[data.bearoff, -data.descent+(3*height/4)]]; arcTo[[data.bearoff+(data.width/2), -data.descent+height], [data.bearoff + data.width, -data.descent+(3*height/4)]]; lineTo[[data.bearoff+data.width, -data.descent+(height/4)]]; arcTo[[data.bearoff+(15*data.width/16), -data.descent+(height/16)], [data.bearoff + (3*data.width/4), -data.descent]]; lineTo[[data.bearoff+(data.width/4), -data.descent-(height/2)]]; lineTo[[data.bearoff+(data.width/2), -data.descent]]; lineTo[[data.bearoff+(data.width/4), -data.descent]]; arcTo[[data.bearoff+(data.width/16), -data.descent+(height/16)], [data.bearoff, -data.descent+(height/4)]]; }; DrawBox: ImagerPath.PathProc = { moveTo[[data.bearoff+data.firstCharWidth, -data.descent]]; lineTo[[data.bearoff+data.firstCharWidth, data.ascent]]; lineTo[[data.bearoff+data.width, data.ascent]]; lineTo[[data.bearoff+data.width, -data.descent]]; lineTo[[data.bearoff+data.firstCharWidth, -data.descent]]; }; Inner: PROC = { Imager.Move[context]; Imager.ShowXChar[context, data.letter]; IF data.label = NIL THEN { Imager.SetStrokeWidth[context, 1.0]; Imager.SetStrokeJoint[context, round]; Imager.MaskStroke[context, DrawBubble, TRUE]; } ELSE { Imager.SetColor[context, textColor]; Imager.SetAmplifySpace[context, 1.0]; Imager.ShowRope[context, data.label]; Imager.SetStrokeWidth[context, 1.0]; Imager.SetStrokeJoint[context, round]; Imager.MaskStroke[context, DrawBox, TRUE]; } }; Imager.DoSave[context, Inner]; }; TalksBubbleFormat: PROC [class: TEditFormat.CharacterArtworkClass, loc: TextNode.Location, style: NodeStyle.Ref, kind: NodeStyleOps.OfStyle] RETURNS [TEditFormat.CharacterArtwork] ~ { font: ImagerFont.Font _ TEditFormat.GetFont[style]; brightness: REAL _ NodeStyle.GetReal[style, textBrightness]; ascent: REAL _ NodeStyle.GetReal[style, backgroundAscent]; descent: REAL _ NodeStyle.GetReal[style, backgroundDescent]; bearoff: REAL _ NodeStyle.GetReal[style, outlineboxBearoff]; label: Rope.ROPE _ NIL; letter: ImagerFont.XChar; width, firstCharWidth: REAL; escapement: Vector2.VEC; char: CHAR; [charSet: letter.set, char: char] _ TextEdit.FetchChar[loc.node, loc.where]; letter.code _ ORD[char]; escapement _ ImagerFont.Escapement[font, letter]; firstCharWidth _ escapement.x; IF ValidateSourceMarker[loc.node, loc.where, TRUE]#NIL THEN <> label _ GetVoiceWindowRope[loc.node, loc.where]; IF label # NIL THEN escapement.x _ escapement.x + ImagerFont.RopeEscapement[font, label].x; width _ escapement.x; IF ascent+descent <= 0.0 THEN { fontBoundingBox: ImagerFont.Extents ~ ImagerFont.FontBoundingBox[font]; ascent _ fontBoundingBox.ascent + bearoff; descent _ fontBoundingBox.descent - bearoff; }; { data: REF TalksBubbleDataRep ~ NEW[TalksBubbleDataRep _ [ letter: letter, label: label, ascent: ascent, descent: descent, width: width, bearoff: bearoff, firstCharWidth: firstCharWidth ]]; extents: ImagerFont.Extents _ [leftExtent: -data.bearoff+2.0, rightExtent: data.bearoff+data.width+2.0, ascent: data.ascent+2.0, descent: (data.descent+data.ascent)/2]; RETURN [NEW[TEditFormat.CharacterArtworkRep _ [paint: TalksBubblePaint, extents: extents, escapement: escapement, data: data]]] } }; talksBubbleClass: TEditFormat.CharacterArtworkClass ~ NEW[TEditFormat.CharacterArtworkClassRep _ [ name: $TalksBubble, format: TalksBubbleFormat, data: NIL ]]; textColor: ImagerColor.ConstantColor _ ImagerColor.ColorFromStipple[7BDEH, [or, null]]; <<>> <> <<>> <> <> ViewerAndDestroyProc: TYPE = RECORD [ viewer: ViewerClasses.Viewer, destroyProc: ViewerEvents.EventRegistration ]; registeredViewerList: LIST OF ViewerAndDestroyProc; <> RegisterViewer: PUBLIC PROC [viewer: ViewerClasses.Viewer] = { <> alreadyInList: BOOLEAN _ FALSE; FOR l: LIST OF ViewerAndDestroyProc _ registeredViewerList, l.rest WHILE l # NIL AND ~alreadyInList DO IF l.first.viewer = viewer THEN alreadyInList _ TRUE ENDLOOP; IF ~alreadyInList THEN { newEntry: ViewerAndDestroyProc _ [viewer, ViewerEvents.RegisterEventProc[ proc: DestroyViewerEvent, event: destroy, filter: viewer, before: TRUE]]; registeredViewerList _ CONS [newEntry, registeredViewerList] } }; DestroyViewerEvent: ViewerEvents.EventProc = { firstEntry: BOOLEAN _ viewer = registeredViewerList.first.viewer; previousEntry: LIST OF ViewerAndDestroyProc _ registeredViewerList; IF ~firstEntry THEN WHILE previousEntry.rest.first.viewer # viewer DO previousEntry _ previousEntry.rest ENDLOOP; <> ViewerEvents.UnRegisterEventProc[(IF firstEntry THEN registeredViewerList ELSE previousEntry.rest).first.destroyProc, destroy]; IF firstEntry THEN registeredViewerList _ registeredViewerList.rest ELSE previousEntry.rest _ previousEntry.rest.rest; TiogaVoicePrivate.RemoveParentPointersTo[viewer: viewer, findSplits: TRUE]; }; <> TrackDeletes: PUBLIC PROC = { <> <> viewer: ViewerClasses.Viewer; start, end, current: TiogaOpsDefs.Location; infoList: LIST OF TiogaVoicePrivate.VoiceViewerInfo _ NIL; SeverLinks: PROC [node: TiogaOpsDefs.Ref, from, to: INT] = { FOR l: TiogaVoicePrivate.VoiceViewerInfoList _ infoList, l.rest WHILE l # NIL DO IF l.first.positionInParent.node = node AND l.first.positionInParent.where IN [from..to] THEN { l.first.parentViewer _ NIL; DebugRope[IO.PutFR["detaching parent link for voice viewer %d\n", IO.int[l.first.viewerNumber]]] } ENDLOOP }; [viewer: viewer, start: start, end: end] _ TiogaOps.GetSelection[]; infoList _ TiogaVoicePrivate.FindAttachedVoiceViewers[viewer]; IF infoList = NIL THEN RETURN; <<>> <> IF start.node = end.node THEN SeverLinks[start.node, start.where, end.where] ELSE { current _ start; SeverLinks[current.node, current.where, TextEdit.Size[TiogaButtons.TextNodeRef[current.node]]-1]; DO current.node _ TiogaOps.StepForward[current.node]; IF current.node = end.node THEN { SeverLinks[current.node, 0, end.where]; EXIT } ELSE SeverLinks[current.node, 0, TextEdit.Size[TiogaButtons.TextNodeRef[current.node]]-1] ENDLOOP }; { positionAdjustment: INT _ end.where - start.where + 1; FOR l: TiogaVoicePrivate.VoiceViewerInfoList _ infoList, l.rest WHILE l # NIL DO IF l.first.positionInParent.node = end.node AND l.first.positionInParent.where > end.where THEN l.first.positionInParent _ [start.node, l.first.positionInParent.where - positionAdjustment] ENDLOOP } }; <> <<>> WhereData: TYPE ~ REF WhereDataRec; WhereDataRec: TYPE ~ RECORD [ root: TiogaOps.Ref, node: TextNode.Ref, offset: INT ]; BreakVoiceWindowLinks: ViewerForkers.CallBack = { <> DoIt: PROC [root: TiogaOps.Ref] = { IF TextEdit.Size[whereData.node] > whereData.offset THEN TextEdit.PutCharProp[whereData.node, whereData.offset, $voiceWindow, NIL]; }; whereData: WhereData _ NARROW[data]; IF whereData.node#NIL THEN TiogaOps.CallWithLocks[DoIt, whereData.root]; }; ValidateSourceMarker: PROC [node: TextNode.Ref, offset: INT, locked: BOOL _ FALSE] RETURNS [viewerInfo: TiogaVoicePrivate.VoiceViewerInfo _ NIL] ~ { <> <> <<>> root: TiogaOps.Ref; v: TiogaVoicePrivate.VoiceWindowRef _ NARROW[TextEdit.GetCharProp[node, offset, $voiceWindow]]; IF v=NIL THEN RETURN; viewerInfo _ TiogaVoicePrivate.GetVoiceViewerInfo[GetVoiceViewerNumber[v.label]]; IF viewerInfo=NIL THEN RETURN; root _ TiogaButtons.TiogaOpsRef[TextNode.Root[node]]; IF viewerInfo.parentViewer=NIL OR root#TiogaOps.ViewerDoc[viewerInfo.parentViewer] THEN { <> whereData: WhereData _ NEW[WhereDataRec _ [root, node, offset]]; IF locked THEN ViewerForkers.ForkCall[proc: BreakVoiceWindowLinks, data: whereData] ELSE BreakVoiceWindowLinks[whereData]; <> viewerInfo _ NIL; }; }; GetVoiceViewerNumber: PROC [voiceLabel: Rope.ROPE] RETURNS [INT] = { RETURN[Convert.IntFromRope[voiceLabel.Substr[voiceLabel.Find["#"]+1]]]; }; GetVoiceWindowRope: PROC [node: TextNode.Ref, offset: INT] RETURNS [Rope.ROPE] ~ { v: TiogaVoicePrivate.VoiceWindowRef _ NARROW[TextEdit.GetCharProp[node, offset, $voiceWindow]]; IF v#NIL THEN RETURN[v.label] ELSE RETURN[NIL]; }; CheckPropProc: TYPE ~ PROC [propVal: REF, charProp: REF] RETURNS [BOOL]; LocActionProc: TYPE ~ PROC [node: TextNode.Ref, offset: INT, charProp: REF]; CheckVoiceWindowProp: CheckPropProc ~ { IF charProp#NIL THEN { v: TiogaVoicePrivate.VoiceWindowRef _ NARROW[charProp]; IF Rope.Equal[v.label, NARROW[propVal, Rope.ROPE]] THEN RETURN[TRUE]; }; RETURN [FALSE] }; MapCharProps: PROC [viewer: ViewerClasses.Viewer, propName: ATOM, actionProc: LocActionProc] ~ { <> rootNode: TextNode.Ref _ TiogaButtons.TextNodeRef[TiogaOps.ViewerDoc[viewer]]; -- TextNodeRef is just a type convertor - replace with a more cautious implementation?? FOR n: TextNode.Ref _ rootNode, TextNode.StepForward[n] UNTIL n = NIL DO IF n.hascharprops THEN FOR i: INT IN [0..TextEdit.Size[n]) DO charProp: REF _ TextEdit.GetCharProp[n, i, propName]; IF charProp#NIL THEN actionProc[n, i, charProp]; ENDLOOP; ENDLOOP; }; FindCharProp: PROC [viewer: ViewerClasses.Viewer, propName: ATOM, propVal: REF, checkPropProc: CheckPropProc, locHint: TiogaOpsDefs.Location] RETURNS [foundLoc: TiogaOpsDefs.Location] ~ { <> Search: PROC [startnode, endnode: TextNode.Ref] ~ { <> FOR n: TextNode.Ref _ startnode, TextNode.StepForward[n] UNTIL n = endnode DO startChar: INT _ 0; endChar: INT _ TextEdit.Size[n]; SearchCharProps: PROC RETURNS [found: BOOLEAN _ FALSE] ~ { FOR i: INT IN [startChar..endChar) UNTIL found DO charProp: REF _ TextEdit.GetCharProp[n, i, propName]; IF checkPropProc[propVal, charProp] THEN {foundLoc.where _ i; found _ TRUE;} ENDLOOP; }; IF n.hascharprops AND SearchCharProps[].found THEN { foundLoc.node _ TiogaButtons.TiogaOpsRef[n]; RETURN; }; ENDLOOP; }; hintNode, nextNode: TextNode.Ref _ NIL; foundLoc.node _ NIL; hintNode _ TiogaButtons.TextNodeRef[locHint.node]; IF TextNode.Root[hintNode]=NIL THEN hintNode _ NIL; <<Indicates hintNode has been deleted - shouldn't be needed if all else has gone well. PTZ, June 12, 1987 7:32:27 pm PDT>> IF hintNode#NIL THEN { IF locHint.where < TextEdit.Size[hintNode] AND checkPropProc[propVal, TextEdit.GetCharProp[hintNode, locHint.where, propName]] THEN RETURN [locHint]; nextNode _ TextNode.StepForward[hintNode]; Search[hintNode, nextNode]; IF foundLoc.node#NIL THEN RETURN; }; Search[TiogaButtons.TextNodeRef[TiogaOps.ViewerDoc[viewer]], hintNode]; IF foundLoc.node#NIL THEN RETURN; Search[nextNode, NIL]; -- = Search[NIL, NIL] if no locHint }; <> <<>> TJaM.Put[dict: NodeStyleWorks.styledict, key: $TiogaVoiceLoaded, val: $TRUE]; NodeStyleOps.FlushCaches[]; CreateTiogaViewerMenu[]; RegisterButtonBehaviors[]; Commander.Register[key: "TiogaVoice", proc: TiogaVoiceCmdProc, doc: "TiogaVoice: start package for voice annotation of Tioga documents"]; <<>> TEditFormat.RegisterCharacterArtwork[talksBubbleClass]; }. <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <>