DIRECTORY Commander USING [CommandProc, Register], Convert USING [IntFromRope, RopeFromInt ], Imager USING [Color, Context, DoSave, MaskStroke, Move, SetAmplifySpace, SetColor, SetStrokeJoint, SetStrokeWidth, ShowRope, ShowXChar ], ImagerColor USING [ConstantColor], ImagerColorPrivate USING [ColorFromStipple], ImagerFont USING [Escapement, Extents, Font, FontBoundingBox, RopeEscapement, XChar ], ImagerPath USING [PathProc], IO USING [int, PutFR1, PutRope, 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 [GetFont, GetReal, Style], NodeStyleOps USING [FlushCaches], NodeStyleWorks USING [styledict], Prop USING [ PropList ], Rope USING [Cat, Concat, Equal, Find, MaxLen, ROPE, Substr ], TEditFormat USING [CharacterArtwork, CharacterArtworkClass, CharacterArtworkClassRep, CharacterArtworkRep, RegisterCharacterArtwork ], TextEdit USING [FetchChar, GetCharProp, PutCharProp, Size ], TextNode USING [Ref, Root, StepForward], Tioga USING [Node, Location ], TiogaAccess USING [EndOf, Get, Reader, TiogaChar ], TiogaAccessViewers USING [FromSelection ], 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, ImagerColorPrivate, ImagerFont, IO, MBQueue, Menus, MessageWindow, NodeStyle, NodeStyleOps, NodeStyleWorks, Rope, TEditFormat, TextEdit, TextNode, TiogaAccess, TiogaAccessViewers, 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[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[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[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[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 ¬ 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 ¬ 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]; }; }; }; PlaySelection: PUBLIC PROC = { selectStream: TiogaAccess.Reader ¬ TiogaAccessViewers.FromSelection[]; charsInSelection: INT ¬ 0; soundsInSelection: INT ¬ 0; heavyChar: TiogaAccess.TiogaChar; props: Prop.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.PutFR1["%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 ¬ 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 ¬ 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=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 ¬ 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[]; 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.PutRope[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: Tioga.Location, style: NodeStyle.Style] RETURNS [TEditFormat.CharacterArtwork] ~ { font: ImagerFont.Font ¬ NodeStyle.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; letter ¬ TextEdit.FetchChar[loc.node, loc.where]; 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 ¬ ImagerColorPrivate.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.PutFR1["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[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[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 ¬ 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 ¬ 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.charProps#NIL 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.charProps=NIL AND SearchCharProps[].found THEN { foundLoc.node ¬ n; RETURN; }; ENDLOOP; }; hintNode, nextNode: TextNode.Ref ¬ NIL; foundLoc.node ¬ NIL; hintNode ¬ locHint.node; IF TextNode.Root[hintNode]=NIL THEN hintNode ¬ NIL; 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[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]; }. ? VoiceInTextImpl.mesa Copyright Ó 1987, 1992 by Xerox Corporation. All rights reserved. Ades, September 24, 1986 5:52:47 pm PDT Swinehart, June 7, 1992 9:43 am PDT Polle Zellweger, December 13, 1990 5:48 pm PST TiogaVoicePrivate dummy TiogaVoice command to allow TiogaVoice.load to live in ///xxx/Commands/ PROC [viewer: Viewer _ NIL] RETURNS [recordAtom: BOOL _ TRUE, quit: BOOL _ FALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams basic code to add voice to textual Tioga documents and to replay that voice this just toggles the voice submenu on and off the screen see comments in interface about this see if already showing the submenu add it. do insertion sort to get it in the right place higher priority means goes above in series of submenus looks at rope for first item to identify the subMenu. put it here a convenience: looks after all the tree walking and calls the given actionProc once for each character in the primary selection this is the same as above, except that it should be called when the primary selection is already locked procedure to put voice from a voice viewer into a text viewer point selection with no following char, try preceding char if there is one next line places a 'talks bubble' on the selected character - see TalksBubbleImpl Make sure that a $voiceWindow property never points to a viewerInfo with parentViewer=NIL unless you really mean it (important for ValidateSourceMarker). test for failure conditions and report them to the user after releasing the viewer lock PROC [viewer: Viewer _ NIL] RETURNS [recordAtom: BOOL _ TRUE, quit: BOOL _ FALSE]; viewer prop $ButtonParams = TiogaVoicePrivate.ButtonParams Called under TiogaOps locks. 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. TiogaVoicePrivate.MakeVoiceEdited[viewerInfo.viewer]; TiogaVoicePrivate.RemoveParentViewer[viewerInfo]; PROC [viewer: Viewer _ NIL] RETURNS [recordAtom: BOOL _ TRUE, quit: BOOL _ FALSE]; viewer prop $ButtonParams = LIST[mouseButton, shift, control] this procedure is in three parts: first look in the primary selection for characters with the property $voice but not $voiceWindow; next open up a window for each: then add $voiceWindow to the relevant characters. If the expected characters don't occur in the expected order then voice window(s) may be created without association to the parent viewer. because we are under a Tioga lock, it is safe to muck with the 'edited' status of the viewer philosophically, putting a source marker in a voice viewer does not constitute editing it, so keep the 'edited' status of the viewer constant through this operation We just broke all invalid links in SearchForVoice above, so this is just an already open voice viewer. Make sure that a $voiceWindow property never points to a viewerInfo with parentViewer=NIL unless you really mean it (important for ValidateSourceMarker). we only move down the voice (etc.) lists if the char property voice rope ID matched the head of the voice list. A non-match may be due to the rope being non-existent (and hence having been filtered out from voiceList by SearchForVoice). The other possibility is that the selection has been changed, in which case voice viewers may or may not be given correct links to their parents Called when a voice viewer is stored (so old links should be broken) or destroyed. If exceptionLoc is not NIL, it represents a marker we just added, so don't delete it. This routine is called with the voice's parent viewer, so all links we find are guaranteed to be valid. PROC [node: TextNode.Ref, offset: INT, charProp: REF] may be that the source marker was put in as a result of DictationMachine being bugged, in which case there is no voice rope present - remove the artwork see comments about 'edited' status at head of procedure AddVoiceWindowProps It wasn't there and we aren't in the process of inserting a new one, so don't bother to look again. Called when a voice viewer is saved. This routine is called with the voice's parent viewer, so all links we find are guaranteed to be valid. PROC [node: TextNode.Ref, offset: INT, charProp: REF] The voiceWindow is really linked to this doc, so if the voiceWindow has been edited, ask user to confirm save without those edits. PROC [viewer: Viewer _ NIL] RETURNS [recordAtom: BOOL _ TRUE, quit: BOOL _ FALSE]; viewer prop $ButtonParams = LIST[mouseButton, shift, control] this procedure severs any parent pointers that voice viewers might have back to this viewer and removes the source marker artworks. [It can also be used to get rid of source marker artworks embarrassingly left in a file by bad tracking etc.] Needn't worry about whether or not these links are valid. PROC [node: TextNode.Ref, offset: INT, charProp: REF] see comments about 'edited' status at head of procedure AddVoiceWindowProps all buttons registered using MBQueue.CreateMenuEntry are serialised on this one queue: all the buttons in this voice system, or having to do with voice [e.g. Finch buttons], ought to be put on this queue, except for those which simply toggle menus on and off the display **** voiceButtonQueue should be deleted, or used for ALL button activities modifications to Tioga menu commands TiogaVoice text viewer menu commands. These are registered with Tioga so that other clients can use Tioga's extension mechanism to call their own procedures before or after these. Clients can find the button parameters in the viewer property $ButtonParams as type TiogaVoicePrivate.ButtonParams. PROC [parent: Viewer, clientData: REF ANY _ NIL, mouseButton: MouseButton _ red, shift, control: BOOL _ FALSE] Utilities TalksBubbles routines to insert distinctive markers around a character to indicate that it contains voice Can't remove the $voiceWindow property from inside a paintproc, because it locks the document for reading. SourceMarkerTracking code that ensures that the records of parent viewers & positions within them attached to voice viewers are kept consistent as the parent viewers are edited first section deals with what happens when you delete a text viewer which is pointed to by voice viewer(s) this is a list of all the viewers in which source markers have been created and which therefore have a destroyProc set up to sever parent links. When the destroyProc is called the viewer will be deleted from this list; however when all relevant source markers have been deleted from the viewer we do not bother to delete it from the list called everytime a source marker is created within a text viewer if this runs off the end then something has gone seriously wrong remainder of the code deals with tracking pointers correctly as the text they point to is edited called before the primary selection is deleted, if in a text viewer N.B. that the selection may span more than one node we now have a list of all the voice viewers whose parent links point at the viewer containing the primary selection: we need to go through all the nodes in the selection severing the links which point within the selection and altering those which lie in the last node of the selection, beyond the end of the selection Utilities CallBack: TYPE = PROC [data: REF] If this character has a $voiceWindow property and the associated voice viewer believes it's connected to this document, then return the viewerInfo for that viewer. Otherwise remove the property and return NIL. Links from text documents to voice viewers do not survive copies/moves to different documents because it would be too hard to search every visible document for a possible link. Two other options would be possible: Tioga could provide help via a callback when properties are moved or copied, or TiogaVoice could monitor all moves, copies, and undos. Break the link from the text document to the voice viewer. Can't break the link in the other direction (from viewerInfo) because we don't know whether this character got here by a move or a copy. Can't simply remove the $voiceWindow property from inside a paintproc, because it locks the document for reading. Maps actionProc over all non-NIL occurrences of character property propName in the document represented by viewer. Searches document for occurrence of character property propName with value propVal. Starts with locHint if specified, then locHint.node, then searches forward from the beginning, skipping locHint.node. Search nodes [startnode..endnode) for property. No search if startnode=endnode. Indicates hintNode has been deleted - shouldn't be needed if all else has gone well. PTZ, June 12, 1987 7:32:27 pm PDT Initialization Swinehart, April 9, 1987 10:42:03 am PDT Gathers up VoiceInTextImpl, VoiceInterestImpl, TalksBubbleImpl, SourceMarkerTrackingImpl Polle Zellweger (PTZ) June 11, 1987 4:11:26 pm PDT Treat VoiceViewerInfo.positionInParent as only a hint for location of associated SourceMarker; untracked edits may change location. changes to: VoiceInTextImpl, DeleteSourceMarker, SaveRopeAtSourceMarker, GetVoiceWindowRope, CheckPropProc, CheckVoiceWindowProp, FindCharProp, Search(local of FindCharProp), SearchCharProps (local of SearchNode, local of FindCharProp) Polle Zellweger (PTZ) June 12, 1987 7:32:46 pm PDT changes to: FindCharProp Polle Zellweger (PTZ) June 17, 1987 11:11:34 am PDT Link to sibling split text viewer if appropriate when text viewer destroyed. Set voice viewer status=edited when voice deleted from doc (pessimistic view; might be stored elsewhere). changes to: DIRECTORY, DeleteVoiceFromChar, RemoveParentPointersTo Polle Zellweger (PTZ) June 18, 1987 2:16:09 pm PDT changes to: DIRECTORY, ScanForVoice (tell Tioga if Save should abort), RecordVoiceInstancesAtRoot (request user confirmation if there are linked unsaved voice viewers), FindCharProp Polle Zellweger (PTZ) June 20, 1987 11:20:18 pm PDT Changes for new policy: voice markers in text viewers are only a hint; must be validated (whenever possible: painting talks bubbles, saving doc, etc.). Voice markers can't be copied or moved across doc boundaries. changes to: DIRECTORY, DeleteVoiceFromChar, SearchForVoice, AddVoiceWindowProps, DeleteSourceMarkers, GetVoiceWindowRope, SaveRopeAtSourceMarkers, DeleteLinks, RemoveAnySourceMarker, RecordVoiceInstancesAtRoot, TalksBubbleFormat, new: GetVoiceViewerNumber, ValidateSourceMarker, MapCharProps Polle Zellweger (PTZ) July 10, 1987 2:02:35 pm PDT Handling cases where voiceViewerInfo.parentViewer=NIL inside ValidateSourceMarker: reduce cases by monotonically increasing voice viewer numbers (VoiceViewersImpl) and by care under EditVoiceProc. Also make voiceViewerInfoList private to VoiceViewersImpl. changes to: DIRECTORY, EditVoiceProc, AddVoiceWindowProps (local of EditVoiceProc), DestroyViewerEvent, SeverLinks (local of TrackDeletes), TrackDeletes, DeleteLinks, AppendViewerInfo, ValidateSourceMarker Polle Zellweger (PTZ) July 17, 1987 6:12:25 pm PDT Move (w/changes) CancelProc to VoiceRecordImpl - part of progress/connection mgmt cleanup. changes to: DIRECTORY, CancelProc Polle Zellweger (PTZ) July 21, 1987 12:14:36 pm PDT changes to: DIRECTORY Polle Zellweger (PTZ) August 5, 1987 11:25:32 am PDT changes to: Dummy, VoiceMenu, Commander, TEditFormat Polle Zellweger (PTZ) December 29, 1987 11:51:43 am PST Talks bubble painting: spaces in open voice viewer indicators ("Sound Viewer #n") were being stretched by later Tioga justification, so the words were falling outside the rectangle onto other characters in the line. Set space size & enclose talks bubble painting in an Imager.DoSave. changes to: voiceRopePrefix, ApplyToLockedChars, AddVoiceMarkerAtPrimarySelection, AddVoiceWindowProps (local of EditVoiceProc), DeleteSourceMarkers, SaveRopeAtSourceMarkers, DrawBox (local of TalksBubblePaint), Inner (local of TalksBubblePaint), TalksBubblePaint Polle Zellweger (PTZ) January 11, 1988 6:36:13 pm PST Make docs with voice complain if opened when TiogaVoice not loaded (uses style machinery & Postfix properties). Also change color of open voice annotation bubbles (trying to make more readable) to textColor w/ textBrightness increased (or decreased) by .2. changes to: DIRECTORY, VoiceInTextImpl, RecordVoiceInstancesAtRoot, checkTiogaVoiceLoaded, TalksBubbleDataRep, Inner (local of TalksBubblePaint), TalksBubbleFormat, bubbleBrightness Polle Zellweger (PTZ) January 12, 1988 10:40:28 am PST textColor changes weren't an improvement, no more readable, & they changed as you scrolled the doc (because they weren't stipples). changes to: DIRECTORY, VoiceInTextImpl, TalksBubbleDataRep, Inner (local of TalksBubblePaint), TalksBubbleFormat, talksBubbleClass, textColor Polle Zellweger (PTZ) January 12, 1988 7:26:27 pm PST Fix bug: Store voice viewer at point selection with no following char gives an ERROR. changes to: StoreVoiceAtSelection, SuitableViewer (local of StoreVoiceAtSelection), AddVoiceMarkerAtPrimarySelection (local of StoreVoiceAtSelection), BlinkMessageWindow, DeleteVoiceProc, AddVoiceWindowProps (local of EditVoiceProc), RegisterInterest Polle Zellweger (PTZ) January 21, 1988 4:02:22 pm PST Catch FS.Error on first file access in RegisterInterest and bail out. Omit setting voice viewer edited bit if voice viewer is open (correctly this time). Guard DeleteLinks menu button. Protect ApplyToCharsInPrimarySelection from TiogaOps.NoSelection errors. Move TiogaVoice cmd registration to .load file for true religion. changes to: RegisterInterest, DeleteVoiceFromChar, initialization, ApplyToCharsInPrimarySelection, DIRECTORY, TiogaVoiceCmdProc Polle Zellweger (PTZ) January 21, 1988 6:48:34 pm PST Protect against calling TextEdit.PutCharProp with no chars! changes to: ApplyToCharsInPrimarySelection, ApplyToLockedChars, BreakVoiceWindowLinks Polle Zellweger (PTZ) January 22, 1988 4:52:15 pm PST user message (in MessageWindow) improvements + try to clear if operation succeeded changes to: DIRECTORY, EditVoiceProc, StoreVoiceAtSelection, DeleteVoiceProc, voiceThere, DeleteVoiceFromChar, PlaySelection Polle Zellweger (PTZ) January 23, 1988 6:30:46 pm PST voiceMenu line was created in TiogaMenuOps.tiogaMenu & then removed, leaving tiogaMenu with a NIL entry. Didn't matter for ordinary Tioga viewers, because they were fixed up afterwards. Disastrous for Remember viewers, however. changes to: DIRECTORY, CreateTiogaViewerMenu, initialization Polle Zellweger (PTZ) July 15, 1988 6:20:31 pm PDT Changes to allow clients to alter button behaviors (for WalnutTiogaVoice). changes to: VoiceMenu, DeleteVoiceProc, EditVoiceProc, DeleteLinks, CreateTiogaViewerMenu, TiogaInterpret Polle Zellweger (PTZ) July 20, 1988 6:48:57 pm PDT changes to: CreateTiogaViewerMenu, TiogaInterpret Polle Zellweger (PTZ) July 22, 1988 5:42:18 pm PDT changes to: TiogaInterpret Polle Zellweger (PTZ) September 12, 1988 11:08:09 pm PDT Finally finish changes to support WalnutTiogaVoice changes to: VoiceMenu, DeleteVoiceProc, EditVoiceProc, CreateTiogaViewerMenu, TiogaInterpret, RegisterButtonBehaviors, CreateTiogaViewerMenu, RegisterButtonBehaviors, DeleteLinks, InstallMenuButton, Polle Zellweger (PTZ) September 13, 1988 1:22:34 pm PDT changes to: DIRECTORY, VoiceMenu, DeleteVoiceProc, DeleteLinks, InstallMenuButton, CreateTiogaViewerMenu, RegisterButtonBehaviors, TiogaInterpret, TJaM Polle Zellweger (PTZ) April 20, 1990 11:29:11 am PDT Make it possible to annotate 16-bit Xerox extended characters with voice (formerly ignored charSet information). changes to: DIRECTORY, TalksBubbleDataRep, Inner (local of TalksBubblePaint), TalksBubbleFormat Polle Zellweger (PTZ) December 7, 1990 12:10:48 pm PST Port to PCedar: change machine-dependent types. Move voice interest registration to VoiceInterestImpl to ease source-sharing. changes to: ScanForVoice removed: MonitorCopiesToGlobal, RegisterInterest, RegisterVoiceInterestCmd Ê"«•NewlineDelimiter –(cedarcode) style™šœ™Icodešœ Ïeœ7™BJšœ%Ïk™(K™#K™.K™—šž ˜ Kšœ žœ˜(Kšœžœ˜*Kšœžœ}˜‰Kšœ žœ˜"Kšœžœ˜,Kšœ žœF˜VKšœ žœ ˜Kšžœžœžœ˜)Kšœžœ#˜0Kšœžœ“˜žKšœžœ!˜4Kšœ žœ˜*Kšœ žœ˜!Kšœžœ ˜!Kšœžœ˜Kšœžœ$žœ ˜=Kšœ žœv˜‡Kšœ žœ.˜K˜šžœžœž˜)šžœ-žœ˜5Kšœ ™ šžœž œžœž˜3Kšœ2žœ˜:—K˜ Kšœžœžœ˜K˜—Kšžœ˜—Kšžœžœ'˜5K˜—K˜0K˜K˜—š¡œžœžœžœ˜ZK™š¡ œžœ˜)Kšœ$˜$Kšœ˜K˜CKšžœ žœžœ¢"˜?šžœž˜Kš žœžœžœžœž˜Q—šžœ˜šžœžœžœ.ž˜=Kšœžœ˜&—šž˜K˜2šžœžœ˜!Kš žœžœžœžœžœ˜FKšž˜K˜—šž˜šžœžœžœ"ž˜1Kšœžœ˜&——Kšžœ˜—K˜—K˜—Kšœ<žœ˜E˜K˜——š¡œžœžœžœ˜NK™gKšœ$˜$Kšœ˜K˜CKšžœ žœžœ¢"˜?šžœž˜Kš žœžœžœžœž˜Q—šžœ˜Kš žœžœžœ.žœžœ˜dšž˜K˜2šžœžœ˜!Kš žœžœžœžœžœ˜FKšž˜K˜—Kš žœžœžœžœ"žœžœ˜]Kšžœ˜—K˜—˜J™——š ¡œžœžœ6žœ žœžœ˜€J™=Kšœ%˜%Kšœžœžœ˜ Kšœžœžœ˜#K˜š¡œžœžœžœ˜+Kšžœ'žœ9žœ˜mKšœ˜—K˜š¡ œžœ˜?Kšœ6˜6Kšœ˜Kšœ žœ˜Kšœžœ˜Kšœ˜K˜K˜™K˜"K˜šžœžœžœ˜*Kšœ(˜(Kšœ…žœ¢"˜¾K˜K˜—šžœžœ˜Kšœ žœ žœ žœ ˜8Kšœ¢˜0šžœ)ž˜/KšœJ™JKšœžœ˜.—Kšžœ)žœžœ˜GKšœKžœ˜OKš žœžœžœžœžœ˜7K˜KšœZ˜ZKšœ}˜}K™QKšœ7žœžœ¢/˜‰šœO˜OK™™—Kšœ;žœ˜½Kšœ˜—Kšœ¢*˜.Kšœ˜—šžœ'žœžœžœ˜9K˜—KšœRžœ˜\J™Wšžœžœž˜Kšžœ\˜_Kšœq˜qKšžœ(žœ˜5—Kšœ¢˜#K˜—š¡œ˜)Kšžœžœžœžœžœžœžœ=™Kšœ žœ˜Kšœ4˜4Kšžœžœ žœ0žœ˜KKšžœ˜šœ˜K˜——Kšœ žœžœ¢²˜Íš¡œžœžœ&˜FJ™K˜#Kšœžœ˜šžœ,žœžœ˜8K˜SKšœ žœ˜Kšœ-žœ˜2Kšœ+žœ˜0Kšœ1žœ˜6šžœžœžœ˜Kšœ1žœ˜6KšÏtœ‰£™¡Kšœ5™5Kšœ1™1K˜—K˜—K˜K˜—š¡ œžœžœ˜K˜FKšœžœ˜Kšœžœ˜Kšœ!˜!K˜Kšœ$žœ˜4K˜šžœžœ!žœž˜.K˜*Kšžœ!žœžœ˜-K˜(šžœ(žœ žœž˜?šžœžœ˜"Kšœ/žœžœ˜SKšœ žœ˜"K˜*K˜—Kšžœ˜—Kšžœ˜—K˜Kšžœžœ0žœ˜RKšžœžœžœ[žœ˜|Kšžœ˜Kšœ žœ(žœ˜NK˜K˜—š¡ œ˜'Kšžœžœžœžœžœžœžœžœ™J™áKš œ žœžœžœžœ˜#Kš œžœžœžœžœ˜)Kšœ;žœ˜?šœ žœžœ˜J™—š¡œžœ(˜žœžœžœ˜PJš£#œC™fK˜—Kšžœ$žœ˜+˜GKšœžœ˜1šœU˜UJ™™—Kšœ;žœˆ˜ÆKšžœž˜šœžœ˜#Kšœ-žœ˜3—K˜K˜K˜K˜+K˜JšœIžœ³™þ—Kšœ˜—K˜K˜KšœHžœ˜Sšžœ žœžœ˜šœ˜Kšžœ žœD˜TKšžœ'˜+Kšžœ˜—Kšžœ˜K˜—Kšžœ˜K˜š žœžœžœžœžœžœž˜=Kšœ+˜+Kšœ}žœ˜„K˜AK˜&—Kšžœ˜K˜KšœMžœ˜VK˜šž˜Kšœž˜ —˜K˜——š¡œžœžœ3žœ*˜€J™’š¡œ˜%Jšœ5™5š žœDžœžœžœžœ˜‹Kšœ žœ˜Kšœ1žœ˜6Jšœ˜™˜šžœ.žœž˜8Kšœ-žœ˜2—K˜—K˜—š¡œžœ˜#KšœU˜U˜K˜——K˜4šœžœ˜(JšœK™K—Kšœ žœžœ˜KšœžœH˜bKšœ'˜'š žœžœ žœžœžœ˜0Kš¢c™cKšœ=˜=Kšœ^˜^K˜—šžœžœ˜Kšœžœ˜Kšœ'žœ˜.K˜—˜K˜——š¡œžœžœ3žœžœžœžœ žœ˜©J™š¡œ˜)Jšœ5™5šžœCžœ˜LKšœ8˜8Kšœ>˜>Kšœ žœ˜K˜—K˜—š¡œžœ˜#KšœY˜Y˜K˜——K˜4KšœžœH˜bKšœ žœ˜Kšœ'˜'šžœžœ žœ¢4˜JKšœ^˜^—˜K˜——š¡ œ˜&šžœžœž˜+Kšžœ+˜1—K˜K˜—š ¡œžœ žœžœžœ˜`K˜3Kšœžœžœ˜Kšœžœžœ&˜EK˜š¡œžœ˜#Kšœ.˜.Kšœ.˜.˜K˜——Kšœžœ˜Kšœžœžœ˜Kšœ)˜)šžœ4žœžœž˜Gšžœžœžœž˜&Kšœžœ˜K˜Kšžœ žœžœ˜K™‚šžœžœžœžœ˜1šžœžœ@žœ˜LKšœ&˜&Kšžœžœ˜ K˜—Kšžœžœ˜K˜—K˜—Kšœ žœ%˜7šžœ žœžœ˜Kšœ˜Kšœ˜K˜(K˜0K˜—Kšžœ˜—Kšžœ˜—šžœžœ˜šžœ3žœ˜:K˜>—Kšœ&˜&Kšœ˜K˜=K˜—Kšžœ˜K˜K˜—š¡ œ˜%Kšžœžœžœžœžœžœžœžœ™J™¬K˜š¡œ˜(Jšœ5™5Kšœ1žœ˜6K˜—š¡œžœ˜#KšœX˜X˜K˜——K˜4šœžœ˜(JšœK™K—Kšœ'˜'šžœžœ˜Kšœžœ˜Kšœ'žœ˜.K˜—KšœEžœ˜Lšœ˜K˜——Kšœžœ¦˜ÆK˜š ¡œžœ žœ$žœžœžœ˜^šœ˜Kšœ?˜?—šœ˜KšœB˜B—šžœž˜ Kšžœ3˜7Kšžœ:˜>—K˜—Kšœ˜šœžœ"˜:Kšœ™K™—š¡œžœ˜ K˜/Kšœ5žœ™JKšœy˜yKšœ{˜{Kšœq˜qKšœz˜zKšœ{˜{Kšœ˜Kšœ‡˜‡Kšœ£žœT˜ûK˜-Kšœ9˜9˜K˜——š¡œžœ˜"K™$K˜K˜=K˜@K˜>K˜K˜>K˜AK˜?K˜Kšœ©™©K˜Kšœ>˜>KšœR˜RKšœW˜WKšœL˜LKšœL˜LKšœB˜BKšœF˜FKšœ]˜]KšœB˜BK˜K™—š¡œžœ˜)Kš žœžœžœžœ3žœžœ™oKšœ)žœF˜rKšœžœ˜-K˜—K˜K™ K™š¡œžœ žœ˜+Kšœžœ˜Kšœ˜K˜K˜—š¡ œžœžœžœžœžœžœžœžœžœ˜\Kš œžœžœžœžœžœ˜5Kšœžœžœžœ˜'Kšžœžœžœžœ˜+Kšžœžœžœ"žœ˜JK˜#Kšžœ˜ K˜—š¡œžœžœžœNžœžœžœ'˜ªKš œžœžœ%žœžœ˜MKšœžœžœ*˜?Kšžœžœžœžœ˜+Kšžœžœžœ"žœ˜JK˜#Kšžœ˜ K˜—K˜Kšœ žœžœžœ˜š¡ œžœžœ žœ˜,Kšžœžœžœ˜3K˜—Kš¡œ4˜CK˜—š  ™ J™J™\J™šœžœžœ˜#Kšœ˜Kšœ žœ˜Kšœžœ˜ Kšœ žœ˜Kšœžœ˜ Kšœ žœ˜Kšœž˜Kšœ˜K˜—š¡œžœB˜XKšœžœžœ ˜1K˜š¡ œ˜#Kšœžœ˜*Kšœ1˜1Kšœ3˜3Kšœt˜tKšœ<˜K™@Kšœžœžœ˜Kšžœžœžœ5žœžœžœžœžœžœžœžœ˜¤K˜Kšžœž˜šœ“žœ˜šKšœžœ!˜<—K˜˜K˜——š¡œ˜/Kšœ žœ.˜AKšœžœžœ-˜Cšžœ žœžœ*ž˜EK˜"šžœ˜Jšœ@™@——K˜Kšœ"žœ žœžœ1˜Kšžœ žœ1˜CKšžœ.˜2K˜KšœEžœ˜KKšœ˜—K˜™`K˜—š¡ œžœžœ˜J™CJ™3K˜Kšœ+˜+Kšœ žœžœ%žœ˜:K˜š¡ œžœ$žœ˜<šžœ=žœžœž˜PKšžœ&žœ žœ ˜XKšž˜šœžœ˜Kšœ žœ7žœ˜a—K˜—Kšž˜—K˜K˜K˜CK˜>Kšžœ žœžœžœ˜J™Jšœ½™½K˜Kšžœ˜Kšžœ/˜3Kšž˜˜K˜Gšž˜K˜2Kšžœž˜šœ*˜*Kšž˜—K˜Kšžœ<˜@—Kšž˜—K˜K˜šœžœ˜9šžœ=žœžœž˜Pšžœ*žœ+ž˜_K˜\——Kšž˜—K˜K˜—K˜K™ K™Kšœ žœžœ˜#šœžœžœ˜Kšœ˜Kšœ˜Kšœž˜ K˜—K˜š¡œ˜1Jšœ žœžœžœ™!š¡œžœ˜#šžœ2žœ˜9KšœEžœ˜J—K˜—Kšœžœ˜$šžœžœž˜Kšœ-˜-—K˜K˜—š¡œžœžœ žœžœžœ2žœ˜”J™ÒK™ÝJ™Kšœ˜Kšœ&žœ3˜_Kšžœžœžœžœ˜K˜QKšžœ žœžœžœ˜K˜šžœžœžœ2žœ˜YJ™ÄKšœžœ&˜@KšžœžœE˜Sšžœ"˜&Kšœq™q—Kšœ žœ˜K˜—K˜K˜—š ¡œžœžœžœžœ˜DKšžœA˜Gšœ˜K˜——š ¡œžœžœžœžœ˜RKšœ&žœ3˜_Kšžœžœžœžœ žœžœžœ˜/K˜K˜—Kš œžœžœ žœ žœžœžœ˜Hš œžœžœžœ žœ˜LK˜—š¡œ˜'šžœ žœžœ˜Kšœ&žœ ˜7Kš žœžœžœžœžœžœ˜EK˜—Kšžœžœ˜K˜K˜—š¡ œžœ*žœ ˜`Kšœr™rKšœ5¢W˜Œšžœ5žœžœž˜Hšžœ žœž˜šžœžœžœž˜&Kšœ žœ(˜5Kšžœ žœžœ˜0Kšžœ˜——Kšžœ˜—K˜K˜—š ¡ œžœ*žœ žœ@žœ&˜»KšœÊ™Êš¡œžœ'˜3KšœP™Pšžœ6žœ ž˜MKšœ žœ˜Kšœ žœ˜ š ¡œžœžœ žœžœ˜:š žœžœžœžœž˜1Kšœ žœ(˜5Kšžœ"žœžœ˜MKšžœ˜—Kšœ˜—šžœ žœžœžœ˜5K˜Kšžœ˜Kšœ˜—Kšžœ˜—K˜—Kšœ#žœ˜'Kšœžœ˜K˜šžœžœžœ žœ˜3Kš£œv£™x—šžœ žœžœ˜defaultšžœ)žœQž˜ƒKšžœ ˜—K˜*Kšœ˜Kšžœžœžœžœ˜!K˜—K˜-Kšžœžœžœžœ˜!Kšœžœ¢#˜;K˜—K˜—š ™J™KšœM˜MKšœ˜K˜Kšœ˜K˜K˜šœ‰˜‰J™—Kšœ7˜7K˜—Kšœ˜šœ%ž™(K™X—šœžœž™2Kšœƒ™ƒKšœ ÏrŠœ¤œ-™ë—™2Kšœ ¤ ™—™3K™·Kšœ Ðkr ¤-™B—™2Kšœ ¥ ¤œ!¤œF¤™µ—™3Kšœ–¤œ?™ÖKšœ ¥ ¤Ñ™æKšœ¤8™=—™2Kšœ€™€Kšœ ¥ ¤$œ¤ œ¤C™Í—™2K™ZKšœ Ðrs ¤ ™!—™3Kšœ ¦ ™—™4Kšœ ¤(™4—™7K™œKšœ ¤Zœ¤7œ¤œ¤™‡—™5K™Kšœ ¦ ¤_œ¤%™µ—™6K™ƒKšœ ¦ ¤,œ¤0™—™5K™UKšœ ¤%œ!¤"œ!¤:œ¤™ú—™5KšœÇ™ÇKšœ ¤s™—™5Kšœ;™;Kšœ ¤I™U—™5K™RKšœ ¦ ¤g™|—™5Kšœå™åKšœ ¦ ¤'™<—™2K™JKšœ ¤]™i—™2Kšœ ¤%™1—™2Kšœ ¤™—™8K™2Kšœ ¤™œ¤œ™Ç—™7Kšœ ¦ ¤‚™——™4K™pKšœ ¦ ¤œ¤™_—™6K™~Kšœ ¤ ™Kšœ ¤A™J——…—€$áë