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], 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, StepForward], TiogaAccess USING [EndOf, FromFile, FromSelection, FromViewer, Get, Reader, TiogaChar ], TiogaButtons USING [TextNodeRef, TiogaOpsRef], TiogaMenuOps USING [tiogaMenu], TiogaOps USING [CallWithLocks, CommandProc, GetSelection, NoSelection, Ref, RegisterCommand, RestoreSelA, SaveSelA, SelectDocument, SelectionGrain, SetSelection, StepForward, ViewerDoc ], TiogaOpsDefs USING [Location, Ref], TiogaVoicePrivate USING [ AddVoiceProc, BuildVoiceViewer, CancelPlayBack, DictationMachine, PlayBackMenuProc, PlayRopeWithoutCue, RemoveParentViewer, RopeFromTextList, SetParentViewer, StopRecording, VoiceViewerInfo, voiceViewerInfoList, VoiceWindowRec, VoiceWindowRef ], Vector2 USING [VEC], ViewerBLT USING [ChangeNumberOfLines], ViewerClasses USING [Viewer], ViewerEvents USING [EventProc, EventRegistration, RegisterEventProc, UnRegisterEventProc ], ViewerOps USING [FetchProp, PaintViewer], VoiceRope USING [Handle, Length, Open, Retain, Stop, 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, 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 TextEdit.PutCharProp[node, targetChar.where, $voiceWindow, NEW[TiogaVoicePrivate.VoiceWindowRec _ [label: Rope.Concat["Sound Viewer #", Convert.RopeFromInt[voiceViewerInfo.viewerNumber]]]]]; voiceViewerInfo.parentViewer _ selectedViewer; voiceViewerInfo.positionInParent _ targetChar; RegisterViewer[selectedViewer] } }; 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", 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; voiceWindowRope: Rope.ROPE _ GetVoiceWindowRope[position]; TextEdit.PutCharProp[node, offset, $Artwork, NIL]; TextEdit.PutCharProp[node, offset, $voice, NIL]; TextEdit.PutCharProp[node, offset, $textInVoice, NIL]; IF voiceWindowRope # NIL THEN { TextEdit.PutCharProp[node, offset, $voiceWindow, NIL]; TiogaVoicePrivate.RemoveParentViewer[Convert.IntFromRope[ voiceWindowRope.Substr[voiceWindowRope.Find["#"]+1]]]; }; }; 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]]] }; CancelProc: PUBLIC Menus.MenuProc = { VoiceRope.Stop[thrushHandle]; TiogaVoicePrivate.CancelPlayBack[]; TiogaVoicePrivate.StopRecording[] }; 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] }; AppendInt: PROC [list: LIST OF INT, entry: INT] RETURNS [LIST OF INT] = { oneElementList: LIST OF INT _ CONS[entry, NIL]; hangOffPoint: LIST OF INT _ list; IF list = NIL THEN RETURN [oneElementList]; WHILE hangOffPoint.rest # NIL DO hangOffPoint _ hangOffPoint.rest ENDLOOP; hangOffPoint.rest _ oneElementList; RETURN [list] }; EditVoiceProc: Menus.MenuProc = { voiceList: LIST OF Rope.ROPE _ NIL; textInVoiceList: LIST OF Rope.ROPE _ NIL; voiceViewerInfoList: LIST OF TiogaVoicePrivate.VoiceViewerInfo _ NIL; voiceViewerNumberList: LIST OF INT _ 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]; 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 TextEdit.GetCharProp[node, targetChar.where, $voiceWindow] = 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; TextEdit.PutCharProp[node, targetChar.where, $voiceWindow, NEW[TiogaVoicePrivate.VoiceWindowRec _ [label: Rope.Concat["Sound Viewer #", Convert.RopeFromInt[voiceViewerNumberList.first]]]]]; TiogaVoicePrivate.SetParentViewer[voiceViewerInfoList.first, parentViewer, targetChar]; IF ~alreadyEdited THEN { parentViewer.newVersion _ FALSE; ViewerOps.PaintViewer[parentViewer, caption, FALSE] }; voiceList _ voiceList.rest; voiceViewerInfoList _ voiceViewerInfoList.rest; voiceViewerNumberList _ voiceViewerNumberList.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; newNumber: INT; [viewerInfo: newInfo, viewerNumber: newNumber] _ TiogaVoicePrivate.BuildVoiceViewer[voiceID: l.first, textInVoice: textInVoiceList.first, youngVoice: FALSE]; voiceViewerInfoList _ AppendViewerInfo[voiceViewerInfoList, newInfo]; voiceViewerNumberList _ AppendInt[voiceViewerNumberList, newNumber]; textInVoiceList _ textInVoiceList.rest ENDLOOP; ApplyToCharsInPrimarySelection[AddVoiceWindowProps ! TiogaOps.NoSelection => CONTINUE] EXITS Quit => NULL }; DeleteSourceMarker: PUBLIC PROC [ viewer: ViewerClasses.Viewer, positionInParent: TiogaOpsDefs.Location, voiceViewerNumber: INT] = { DoIt: PROC [root: TiogaOps.Ref] = { node: TextNode.Ref _ TiogaButtons.TextNodeRef[positionInParent.node]; voiceWindowRope: Rope.ROPE _ Rope.Concat["Sound Viewer #", Convert.RopeFromInt[voiceViewerNumber]]; positionInParent _ FindCharProp[ viewer: viewer, propName: $voiceWindow, propVal: voiceWindowRope, checkPropProc: CheckVoiceWindowProp, locHint: positionInParent]; IF positionInParent.node # NIL THEN { alreadyEdited: BOOLEAN _ viewer.newVersion; TextEdit.PutCharProp[node, positionInParent.where, $voiceWindow, NIL]; IF TextEdit.GetCharProp[node, positionInParent.where, $voice] = NIL THEN TextEdit.PutCharProp[node, positionInParent.where, $Artwork, NIL]; IF ~alreadyEdited THEN { viewer.newVersion _ FALSE; ViewerOps.PaintViewer[viewer, caption, FALSE] } } ELSE DebugRope["Source marker not found in expected document!!\n"] }; TiogaOps.CallWithLocks[DoIt, TiogaOps.ViewerDoc[viewer]]; }; GetVoiceWindowRope: PROC [char: TiogaOpsDefs.Location] RETURNS [Rope.ROPE] ~ { v: TiogaVoicePrivate.VoiceWindowRef _ NARROW[TextEdit.GetCharProp[TiogaButtons.TextNodeRef[char.node], char.where, $voiceWindow]]; IF v#NIL THEN RETURN[v.label] ELSE RETURN[NIL]; }; SaveRopeAtSourceMarker: PUBLIC PROC [ viewer: ViewerClasses.Viewer, positionInParent: TiogaOpsDefs.Location, voiceViewerNumber: INT, voiceRopeID: Rope.ROPE, textInVoice: Rope.ROPE] RETURNS [succeeded: BOOLEAN _ FALSE] = { DoIt: PROC [root: TiogaOps.Ref] = { node: TextNode.Ref _ TiogaButtons.TextNodeRef[positionInParent.node]; voiceWindowRope: Rope.ROPE _ Rope.Concat["Sound Viewer #", Convert.RopeFromInt[voiceViewerNumber]]; positionInParent _ FindCharProp[ viewer: viewer, propName: $voiceWindow, propVal: voiceWindowRope, checkPropProc: CheckVoiceWindowProp, locHint: positionInParent]; IF positionInParent.node # NIL THEN { TextEdit.PutCharProp[node, positionInParent.where, $voice, voiceRopeID]; TextEdit.PutCharProp[node, positionInParent.where, $textInVoice, textInVoice]; succeeded _ TRUE } ELSE DebugRope["Source marker not found in expected document!!\n"] }; TiogaOps.CallWithLocks[DoIt, TiogaOps.ViewerDoc[viewer]]; }; ScanForVoice: TiogaOps.CommandProc = { IF viewer.newFile OR viewer.newVersion THEN RecordVoiceInstancesAtRoot[viewer] ELSE MessageWindow.Append["File is not altered", TRUE] }; DeleteLinks: Menus.MenuProc = { viewer: ViewerClasses.Viewer _ NARROW[parent]; RemoveAnySourceMarker: PROC [position: TiogaOpsDefs.Location] = { node: TextNode.Ref _ TiogaButtons.TextNodeRef[position.node]; IF TextEdit.GetCharProp[node, position.where, $voiceWindow] # NIL THEN { -- see comments about 'edited' status at head of procedure AddVoiceWindowProps alreadyEdited: BOOLEAN _ viewer.newVersion; TextEdit.PutCharProp[node, position.where, $voiceWindow, NIL]; IF ~alreadyEdited THEN { viewer.newVersion _ FALSE; ViewerOps.PaintViewer[viewer, caption, FALSE] } } }; TiogaOps.SaveSelA[]; TiogaOps.SelectDocument[viewer: viewer, level: branch]; ApplyToCharsInPrimarySelection[RemoveAnySourceMarker]; TiogaOps.RestoreSelA[]; RemoveParentPointersTo[viewer]; }; RecordVoiceInstancesAtRoot: PROC [ viewer: ViewerClasses.Viewer] = { wholeFile: TiogaAccess.Reader _ TiogaAccess.FromViewer[viewer]; heavyChar: TiogaAccess.TiogaChar; props: Atom.PropList; soundsInDocument: INT _ 0; rootNode: TextNode.Ref; voiceList: Rope.ROPE _ NIL; DebugRope["Voice messages in document:"]; IF NOT TiogaAccess.EndOf[wholeFile] THEN DO heavyChar _ TiogaAccess.Get[wholeFile]; IF TiogaAccess.EndOf[wholeFile] THEN EXIT; FOR props _ heavyChar.propList, props.rest WHILE props # NIL DO IF props.first.key = $voice THEN { DebugRope["\n"]; DebugRope[NARROW[props.first.val, Rope.ROPE]]; soundsInDocument _ soundsInDocument + 1; voiceList _ voiceList.Concat["&"]; -- just used here as a separator: a character not found in the IDs voiceList _ voiceList.Concat[NARROW[props.first.val, Rope.ROPE]] } ENDLOOP ENDLOOP; rootNode _ TiogaButtons.TextNodeRef[TiogaOps.ViewerDoc[viewer]]; -- TextNodeRef is just a type convertor - replace with a more cautious implementation?? [several references throughout TiogaVoice] TextEdit.PutProp[rootNode, $voicelist, voiceList]; 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[]; 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]; windowRef: TiogaVoicePrivate.VoiceWindowRef _ NARROW[TextEdit.GetCharProp[loc.node, loc.where, $voiceWindow], TiogaVoicePrivate.VoiceWindowRef]; label: Rope.ROPE _ IF windowRef = NIL THEN NIL ELSE windowRef.label; escapement: Vector2.VEC _ ImagerFont.Escapement[TEditFormat.GetFont[style], [set: charSet, code: letter-'\000]]; firstCharWidth: REAL _ escapement.x; 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; -- if this runs off the end then something has gone seriously wrong ViewerEvents.UnRegisterEventProc[(IF firstEntry THEN registeredViewerList ELSE previousEntry.rest).first.destroyProc, destroy]; IF firstEntry THEN registeredViewerList _ registeredViewerList.rest ELSE previousEntry.rest _ previousEntry.rest.rest; RemoveParentPointersTo[viewer]; }; RemoveParentPointersTo: PUBLIC PROC [viewer: ViewerClasses.Viewer] = { FOR info: TiogaVoicePrivate.VoiceViewerInfo _ TiogaVoicePrivate.voiceViewerInfoList, info.nextInfoRec WHILE info # NIL DO IF info.parentViewer = viewer THEN { info.parentViewer _ NIL; DebugRope[IO.PutFR["detaching parent link for voice viewer %d\n", IO.int[info.viewerNumber]]] } ENDLOOP }; 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: LIST OF TiogaVoicePrivate.VoiceViewerInfo _ 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[]; FOR info: TiogaVoicePrivate.VoiceViewerInfo _ TiogaVoicePrivate.voiceViewerInfoList, info.nextInfoRec WHILE info # NIL DO IF info.parentViewer = viewer THEN infoList _ CONS [info, infoList] ENDLOOP; 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: LIST OF TiogaVoicePrivate.VoiceViewerInfo _ 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 } }; CheckPropProc: TYPE ~ PROC [propVal: REF, charProp: REF] RETURNS [BOOL]; 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] }; 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 hintNode.deleted 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[TiogaButtons.TextNodeRef[TiogaOps.ViewerDoc[viewer]], hintNode]; IF foundLoc.node#NIL THEN RETURN; Search[nextNode, NIL]; -- = Search[NIL, NIL] if no locHint }; 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", 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]; }. "VoiceInTextImpl.mesa Copyright Σ 1987 by Xerox Corporation. All rights reserved. Ades, September 24, 1986 5:52:47 pm PDT Swinehart, April 9, 1987 12:24:18 pm PDT Polle Zellweger (PTZ) June 12, 1987 7:32:45 pm PDT TiogaVoicePrivate 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 next line places a 'talks bubble' on the selected character - see TalksBubbleImpl test for failure conditions and report them to the user after releasing the viewer lock these routines are simply append procedures for the three list types above [alas] 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 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 see comments about 'edited' status at head of procedure AddVoiceWindowProps 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 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 embarassingly left in a file by bad tracking etc.] 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 VoiceInterests a mechanism to track all files copied to global file space and register interest in any voice contained in them, plus a command to register interests in any voice found in a named file [which must be global or with global attachment: the interest is registered for the global name] the code is very inefficient in the way that it deals with files: it opens the file as a stream to see if it is a tioga file and then gets a TiogaAccess.Reader on each tioga file, which means making a complete copy of the file into TiogaAccess.Reader format, and then reads its root properties faster code could be produced by using the 'control' rope reader returned by FileReader.FromStream but that means writing fairly grubby code that knows a lot about the tioga file format [see FileOps.mesa in tioga.df if interested] in order to determine if some file is a tioga file, we need to open it as a plain file [hence following line] and use FileReader.FromStream to determine whether it is of the correct format. FromStream also returns the text, comments and control sections of the documents if we are interested [see comments at the head of this code] the voiceRope specification in this procedure call is only used to extract the RopeID - the start and length are not used TalksBubbles routines to insert distinctive markers around a character to indicate that it contains voice 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 n.b. no consideration given in this code to the existence of split viewers 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 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 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. This shouldn't be needed if all else has gone well. PTZ, June 12, 1987 7:32:27 pm PDT Initialization **** voiceButtonQueue should be deleted, or used for ALL button activities 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 Κ ˜šœ™Icode™K˜šœœ˜)šœ-œ˜5Kšœ ™ šœ œœ˜3Kšœ2œ˜:—K˜ Kšœœœ˜K˜—Kšœ˜—Kšœœ'˜5K˜—K˜0K˜K˜—š œœœ˜-Kš  œœ˜-K™š  œœ˜)Kšœ$˜$Kšœ5˜5Kšœ˜Kš œœœœœ˜VKš˜š œœœœHœœ˜š˜Kšœ2˜2Kšœ˜š œœœœœœ˜IKš˜—K˜Kš œœœœ<œ˜v—Kš˜—K˜—K˜K˜Kšœ"˜"˜K˜——š  œœœ  œœ˜NK™gKšœ$˜$Kšœ5˜5Kšœ˜Kš œœœœœ˜VKš˜š œœœœHœœ˜š˜Kšœ2˜2Kšœ˜š œœœœœœ˜IKš˜—K˜Kš œœœœ<œ˜v—Kš˜—K˜˜K˜J™——š œœœ˜$Jšœ4œ œœ˜\J™=Jšœ%˜%Jšœœ˜Jšœœ˜J˜š œœœœ˜+Jšœ&œ9œ˜o—J˜š  œœ˜?Jšœ6˜6Jšœ˜Jšœ œ˜Jšœœ˜Jšœ˜J˜JšœΌ˜ΌJ˜Jšœœœ˜)šœ+˜+Jšœ…œ‘"˜Ύ—˜J˜—Jšœ˜šœœ œ œ ˜;Jšœ2‘˜JJšœKœ˜OJšœœœ˜!JšœZ˜ZJšœ}˜}J™QJšœ7œœ‘/˜‰Jšœ;œ€˜ΎJšœ.˜.Jšœ.˜.Jšœ˜—Jšœ˜J˜Jšœ˜—šœ'œœœ˜9J˜—Jšœdœœ˜vJ™WJšœœ˜šœDœ˜JJšœ˜—J˜Jšœ˜šœœ˜šœCœ˜IJšœ˜—J˜Jšœ ˜—J˜˜J˜—J˜—š œ˜#šœ6˜6J˜J˜——š œœœ&˜FJšœ=˜=Jšœœ˜Jšœœ ˜:Jšœ-œ˜2Jšœ+œ˜0Jšœ1œ˜6šœœœ˜Jšœ1œ˜6Kšœp˜pJ˜—J˜J˜—š  œœœ˜Jšœ?˜?Jšœœ˜Jšœœ˜Jšœ!˜!Jšœ˜J˜šœœ!œ˜.Jšœ*˜*Jšœ!œœ˜-J˜(šœ(œ œ˜?Jšœ˜ šœ(œœ˜LJšœ)˜)—J˜—Jš˜—Jšœ˜J˜Jšœœ0œ˜SJšœ œ'œ˜MJ˜J˜—š  œœ˜&Jšœ˜Jšœ#˜#Jšœ!˜!Jšœ˜J˜—J™Qš  œœœœœœœœœœ˜\Jš œœœœœœ˜5Jšœœœœ˜'Jšœœœœ˜+Jšœœœ"œ˜JJ˜#Jšœ˜ J˜—š œœœœNœœœ'˜ͺJš œœœ%œœ˜MJšœœœ*˜?Jšœœœœ˜+Jšœœœ"œ˜JJ˜#Jšœ˜ J˜—š  œœœœœ œœœœœ˜IJš œœœœœœ˜/Jšœœœœ˜!Jšœœœœ˜+Jšœœœ"œ˜JJ˜#Jšœ˜ J˜—J˜š  œ˜!J™αJš œ œœœœ˜#Jš œœœœœ˜)Jšœœœ%œ˜Eš œœœœœ˜)J™—š œœ(˜œœ˜JJšœ/˜/Kšœ.œBœ˜|J˜—Jšœ6˜:J˜—J˜—J˜—J˜š œœ(˜AJ™\J™€Jšœ?˜?Jšœœœ<œ˜aJšœ œœœ˜Jšœœœœ˜"šœ>œœœ˜PJ˜—Jšœ$œ˜+šœG˜GJšœœ˜1Jšœ;œ€˜ΎJšœW˜WJšœ˜šœœ˜#Jšœ-œ˜3—J˜J˜Jšœ˜Jšœ/˜/Jšœ2˜2J˜JšœIœ³™ώ—Jšœ˜—J˜J˜JšœHœ˜SJ˜Jšœ œœ˜šœ=œ˜CJš˜—J˜J˜š œœœœœœ˜=Jšœ+˜+Jšœ œ˜Jšœ–œ˜JšœE˜EJšœD˜DJ˜&—Jšœ˜J˜JšœMœ˜VJ˜š˜Jšœ˜ —˜J˜——š œœœ˜!JšœZœ˜bš œœ˜#JšœE˜EJšœœI˜cJ˜šœ ˜ KšœA˜AKšœ@˜@—šœœœ˜%JšœK™KJšœœ˜+JšœAœ˜FJ˜Jšœ˜™˜Jšœ>œ˜HJšœ=œ˜BJ˜šœœ˜Jšœœ˜Jšœ'œ˜-J˜—J˜—Jšœ>˜BJ˜J˜—Jšœ9˜9˜J˜——š œœœœ˜NKšœ&œV˜‚Kšœœœœ œœœ˜/J˜K˜—š œœœ˜%Jš œZœœœœ œœ˜·š œœ˜#JšœE˜EJšœœI˜cJ˜šœ ˜ KšœA˜AKšœ@˜@—šœœœ˜%JšœH˜HJšœN˜NJšœ ˜—J˜Jšœ>˜B—˜J˜—Jšœ9˜9˜J˜——š  œ˜&Jš œœœ$œ-œ˜…J˜J˜—š  œ˜J™πJšœœ ˜.J˜š œœ&˜AJšœ=˜=Jšœ<œ˜Fšœ‘N˜PJšœœ˜+Jšœ9œ˜>Jšœ˜šœœ˜Jšœ'œ˜-—J˜—J˜—J˜J˜Jšœ˜Jšœ7˜7Jšœ6˜6Jšœ˜J˜Jšœ˜šœ˜J˜——š œœ˜"Jšœ!˜!Jšœ?˜?Jšœ!˜!Jšœ˜Jšœœ˜Jšœ˜Jšœœœ˜J˜Jšœ)˜)J˜šœœœ˜+Jšœ'˜'Jšœœœ˜*šœ(œ œ˜?Jšœ˜ šœ˜Jšœ œœ˜.Jšœ(˜(Jšœ#‘B˜eJšœœœ˜@—J˜—Jš˜—Jšœ˜J˜JšœA‘ƒ˜ΔJšœ2˜2J˜Jšœœœ˜GJ˜J˜—š œœ œ˜Cšœ˜Kšœ?˜?—šœ˜Kšœ*˜*—šœ˜ Kšœ3˜7Kšœ:˜>—Kšœ˜—Kšœœ%˜9Kšœ˜šœœ"˜:Kšœ™—K˜Kšœ œœœ˜š  œœœ œ˜,Kšœœœ˜0K˜—Kš œ4˜CK˜—šŸ™J™J™™J™J™₯J™Jšœζ™ζš  œœ˜J˜—šœœœœ˜&J˜—šœœœœ˜;J˜—š œœ‘ ˜@Jšœ0˜0š˜Jšœ>˜>Jšœ!œ>˜e—Jš˜˜J˜——š  œœ œœœœ˜IJšœ(œ˜-Jšœœ˜Jšœœœ˜Jšœœ˜J˜Jš   œœœœœœ˜\J˜JšœSœ‘0˜–Jšœ œœ˜7Jšœ˜Jšœ˜šœœ œœ ‘>˜dJšœ<˜@—J˜Jšœ'œ0œ˜uJ˜šœœœ˜Jšœœœ˜:Jšœ œ˜J˜JšœΛ™ΛJšœœ˜!Jšœ œ?˜NJšœ;œœ˜FJšœ œ˜šœ'˜'Jš˜—J˜—J˜J˜šœD˜DJšœ?‘/˜nšœ>œ œ˜YJšœ"œœ œœ œœœœ˜‚—Jš˜—J˜J˜Jšœ œ˜Jšœ˜šœ'˜'Jš˜—J˜J˜)J˜šœœ˜Jšœ œ˜Jšœ œ˜ Jš œœœœœ˜GJ˜š˜J˜*Jšœ(œœ œ˜cJšœœœ˜#J˜Jšœy™yJšœ+œˆ˜ΆJšœ(˜(J˜Jšœœœ˜J˜—Jš˜—J˜˜J˜J˜——š œ˜0J˜šœ˜Jš œ œœœœœ˜XJšœGœ˜R—J˜J˜Jšœ!˜!JšœFœ˜Ršœœ˜Jšœ,˜,Jšœ˜ J˜—Jšœ"˜"š˜Jšœœ˜—˜J˜——šœU˜UJ˜—Jšœœœ˜(J™—šŸ ™ J™J™\J™šœœœ˜#Kšœœ˜ Kšœ œ˜Kšœœ˜ Kšœ œ˜Kšœœ˜ Kšœ œ˜Kšœ˜Kšœ˜K˜—š œœB˜XKšœœœ ˜1K˜š  œ˜#Kšœœ˜*Kšœ1˜1Kšœ3˜3Kšœt˜tKšœ<˜K™@Jšœœœ˜Jšœœœ5œœœœœœœœ˜€J˜Jšœ˜šœ“œ˜šJšœœ!˜<—J˜˜J˜——š œ˜/Jšœ œ.˜AJšœœœ-˜CJš œ œœ*œ$œ‘C˜΅J˜Jšœ"œ œœ1˜Jšœ œ2œ.˜vJ˜Jšœ˜šœ˜J˜——š œœœ#˜Fšœcœœ˜yJšœ˜"šœœ˜Jšœ œ6œ˜]—J˜—Jš˜˜J˜——™`J˜—š  œœœ˜J™CJ™3J˜Jšœ+˜+Jšœ œœ%œ˜:J˜š  œœ$œ˜<š œœœ6œœ˜TJšœ&œ œ ˜XJš˜šœœ˜Jšœ œ6œ˜`—J˜—Jš˜—J˜J˜JšœC˜CJšœcœœœœœ œœ˜ΖJšœ œœœ˜J™Jšœ½™½J˜Jšœ˜Kšœ/˜3Kš˜šœ˜Kšœa˜aš˜Kšœ2˜2Kšœ˜šœ*˜*Kš˜—K˜KšœV˜Z—Kš˜—K˜K˜šœœ˜9š œœœ6œœ˜TJšœ*œ+˜ZJšœ]˜a—Jš˜—K˜K˜K˜—š œœœ œ œœœ˜HK˜—š œ˜'šœ œœ˜Kšœ&œ ˜7Kš œœœœœœ˜EK˜—Jšœœ˜J˜K˜—š   œœ*œ œ@œ&˜»KšœΚ™Κš œœ'˜3KšœP™Pšœ6œ ˜MKšœ œ˜Kšœ œ˜ š  œœœ œœ˜:š œœœœ˜1Kšœ œ(˜5Kšœ"œœ˜MKšœ˜—Kšœ˜—šœœœ˜4Kšœ,˜,Kšœ˜Kšœ˜—Kšœ˜—K˜—Jšœ#œ˜'Kšœœ˜Kšœ2˜2šœœ œ˜(KšΟtœU’™W—šœ œœ˜defaultšœ)œQ˜ƒKšœ ˜—Kšœ*˜*Kšœ˜Kšœœœœ˜!K˜—JšœG˜GKšœœœœ˜!Jšœœ‘#˜;K˜—J˜—šŸ™J™Kšœ5œ™JKšœx˜xKšœ”˜”Kšœ|˜|Kšœx˜xKšœp˜pKšœ˜Kšœˆ˜ˆKšœ6˜6Kšœ&˜&šœ)œ˜.K˜—K˜=K˜@K˜>K˜>K˜A˜?K˜—šœ˜J˜—šœΟ˜ΟJ˜—šœͺ˜ͺJ˜—Jšœ7˜7K˜—Kšœ˜šœ%™(K™X—šœœ™2Kšœƒ™ƒKšœ ΟrŠœ£œ-™λ—™2Kšœ £ ™—K™—…—€Ξ»€