<> <> << Ades, September 24, 1986 5:52:47 pm PDT>> <> <> <<>> DIRECTORY Atom USING [PropList], BasicTime USING [GMT], Commander USING [CommandProc, Register], CommandTool USING [ArgumentVector, Failed, Parse ], Convert USING [IntFromRope, RopeFromInt, RopeFromTime ], FileReader USING [FromStream], FS USING [defaultStreamOptions, Error, FileInfo, StreamOpen, StreamOptions ], FSBackdoor USING [RemoteEvent, NextRemoteEvent ], Imager USING [Color, Context, MaskStroke, Move, SetColor, SetStrokeJoint, SetStrokeWidth, ShowChar, ShowRope ], ImagerColor USING [ConstantColor, ColorFromStipple], ImagerFont USING [Extents, FontBoundingBox, Escapement ], ImagerPath USING [PathProc], IO USING [int, PutF, PutFR, rope, STREAM ], MBQueue USING [Create, CreateMenuEntry, Queue ], Menus USING [AppendMenuEntry, CreateEntry, FindEntry, GetLine, GetNumberOfLines, InsertMenuEntry, Menu, MenuEntry, MenuLine, MenuProc, ReplaceMenuEntry, SetLine ], MessageWindow USING [Append, Blink, Confirm], NodeStyle USING [GetReal, Ref ], NodeStyleOps USING [OfStyle], Process USING [Detach, priorityBackground, SetPriority ], Rope USING [Cat, Concat, Equal, Fetch, Find, Length, MaxLen, ROPE, Substr ], TEditFormat USING [CharacterArtwork, CharacterArtworkClass, CharacterArtworkClassRep, CharacterArtworkRep, GetFont, RegisterCharacterArtwork ], TextEdit USING [CharSet, FetchChar, GetCharProp, PutCharProp, PutProp, Size ], TextNode USING [Location, Ref, Root, StepForward], TiogaAccess USING [EndOf, FromFile, FromSelection, Get, Reader, TiogaChar ], TiogaButtons USING [TextNodeRef, TiogaOpsRef], TiogaMenuOps USING [tiogaMenu], TiogaOps USING [CallWithLocks, CommandProc, GetSelection, NoSelection, Ref, RegisterCommand, SelectionGrain, SetSelection, StepForward, ViewerDoc ], TiogaOpsDefs USING [Location, Ref], TiogaVoicePrivate USING [ AddVoiceProc, BuildVoiceViewer, CancelProc, DictationMachine, FindAttachedVoiceViewers, GetVoiceViewerInfo, MakeVoiceEdited, PlayBackMenuProc, PlayRopeWithoutCue, RemoveParentPointersTo, RemoveParentViewer, RopeFromTextList, SetParentViewer, VoiceViewerInfo, VoiceViewerInfoList, VoiceWindowRec, VoiceWindowRef ], Vector2 USING [VEC], ViewerBLT USING [ChangeNumberOfLines], ViewerClasses USING [Viewer], ViewerEvents USING [EventProc, EventRegistration, RegisterEventProc, UnRegisterEventProc ], ViewerForkers USING [ CallBack, ForkCall], ViewerOps USING [FetchProp, PaintViewer], VoiceRope USING [Handle, Length, Open, Retain, VoiceRope, VoiceRopeInterval ] ; VoiceInTextImpl: CEDAR PROGRAM IMPORTS Commander, CommandTool, Convert, FileReader, FS, FSBackdoor, Imager, ImagerColor, ImagerFont, IO, MBQueue, Menus, MessageWindow, NodeStyle, Process, Rope, TEditFormat, TextEdit, TextNode, TiogaAccess, TiogaButtons, TiogaMenuOps, TiogaOps, TiogaVoicePrivate, ViewerBLT, ViewerEvents, ViewerForkers, ViewerOps, VoiceRope EXPORTS TiogaVoicePrivate SHARES ViewerClasses = { <> VoiceMenu: Menus.MenuProc = { ChangeMenu[NARROW[parent], 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; [start: current, end: end] _ TiogaOps.GetSelection[]; 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] }; ApplyToLockedChars: PUBLIC PROC [ActionProc: PROC [TiogaOpsDefs.Location]] = { <> current, end: TiogaOpsDefs.Location; [start: current, end: end] _ TiogaOps.GetSelection[]; 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; alreadyVoiceThere: BOOLEAN; 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 alreadyVoiceThere _ TextEdit.GetCharProp[node, targetChar.where, $voice] # NIL; IF 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["Sound Viewer #", Convert.RopeFromInt[voiceViewerInfo.viewerNumber]]]]]; } }; IF voiceViewerInfo.ropeInterval.ropeID = NIL THEN RETURN; TiogaOps.CallWithLocks[AddVoiceMarkerAtPrimarySelection ! TiogaOps.NoSelection => {suitableViewer _ FALSE; CONTINUE}]; <> IF NOT suitableViewer THEN { MessageWindow.Append["Make a selection in a tioga viewer first", TRUE]; MessageWindow.Blink[] } ELSE { IF alreadyVoiceThere THEN { MessageWindow.Append["Cannot add sound on top of another sound; delete old sound if desired", TRUE]; MessageWindow.Blink[] } ELSE succeeded _ TRUE } }; DeleteVoiceProc: Menus.MenuProc = { ApplyToCharsInPrimarySelection[DeleteVoiceFromChar] }; DeleteVoiceFromChar: PUBLIC PROC [position: TiogaOpsDefs.Location] = { <> node: TextNode.Ref _ TiogaButtons.TextNodeRef[position.node]; offset: INT _ position.where; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo _ ValidateSourceMarker[node, offset]; 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]; <<need to see whether there are any more copies of this source marker/voice rope in this doc before breaking links or indicating edited>> TiogaVoicePrivate.MakeVoiceEdited[viewerInfo.viewer]; <> <> <> }; }; PlaySelection: PUBLIC PROC = { selectStream: TiogaAccess.Reader _ TiogaAccess.FromSelection[]; charsInSelection: INT _ 0; soundsInSelection: INT _ 0; heavyChar: TiogaAccess.TiogaChar; props: Atom.PropList; 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 { TiogaVoicePrivate.PlayRopeWithoutCue[NARROW[props.first.val, Rope.ROPE]]; soundsInSelection _ soundsInSelection + 1 }; ENDLOOP ENDLOOP; IF soundsInSelection = 0 THEN MessageWindow.Append["No sounds in selection", TRUE]; DebugRope[IO.PutFR["%d characters in selection\n", IO.int[charsInSelection]]] }; EditVoiceProc: Menus.MenuProc = { <> voiceList: LIST OF Rope.ROPE _ NIL; textInVoiceList: LIST OF Rope.ROPE _ NIL; newViewerInfoList: TiogaVoicePrivate.VoiceViewerInfoList _ NIL; <<>> 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 MessageWindow.Append["Non-existent or zero length voice utterance(s) found in selection", 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["Sound Viewer #", 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["No undisplayed sounds in selection", TRUE]; RETURN }; 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["Sound Viewer #", 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["Sound Viewer #", 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]]; }; DeleteLinks: Menus.MenuProc = { <> viewer: ViewerClasses.Viewer _ NARROW[parent]; 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]; }; RecordVoiceInstancesAtRoot: PROC [viewer: ViewerClasses.Viewer] RETURNS [quit: BOOL _ FALSE] = { <<Consider locking issue: this routine used to read the file with TiogaAccess, so didn't worry.>> rootNode: TextNode.Ref _ TiogaButtons.TextNodeRef[TiogaOps.ViewerDoc[viewer]]; voiceList: Rope.ROPE _ NIL; DoIt: PROC [root: TiogaOps.Ref] = { TextEdit.PutProp[rootNode, $voicelist, voiceList]; }; soundsInDocument: INT _ 0; userConfirmed: BOOL _ FALSE; DebugRope["Voice messages in document:"]; FOR n: TextNode.Ref _ rootNode, 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; TiogaOps.CallWithLocks[DoIt, TiogaButtons.TiogaOpsRef[rootNode]]; IF soundsInDocument = 0 THEN DebugRope[" none\n"] ELSE DebugRope["\n"] }; InstallMenuButton: PROC [name: Rope.ROPE, proc: Menus.MenuProc] = { old: Menus.MenuEntry = Menus.FindEntry[menu: TiogaMenuOps.tiogaMenu, entryName: name]; new: Menus.MenuEntry = Menus.CreateEntry[name: name, proc: proc]; IF old = NIL THEN Menus.AppendMenuEntry[TiogaMenuOps.tiogaMenu, new] ELSE Menus.ReplaceMenuEntry[TiogaMenuOps.tiogaMenu, old, new]; }; thrushHandle: PUBLIC VoiceRope.Handle _ VoiceRope.Open[]; voiceMenu: Menus.MenuEntry; voiceButtonQueue: PUBLIC MBQueue.Queue _ MBQueue.Create[]; <> <<>> <> <<>> 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 }; <> <<>> <> <<>> <> <<>> <> LocalFile: ERROR; backgroundCommentary: IO.STREAM _ NIL; lastRemoteEvent: REF READONLY FSBackdoor.RemoteEvent _ NIL; MonitorCopiesToGlobal: PROC = { -- invoked as a separate process Process.SetPriority[Process.priorityBackground]; DO lastRemoteEvent _ FSBackdoor.NextRemoteEvent[lastRemoteEvent]; IF lastRemoteEvent.op = endStoring THEN RegisterInterest[lastRemoteEvent.fName, backgroundCommentary] ENDLOOP }; RegisterInterest: PROC [file: Rope.ROPE, commentary: IO.STREAM _ NIL] = { fullFName, attachedTo, globalName: Rope.ROPE; keep: CARDINAL; voiceList: Rope.ROPE _ NIL; createDate: BasicTime.GMT; Commentate: PROC [remark: Rope.ROPE] = { IF commentary # NIL THEN commentary.PutF[remark] }; [fullFName: fullFName, attachedTo: attachedTo, keep: keep, created: createDate] _ FS.FileInfo[file]; -- read FS.Mesa to understand the next few lines Commentate[IO.PutFR["File %g:\n", IO.rope[fullFName]]]; IF attachedTo = NIL THEN { IF keep # 0 THEN ERROR LocalFile[] -- neither a global file or a local one with global attachment ELSE { globalName _ fullFName; Commentate["File is global:\n"] } } ELSE { globalName _ attachedTo; Commentate[IO.PutFR["File is attached to global file %g:\n", IO.rope[globalName]]] }; { fileStream: IO.STREAM; streamOptions: FS.StreamOptions _ FS.defaultStreamOptions; tiogaFile: BOOLEAN; <> streamOptions[tiogaRead] _ FALSE; fileStream _ FS.StreamOpen[fileName: fullFName, streamOptions: streamOptions]; [tiogaFile: tiogaFile] _ FileReader.FromStream[fileStream, LAST[INT]]; IF ~tiogaFile THEN { Commentate[" Not a tioga file\n"]; RETURN } }; { fileStream: TiogaAccess.Reader _ TiogaAccess.FromFile[fullFName]; rootChar: TiogaAccess.TiogaChar _ TiogaAccess.Get[fileStream]; -- first character produces the root properties FOR rootProps: Atom.PropList _ rootChar.propList, rootProps.rest WHILE rootProps # NIL DO IF rootProps.first.key = $voicelist THEN { IF voiceList = NIL THEN voiceList _ NARROW[rootProps.first.val, Rope.ROPE] ELSE ERROR } ENDLOOP }; IF voiceList = NIL THEN { Commentate[" No voice in file\n"]; RETURN }; Commentate[" Voice message IDs are\n"]; { nextVoice: Rope.ROPE; startOfID: INT _ 1; endOfID: INT; IF NOT ( voiceList.Length > 0 AND voiceList.Fetch[0] = '& ) THEN ERROR; DO endOfID _ voiceList.Find["&", startOfID]; nextVoice _ voiceList.Substr[startOfID, IF endOfID = -1 THEN Rope.MaxLen ELSE endOfID - startOfID]; IF nextVoice.Length = 0 THEN ERROR; <> VoiceRope.Retain[handle: thrushHandle, vr: NEW [VoiceRope.VoiceRopeInterval _ [nextVoice, 0, 0]], refID: globalName.Cat[" ", Convert.RopeFromTime[createDate]], class: "TiogaVoice"]; Commentate[nextVoice]; Commentate["\n"]; IF endOfID = -1 THEN EXIT; startOfID _ endOfID + 1 ENDLOOP } }; RegisterVoiceInterest: Commander.CommandProc = { { ENABLE { FS.Error => IF error.group # user THEN REJECT ELSE {msg _ error.explanation; GOTO Quit}; LocalFile => {msg _ "File must be global or have a global attachment"; GOTO Quit}; }; argv: CommandTool.ArgumentVector; argv _ CommandTool.Parse[cmd ! CommandTool.Failed => {msg _ errorMsg; GOTO Quit}]; IF argv.argc # 2 THEN { msg _ "Usage: RegisterVoiceInterest inFile"; GOTO Quit; }; RegisterInterest[argv[1], cmd.out] EXITS Quit => RETURN [$Failure, msg] }}; backgroundCommentaryInit: Commander.CommandProc = { backgroundCommentary _ cmd.out }; p: PROCESS _ FORK MonitorCopiesToGlobal; <<>> <> <<>> <> <<>> TalksBubbleDataRep: TYPE ~ RECORD [ letter: CHAR, 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]] }; Imager.Move[context]; Imager.ShowChar[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.ShowRope[context, data.label]; Imager.SetStrokeWidth[context, 1.0]; Imager.SetStrokeJoint[context, round]; Imager.MaskStroke[context, DrawBox, TRUE] } }; TalksBubbleFormat: PROC [class: TEditFormat.CharacterArtworkClass, loc: TextNode.Location, style: NodeStyle.Ref, kind: NodeStyleOps.OfStyle] RETURNS [TEditFormat.CharacterArtwork] ~ { charSet: TextEdit.CharSet _ TextEdit.FetchChar[loc.node, loc.where].charSet; letter: CHAR _ TextEdit.FetchChar[loc.node, loc.where].char; ascent: REAL _ NodeStyle.GetReal[style, backgroundAscent]; descent: REAL _ NodeStyle.GetReal[style, backgroundDescent]; width: REAL; bearoff: REAL _ NodeStyle.GetReal[style, outlineboxBearoff]; escapement: Vector2.VEC _ ImagerFont.Escapement[TEditFormat.GetFont[style], [set: charSet, code: letter-'\000]]; firstCharWidth: REAL _ escapement.x; label: Rope.ROPE _ NIL; IF ValidateSourceMarker[loc.node, loc.where, TRUE]#NIL THEN <> label _ GetVoiceWindowRope[loc.node, loc.where]; IF label # NIL THEN FOR i: INT IN [0..label.Length) DO escapement.x _ escapement.x + ImagerFont.Escapement[TEditFormat.GetFont[style], [set: charSet, code: label.Fetch[i]-'\000]].x ENDLOOP; width _ escapement.x; IF ascent+descent <= 0.0 THEN { fontBoundingBox: ImagerFont.Extents ~ ImagerFont.FontBoundingBox[TEditFormat.GetFont[style]]; 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] = { TextEdit.PutCharProp[whereData.node, whereData.offset, $voiceWindow, NIL]; }; whereData: WhereData _ NARROW[data]; 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 }; <> <<>> <<**** voiceButtonQueue should be deleted, or used for ALL button activities>> Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "DeleteLinks", DeleteLinks], 1]; Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "DictationMachine", TiogaVoicePrivate.DictationMachine], 1]; Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "DeleteVoice", DeleteVoiceProc], 1]; Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "EditVoice", EditVoiceProc], 1]; Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "STOP", TiogaVoicePrivate.CancelProc], 1]; Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "PlayVoice", TiogaVoicePrivate.PlayBackMenuProc], 1]; Menus.InsertMenuEntry[TiogaMenuOps.tiogaMenu, MBQueue.CreateMenuEntry[voiceButtonQueue, "AddVoice", TiogaVoicePrivate.AddVoiceProc], 1]; voiceMenu _ Menus.GetLine[TiogaMenuOps.tiogaMenu, 1]; InstallMenuButton["Voice", VoiceMenu]; Menus.SetLine[TiogaMenuOps.tiogaMenu, 1, NIL]; 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]; TRUSTED {Process.Detach[p]}; Commander.Register[key: "RegisterVoiceInterest", proc: RegisterVoiceInterest, doc: "RegisterVoiceInterest inFile: register loganberry interests for all voice messages in a global or globally attached file"]; Commander.Register[key: "InterestInfo", proc: backgroundCommentaryInit, doc: "InterestInfo: registers an output stream for commentary about voice interest registration"]; TEditFormat.RegisterCharacterArtwork[talksBubbleClass]; }. <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <<>>