1# Orca
2#
3# Copyright 2005-2009 Sun Microsystems Inc.
4#
5# This library is free software; you can redistribute it and/or
6# modify it under the terms of the GNU Lesser General Public
7# License as published by the Free Software Foundation; either
8# version 2.1 of the License, or (at your option) any later version.
9#
10# This library is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13# Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with this library; if not, write to the
17# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
18# Boston MA  02110-1301 USA.
19
20"""A very experimental approach to the refreshable Braille display.  This
21module treats each line of the display as a sequential set of regions, where
22each region can potentially backed by an Accessible object.  Depending upon
23the Accessible object, the cursor routing keys can be used to perform
24operations on the Accessible object, such as invoking default actions or
25moving the text caret.
26"""
27
28__id__        = "$Id$"
29__version__   = "$Revision$"
30__date__      = "$Date$"
31__copyright__ = "Copyright (c) 2005-2009 Sun Microsystems Inc."
32__license__   = "LGPL"
33
34import locale
35import signal
36import os
37import re
38
39from gi.repository import GLib
40
41from . import brltablenames
42from . import cmdnames
43from . import debug
44from . import eventsynthesizer
45from . import logger
46from . import orca_state
47from . import settings
48from . import settings_manager
49
50from .orca_platform import tablesdir
51
52_logger = logger.getLogger()
53log = _logger.newLog("braille")
54_monitor = None
55_settingsManager = settings_manager.getManager()
56
57try:
58    msg = "BRAILLE: About to import brlapi."
59    debug.println(debug.LEVEL_INFO, msg, True)
60
61    import brlapi
62    _brlAPI = None
63    _brlAPIAvailable = True
64    _brlAPIRunning = False
65    _brlAPISourceId = 0
66except:
67    msg = "BRAILLE: Could not import brlapi."
68    debug.println(debug.LEVEL_INFO, msg, True)
69    _brlAPIAvailable = False
70    _brlAPIRunning = False
71else:
72    msg = "BRAILLE: brlapi imported %s" % brlapi
73    debug.println(debug.LEVEL_INFO, msg, True)
74
75try:
76    msg = "BRAILLE: About to import louis."
77    debug.println(debug.LEVEL_INFO, msg, True)
78    import louis
79except:
80    msg = "BRAILLE: Could not import liblouis"
81    debug.println(debug.LEVEL_INFO, msg, True)
82    louis = None
83else:
84    msg = "BRAILLE: liblouis imported %s" % louis
85    debug.println(debug.LEVEL_INFO, msg, True)
86
87    msg = "BRAILLE: tables location: %s" % tablesdir
88    debug.println(debug.LEVEL_INFO, msg, True)
89
90    # TODO: Can we get the tablesdir info at runtime?
91    if not tablesdir:
92        msg = "BRAILLE: Disabling liblouis due to unknown table location." \
93              "This usually means orca was built before liblouis was installed."
94        debug.println(debug.LEVEL_INFO, msg, True)
95        louis = None
96
97try:
98    from . import brlmon
99except:
100    settings.enableBrailleMonitor = False
101
102
103# brlapi keys which are not allowed to interrupt speech:
104#
105dontInteruptSpeechKeys = []
106if _brlAPIAvailable:
107    dontInteruptSpeechKeys = [ \
108        brlapi.KEY_CMD_HWINLT, brlapi.KEY_CMD_HWINRT, \
109        brlapi.KEY_CMD_FWINLT, brlapi.KEY_CMD_FWINRT, \
110        brlapi.KEY_CMD_FWINLTSKIP, brlapi.KEY_CMD_FWINRTSKIP, \
111        brlapi.KEY_CMD_LNUP, brlapi.KEY_CMD_LNDN]
112
113# Common names for most used BrlTTY commands, to be shown in the GUI:
114# ATM, the ones used in default.py are:
115#
116command_name = {}
117
118if _brlAPIAvailable:
119    command_name[brlapi.KEY_CMD_HWINLT]     = cmdnames.BRAILLE_LINE_LEFT
120    command_name[brlapi.KEY_CMD_FWINLT]     = cmdnames.BRAILLE_LINE_LEFT
121    command_name[brlapi.KEY_CMD_FWINLTSKIP] = cmdnames.BRAILLE_LINE_LEFT
122    command_name[brlapi.KEY_CMD_HWINRT]     = cmdnames.BRAILLE_LINE_RIGHT
123    command_name[brlapi.KEY_CMD_FWINRT]     = cmdnames.BRAILLE_LINE_RIGHT
124    command_name[brlapi.KEY_CMD_FWINRTSKIP] = cmdnames.BRAILLE_LINE_RIGHT
125    command_name[brlapi.KEY_CMD_LNUP]       = cmdnames.BRAILLE_LINE_UP
126    command_name[brlapi.KEY_CMD_LNDN]       = cmdnames.BRAILLE_LINE_DOWN
127    command_name[brlapi.KEY_CMD_FREEZE]     = cmdnames.BRAILLE_FREEZE
128    command_name[brlapi.KEY_CMD_TOP_LEFT]   = cmdnames.BRAILLE_TOP_LEFT
129    command_name[brlapi.KEY_CMD_BOT_LEFT]   = cmdnames.BRAILLE_BOTTOM_LEFT
130    command_name[brlapi.KEY_CMD_HOME]       = cmdnames.BRAILLE_HOME
131    command_name[brlapi.KEY_CMD_SIXDOTS]    = cmdnames.BRAILLE_SIX_DOTS
132    command_name[brlapi.KEY_CMD_ROUTE]      = cmdnames.BRAILLE_ROUTE_CURSOR
133    command_name[brlapi.KEY_CMD_CUTBEGIN]   = cmdnames.BRAILLE_CUT_BEGIN
134    command_name[brlapi.KEY_CMD_CUTLINE]    = cmdnames.BRAILLE_CUT_LINE
135
136# The size of the physical display (width, height).  The coordinate system of
137# the display is set such that the upper left is (0,0), x values increase from
138# left to right, and y values increase from top to bottom.
139#
140# For the purposes of testing w/o a braille display, we'll set the display
141# size to width=32 and height=1.
142#
143# [[[TODO: WDW - Only a height of 1 is support at this time.]]]
144#
145DEFAULT_DISPLAY_SIZE = 32
146_displaySize = [DEFAULT_DISPLAY_SIZE, 1]
147
148# The list of lines on the display.  This represents the entire amount of data
149# to be drawn on the display.  It will be clipped by the viewport if too large.
150#
151_lines = []
152
153# The region with focus.  This will be displayed at the home position.
154#
155_regionWithFocus = None
156
157# The last text information painted.  This has the following fields:
158#
159# lastTextObj = the last accessible
160# lastCaretOffset = the last caret offset of the last text displayed
161# lastLineOffset = the last line offset of the last text displayed
162# lastCursorCell = the last cell on the braille display for the caret
163#
164_lastTextInfo = (None, 0, 0, 0)
165
166# The viewport is a rectangular region of size _displaySize whose upper left
167# corner is defined by the point (x, line number).  As such, the viewport is
168# identified solely by its upper left point.
169#
170viewport = [0, 0]
171
172# The callback to call on a BrlTTY input event.  This is passed to
173# the init method.
174#
175_callback = None
176
177# If True, the given portion of the currently displayed line is showing
178# on the display.
179#
180endIsShowing = False
181beginningIsShowing = False
182
183# 1-based offset saying which braille cell has the cursor.  A value
184# of 0 means no cell has the cursor.
185#
186cursorCell = 0
187
188# The event source of a timeout used for flashing a message.
189#
190_flashEventSourceId = 0
191
192# Line information saved prior to flashing any messages
193#
194_saved = None
195
196# Set to True when we lower our output priority
197#
198idle = False
199
200# Translators: These are the braille translation table names for different
201# languages. You could read about braille tables at:
202# http://en.wikipedia.org/wiki/Braille
203#
204TABLE_NAMES = {"Cz-Cz-g1": brltablenames.CZ_CZ_G1,
205               "Es-Es-g1": brltablenames.ES_ES_G1,
206               "Fr-Ca-g2": brltablenames.FR_CA_G2,
207               "Fr-Fr-g2": brltablenames.FR_FR_G2,
208               "Lv-Lv-g1": brltablenames.LV_LV_G1,
209               "Nl-Nl-g1": brltablenames.NL_NL_G1,
210               "No-No-g0": brltablenames.NO_NO_G0,
211               "No-No-g1": brltablenames.NO_NO_G1,
212               "No-No-g2": brltablenames.NO_NO_G2,
213               "No-No-g3": brltablenames.NO_NO_G3,
214               "Pl-Pl-g1": brltablenames.PL_PL_G1,
215               "Pt-Pt-g1": brltablenames.PT_PT_G1,
216               "Se-Se-g1": brltablenames.SE_SE_G1,
217               "ar-ar-g1": brltablenames.AR_AR_G1,
218               "cy-cy-g1": brltablenames.CY_CY_G1,
219               "cy-cy-g2": brltablenames.CY_CY_G2,
220               "de-de-g0": brltablenames.DE_DE_G0,
221               "de-de-g1": brltablenames.DE_DE_G1,
222               "de-de-g2": brltablenames.DE_DE_G2,
223               "en-GB-g2": brltablenames.EN_GB_G2,
224               "en-gb-g1": brltablenames.EN_GB_G1,
225               "en-us-g1": brltablenames.EN_US_G1,
226               "en-us-g2": brltablenames.EN_US_G2,
227               "fr-ca-g1": brltablenames.FR_CA_G1,
228               "fr-fr-g1": brltablenames.FR_FR_G1,
229               "gr-gr-g1": brltablenames.GR_GR_G1,
230               "hi-in-g1": brltablenames.HI_IN_G1,
231               "hu-hu-comp8": brltablenames.HU_HU_8DOT,
232               "hu-hu-g1": brltablenames.HU_HU_G1,
233               "it-it-g1": brltablenames.IT_IT_G1,
234               "nl-be-g1": brltablenames.NL_BE_G1}
235
236def listTables():
237    tables = {}
238    try:
239        for fname in os.listdir(tablesdir):
240            if fname[-4:] in (".utb", ".ctb"):
241                alias = fname[:-4]
242                tables[TABLE_NAMES.get(alias, alias)] = \
243                    os.path.join(tablesdir, fname)
244    except OSError:
245        pass
246
247    return tables
248
249def getDefaultTable():
250    userLocale = locale.getlocale(locale.LC_MESSAGES)[0]
251    msg = "BRAILLE: User locale is %s" % userLocale
252    debug.println(debug.LEVEL_INFO, msg, True)
253
254    if userLocale in (None, "C"):
255        userLocale = locale.getdefaultlocale()[0]
256        msg = "BRAILLE: Default locale is %s" % userLocale
257        debug.println(debug.LEVEL_INFO, msg, True)
258
259    if userLocale in (None, "C"):
260        msg = "BRAILLE: Locale cannot be determined. Falling back on 'en-us'"
261        debug.println(debug.LEVEL_INFO, msg, True)
262        language = "en-us"
263    else:
264        language = "-".join(userLocale.split("_")).lower()
265
266    try:
267        tables = [x for x in os.listdir(tablesdir) if x[-4:] in (".utb", ".ctb")]
268    except OSError:
269        msg = "BRAILLE: Exception calling os.listdir for %s" % tablesdir
270        debug.println(debug.LEVEL_INFO, msg, True)
271        return ""
272
273    # Some of the tables are probably not a good choice for default table....
274    exclude = ["interline", "mathtext"]
275
276    # Some of the tables might be a better default than others. For instance, someone who
277    # can read grade 2 braille presumably can read grade 1; the reverse is not necessarily
278    # true. Literary braille might be easier for some users to read than computer braille.
279    # We can adjust this based on user feedback, but in general the goal is a sane default
280    # for the largest group of users; not the perfect default for all users.
281    prefer = ["g1", "g2", "comp6", "comp8"]
282
283    isCandidate = lambda t: t.startswith(language) and not any(e in t for e in exclude)
284    tables = list(filter(isCandidate, tables))
285    msg = "BRAILLE: %i candidate tables for locale found: %s" % (len(tables), ", ".join(tables))
286    debug.println(debug.LEVEL_INFO, msg, True)
287
288    if not tables:
289        return ""
290
291    for p in prefer:
292        for table in tables:
293            if p in table:
294                return os.path.join(tablesdir, table)
295
296    # If we couldn't find a preferred match, just go with the first match for the locale.
297    return os.path.join(tablesdir, tables[0])
298
299if louis:
300    _defaultContractionTable = getDefaultTable()
301    msg = "BRAILLE: Default contraction table is: %s" % _defaultContractionTable
302    debug.println(debug.LEVEL_INFO, msg, True)
303
304def _printBrailleEvent(level, command):
305    """Prints out a Braille event.  The given level may be overridden
306    if the eventDebugLevel (see debug.setEventDebugLevel) is greater in
307    debug.py.
308
309    Arguments:
310    - command: the BrlAPI command for the key that was pressed.
311    """
312
313    debug.printInputEvent(
314        level,
315        "BRAILLE EVENT: %s" % repr(command))
316
317class Region:
318    """A Braille region to be displayed on the display.  The width of
319    each region is determined by its string.
320    """
321
322    def __init__(self, string, cursorOffset=0, expandOnCursor=False):
323        """Creates a new Region containing the given string.
324
325        Arguments:
326        - string: the string to be displayed
327        - cursorOffset: a 0-based index saying where to draw the cursor
328                        for this Region if it gets focus.
329        """
330
331        if not string:
332            string = ""
333
334        # If louis is None, then we don't go into contracted mode.
335        self.contracted = settings.enableContractedBraille and louis is not None
336
337        self.expandOnCursor = expandOnCursor
338
339        # The uncontracted string for the line.
340        #
341        self.rawLine = string.strip("\n")
342
343        if self.contracted:
344            self.contractionTable = settings.brailleContractionTable or _defaultContractionTable
345            if string.strip():
346                msg = "BRAILLE: Contracting '%s' with table %s" % (string, self.contractionTable)
347                debug.println(debug.LEVEL_INFO, msg, True)
348
349            self.string, self.inPos, self.outPos, self.cursorOffset = \
350                         self.contractLine(self.rawLine,
351                                           cursorOffset, expandOnCursor)
352        else:
353            if string.strip():
354                if not settings.enableContractedBraille:
355                    msg = "BRAILLE: Not contracting '%s' because contracted braille is not enabled." % string
356                    debug.println(debug.LEVEL_INFO, msg, True)
357                else:
358                    msg = "BRAILLE: Not contracting '%s' due to problem with liblouis." % string
359                    debug.println(debug.LEVEL_INFO, msg, True)
360
361            self.string = self.rawLine
362            self.cursorOffset = cursorOffset
363
364    def __str__(self):
365        return "Region: '%s', %d" % (self.string, self.cursorOffset)
366
367    def processRoutingKey(self, offset):
368        """Processes a cursor routing key press on this Component.  The offset
369        is 0-based, where 0 represents the leftmost character of string
370        associated with this region.  Note that the zeroeth character may have
371        been scrolled off the display."""
372        pass
373
374    def getAttributeMask(self, getLinkMask=True):
375        """Creates a string which can be used as the attrOr field of brltty's
376        write structure for the purpose of indicating text attributes, links,
377        and selection.
378
379        Arguments:
380        - getLinkMask: Whether or not we should take the time to get
381          the attributeMask for links. Reasons we might not want to
382          include knowing that we will fail and/or it taking an
383          unreasonable amount of time (AKA Gecko).
384        """
385
386        # Create an empty mask.
387        #
388        return '\x00' * len(self.string)
389
390    def repositionCursor(self):
391        """Reposition the cursor offset for contracted mode.
392        """
393        if self.contracted:
394            self.string, self.inPos, self.outPos, self.cursorOffset = \
395                       self.contractLine(self.rawLine,
396                                         self.cursorOffset,
397                                         self.expandOnCursor)
398
399    def contractLine(self, line, cursorOffset=0, expandOnCursor=False):
400        """Contract the given line. Returns the contracted line, and the
401        cursor position in the contracted line.
402
403        Arguments:
404        - line: Line to contract.
405        - cursorOffset: Offset of cursor,defaults to 0.
406        - expandOnCursor: Expand word under cursor, False by default.
407        """
408
409        try:
410            cursorOnSpace = line[cursorOffset] == ' '
411        except IndexError:
412            cursorOnSpace = False
413
414        if not expandOnCursor or cursorOnSpace:
415            mode = 0
416        else:
417            mode = louis.compbrlAtCursor
418
419        contracted, inPos, outPos, cursorPos = \
420            louis.translate([self.contractionTable],
421                            line,
422                            cursorPos=cursorOffset,
423                            mode=mode)
424
425        # Make sure the cursor is at a realistic spot.
426        # Note that if cursorOffset is beyond the end of the buffer,
427        # a spurious value is returned by liblouis in cursorPos.
428        #
429        if cursorOffset >= len(line):
430            cursorPos = len(contracted)
431        else:
432            cursorPos = min(cursorPos, len(contracted))
433
434        return contracted, inPos, outPos, cursorPos
435
436    def displayToBufferOffset(self, display_offset):
437        try:
438            offset = self.inPos[display_offset]
439        except IndexError:
440            # Off the chart, we just place the cursor at the end of the line.
441            offset = len(self.rawLine)
442        except AttributeError:
443            # Not in contracted mode.
444            offset = display_offset
445
446        return offset
447
448    def setContractedBraille(self, contracted):
449        if contracted:
450            self.contractionTable = settings.brailleContractionTable or _defaultContractionTable
451            self.contractRegion()
452        else:
453            self.expandRegion()
454
455    def contractRegion(self):
456        if self.contracted:
457            return
458        self.string, self.inPos, self.outPos, self.cursorOffset = \
459                     self.contractLine(self.rawLine,
460                                       self.cursorOffset,
461                                       self.expandOnCursor)
462        self.contracted = True
463
464    def expandRegion(self):
465        if not self.contracted:
466            return
467        self.string = self.rawLine
468        try:
469            self.cursorOffset = self.inPos[self.cursorOffset]
470        except IndexError:
471            self.cursorOffset = len(self.string)
472        self.contracted = False
473
474class Component(Region):
475    """A subclass of Region backed by an accessible.  This Region will react
476    to any cursor routing key events and perform the default action on the
477    accessible, if a default action exists.
478    """
479
480    def __init__(self, accessible, string, cursorOffset=0,
481                 indicator='', expandOnCursor=False):
482        """Creates a new Component.
483
484        Arguments:
485        - accessible: the accessible
486        - string: the string to use to represent the component
487        - cursorOffset: a 0-based index saying where to draw the cursor
488                        for this Region if it gets focus.
489        """
490
491        Region.__init__(self, string, cursorOffset, expandOnCursor)
492        if indicator:
493            if self.string:
494                self.string = indicator + ' ' + self.string
495            else:
496                self.string = indicator
497
498        self.accessible = accessible
499
500    def __str__(self):
501        return "Component: '%s', %d" % (self.string, self.cursorOffset)
502
503    def getCaretOffset(self, offset):
504        """Returns the caret position of the given offset if the object
505        has text with a caret.  Otherwise, returns -1.
506
507        Arguments:
508        - offset: 0-based offset of the cell on the physical display
509        """
510        return -1
511
512    def processRoutingKey(self, offset):
513        """Processes a cursor routing key press on this Component.  The offset
514        is 0-based, where 0 represents the leftmost character of string
515        associated with this region.  Note that the zeroeth character may have
516        been scrolled off the display."""
517
518        if orca_state.activeScript and orca_state.activeScript.utilities.\
519           grabFocusBeforeRouting(self.accessible, offset):
520            try:
521                self.accessible.queryComponent().grabFocus()
522            except:
523                pass
524
525        try:
526            action = self.accessible.queryAction()
527        except:
528            # Do a mouse button 1 click if we have to.  For example, page tabs
529            # don't have any actions but we want to be able to select them with
530            # the cursor routing key.
531            #
532            debug.println(debug.LEVEL_FINEST,
533                          "braille.Component.processRoutingKey: no action")
534            try:
535                eventsynthesizer.clickObject(self.accessible, 1)
536            except:
537                debug.println(debug.LEVEL_SEVERE,
538                              "Could not process routing key:")
539                debug.printException(debug.LEVEL_SEVERE)
540        else:
541            action.doAction(0)
542
543class Link(Component):
544    """A subclass of Component backed by an accessible.  This Region will be
545    marked as a link by dots 7 or 8, depending on the user's preferences.
546    """
547
548    def __init__(self, accessible, string, cursorOffset=0):
549        """Initialize a Link region. similar to Component, but here we always
550        have the region expand on cursor."""
551        Component.__init__(self, accessible, string, cursorOffset, '', True)
552
553    def __str__(self):
554        return "Link: '%s', %d" % (self.string, self.cursorOffset)
555
556    def getAttributeMask(self, getLinkMask=True):
557        """Creates a string which can be used as the attrOr field of brltty's
558        write structure for the purpose of indicating text attributes and
559        selection.
560        Arguments:
561
562        - getLinkMask: Whether or not we should take the time to get
563          the attributeMask for links. Reasons we might not want to
564          include knowing that we will fail and/or it taking an
565          unreasonable amount of time (AKA Gecko).
566        """
567
568        # Create an link indicator mask.
569        #
570        return chr(settings.brailleLinkIndicator) * len(self.string)
571
572class Text(Region):
573    """A subclass of Region backed by a Text object.  This Region will
574    react to any cursor routing key events by positioning the caret in
575    the associated text object. The line displayed will be the
576    contents of the text object preceded by an optional label.
577    [[[TODO: WDW - need to add in text selection capabilities.  Logged
578    as bugzilla bug 319754.]]]"""
579
580    def __init__(self, accessible, label="", eol="",
581                 startOffset=None, endOffset=None):
582        """Creates a new Text region.
583
584        Arguments:
585        - accessible: the accessible that implements AccessibleText
586        - label: an optional label to display
587        """
588
589        self.accessible = accessible
590        if orca_state.activeScript and self.accessible:
591            [string, self.caretOffset, self.lineOffset] = \
592                 orca_state.activeScript.getTextLineAtCaret(
593                     self.accessible, startOffset=startOffset, endOffset=endOffset)
594        else:
595            string = ""
596            self.caretOffset = 0
597            self.lineOffset = 0
598
599        try:
600            endOffset = endOffset - self.lineOffset
601        except TypeError:
602            endOffset = len(string)
603
604        try:
605            self.startOffset = startOffset - self.lineOffset
606        except TypeError:
607            self.startOffset = 0
608
609        string = string[self.startOffset:endOffset]
610
611        self.caretOffset -= self.startOffset
612
613        cursorOffset = min(self.caretOffset - self.lineOffset, len(string))
614
615        self._maxCaretOffset = self.lineOffset + len(string)
616
617        self.eol = eol
618
619        if label:
620            self.label = label + ' '
621        else:
622            self.label = ''
623
624        string = self.label + string
625
626        cursorOffset += len(self.label)
627
628        Region.__init__(self, string, cursorOffset, True)
629
630        if not self.contracted and not settings.disableBrailleEOL:
631            self.string += self.eol
632        elif settings.disableBrailleEOL:
633            # Ensure there is a place to click on at the end of a line
634            # so the user can route the caret to the end of the line.
635            #
636            self.string += ' '
637
638    def __str__(self):
639        return "Text: '%s', %d" % (self.string, self.cursorOffset)
640
641    def repositionCursor(self):
642        """Attempts to reposition the cursor in response to a new
643        caret position.  If it is possible (i.e., the caret is on
644        the same line as it was), reposition the cursor and return
645        True.  Otherwise, return False.
646        """
647
648        if not _regionWithFocus:
649            return False
650
651        [string, caretOffset, lineOffset] = \
652                 orca_state.activeScript.getTextLineAtCaret(self.accessible)
653
654        cursorOffset = min(caretOffset - lineOffset, len(string))
655
656        if lineOffset != self.lineOffset:
657            return False
658
659        self.caretOffset = caretOffset
660        self.lineOffset = lineOffset
661
662        cursorOffset += len(self.label)
663
664        if self.contracted:
665            self.string, self.inPos, self.outPos, cursorOffset = \
666                       self.contractLine(self.rawLine, cursorOffset, True)
667
668        self.cursorOffset = cursorOffset
669
670        return True
671
672    def getCaretOffset(self, offset):
673        """Returns the caret position of the given offset if the object
674        has text with a caret.  Otherwise, returns -1.
675
676        Arguments:
677        - offset: 0-based offset of the cell on the physical display
678        """
679        offset = self.displayToBufferOffset(offset)
680
681        if offset < 0:
682            return -1
683
684        return min(self.lineOffset + offset, self._maxCaretOffset)
685
686    def processRoutingKey(self, offset):
687        """Processes a cursor routing key press on this Component.  The offset
688        is 0-based, where 0 represents the leftmost character of text
689        associated with this region.  Note that the zeroeth character may have
690        been scrolled off the display.
691        """
692
693        caretOffset = self.getCaretOffset(offset)
694
695        if caretOffset < 0:
696            return
697
698        orca_state.activeScript.utilities.setCaretOffset(
699            self.accessible, caretOffset)
700
701    def getAttributeMask(self, getLinkMask=True):
702        """Creates a string which can be used as the attrOr field of brltty's
703        write structure for the purpose of indicating text attributes, links,
704        and selection.
705
706        Arguments:
707        - getLinkMask: Whether or not we should take the time to get
708          the attributeMask for links. Reasons we might not want to
709          include knowing that we will fail and/or it taking an
710          unreasonable amount of time (AKA Gecko).
711        """
712
713        try:
714            text = self.accessible.queryText()
715        except NotImplementedError:
716            return ''
717
718        # Start with an empty mask.
719        #
720        stringLength = len(self.rawLine) - len(self.label)
721        lineEndOffset = self.lineOffset + stringLength
722        regionMask = [settings.BRAILLE_UNDERLINE_NONE]*stringLength
723
724        attrIndicator = settings.textAttributesBrailleIndicator
725        selIndicator = settings.brailleSelectorIndicator
726        linkIndicator = settings.brailleLinkIndicator
727        script = orca_state.activeScript
728
729        if getLinkMask and linkIndicator != settings.BRAILLE_UNDERLINE_NONE:
730            try:
731                hyperText = self.accessible.queryHypertext()
732                nLinks = hyperText.getNLinks()
733            except:
734                nLinks = 0
735
736            n = 0
737            while n < nLinks:
738                link = hyperText.getLink(n)
739                if self.lineOffset <= link.startIndex:
740                    for i in range(link.startIndex, link.endIndex):
741                        try:
742                            regionMask[i] |= linkIndicator
743                        except:
744                            pass
745                n += 1
746
747        if attrIndicator:
748            keys, enabledAttributes = script.utilities.stringToKeysAndDict(
749                settings.enabledBrailledTextAttributes)
750
751            offset = self.lineOffset
752            while offset < lineEndOffset:
753                attributes, startOffset, endOffset = \
754                    script.utilities.textAttributes(self.accessible,
755                                                    offset, True)
756                if endOffset <= offset:
757                    break
758                mask = settings.BRAILLE_UNDERLINE_NONE
759                offset = endOffset
760                for attrib in attributes:
761                    if enabledAttributes.get(attrib, '') != '':
762                        if enabledAttributes[attrib] != attributes[attrib]:
763                            mask = attrIndicator
764                            break
765                if mask != settings.BRAILLE_UNDERLINE_NONE:
766                    maskStart = max(startOffset - self.lineOffset, 0)
767                    maskEnd = min(endOffset - self.lineOffset, stringLength)
768                    for i in range(maskStart, maskEnd):
769                        regionMask[i] |= attrIndicator
770
771        if selIndicator:
772            selections = script.utilities.allTextSelections(self.accessible)
773            for startOffset, endOffset in selections:
774                maskStart = max(startOffset - self.lineOffset, 0)
775                maskEnd = min(endOffset - self.lineOffset, stringLength)
776                for i in range(maskStart, maskEnd):
777                    regionMask[i] |= selIndicator
778
779        if self.contracted:
780            contractedMask = [0] * len(self.rawLine)
781            outPos = self.outPos[len(self.label):]
782            if self.label:
783                # Transform the offsets.
784                outPos = \
785                       [offset - len(self.label) - 1 for offset in outPos]
786            for i, m in enumerate(regionMask):
787                try:
788                    contractedMask[outPos[i]] |= m
789                except IndexError:
790                    continue
791            regionMask = contractedMask[:len(self.string)]
792
793        # Add empty mask characters for the EOL character as well as for
794        # any label that might be present.
795        #
796        regionMask += [0]*len(self.eol)
797
798        if self.label:
799            regionMask = [0]*len(self.label) + regionMask
800
801        return ''.join(map(chr, regionMask))
802
803    def contractLine(self, line, cursorOffset=0, expandOnCursor=True):
804        contracted, inPos, outPos, cursorPos = Region.contractLine(
805            self, line, cursorOffset, expandOnCursor)
806
807        return contracted + self.eol, inPos, outPos, cursorPos
808
809    def displayToBufferOffset(self, display_offset):
810        offset = Region.displayToBufferOffset(self, display_offset)
811        offset += self.startOffset
812        offset -= len(self.label)
813        return offset
814
815    def setContractedBraille(self, contracted):
816        Region.setContractedBraille(self, contracted)
817        if not contracted:
818            self.string += self.eol
819
820class ReviewComponent(Component):
821    """A subclass of Component that is to be used for flat review mode."""
822
823    def __init__(self, accessible, string, cursorOffset, zone):
824        """Creates a new Component.
825
826        Arguments:
827        - accessible: the accessible
828        - string: the string to use to represent the component
829        - cursorOffset: a 0-based index saying where to draw the cursor
830                        for this Region if it gets focus.
831        - zone: the flat review Zone associated with this component
832        """
833        Component.__init__(self, accessible, string,
834                           cursorOffset, expandOnCursor=True)
835        self.zone = zone
836
837    def __str__(self):
838        return "ReviewComponent: %s, %d" % (self.zone, self.cursorOffset)
839
840class ReviewText(Region):
841    """A subclass of Region backed by a Text object.  This Region will
842    does not react to the caret changes, but will react if one updates
843    the cursorPosition.  This class is meant to be used by flat review
844    mode to show the current character position.
845    """
846
847    def __init__(self, accessible, string, lineOffset, zone):
848        """Creates a new Text region.
849
850        Arguments:
851        - accessible: the accessible that implements AccessibleText
852        - string: the string to use to represent the component
853        - lineOffset: the character offset into where the text line starts
854        - zone: the flat review Zone associated with this component
855        """
856        Region.__init__(self, string, expandOnCursor=True)
857        self.accessible = accessible
858        self.lineOffset = lineOffset
859        self.zone = zone
860
861    def __str__(self):
862        return "ReviewText: %s, %d" % (self.zone, self.cursorOffset)
863
864    def getCaretOffset(self, offset):
865        """Returns the caret position of the given offset if the object
866        has text with a caret.  Otherwise, returns -1.
867
868        Arguments:
869        - offset: 0-based offset of the cell on the physical display
870        """
871        offset = self.displayToBufferOffset(offset)
872
873        if offset < 0:
874            return -1
875
876        return self.lineOffset + offset
877
878    def processRoutingKey(self, offset):
879        """Processes a cursor routing key press on this Component.  The offset
880        is 0-based, where 0 represents the leftmost character of text
881        associated with this region.  Note that the zeroeth character may have
882        been scrolled off the display."""
883
884        caretOffset = self.getCaretOffset(offset)
885        orca_state.activeScript.utilities.setCaretOffset(
886            self.accessible, caretOffset)
887
888class Line:
889    """A horizontal line on the display.  Each Line is composed of a sequential
890    set of Regions.
891    """
892
893    def __init__(self, region=None):
894        self.regions = []
895        self.string = ""
896        if region:
897            self.addRegion(region)
898
899    def addRegion(self, region):
900        self.regions.append(region)
901
902    def addRegions(self, regions):
903        self.regions.extend(regions)
904
905    def getLineInfo(self, getLinkMask=True):
906        """Computes the complete string for this line as well as a
907        0-based index where the focused region starts on this line.
908        If the region with focus is not on this line, then the index
909        will be -1.
910
911        Arguments:
912        - getLinkMask: Whether or not we should take the time to get
913          the attributeMask for links. Reasons we might not want to
914          include knowing that we will fail and/or it taking an
915          unreasonable amount of time (AKA Gecko).
916
917        Returns [string, offsetIndex, attributeMask, ranges]
918        """
919
920        string = ""
921        focusOffset = -1
922        attributeMask = ""
923        ranges = []
924        for region in self.regions:
925            if region == _regionWithFocus:
926                focusOffset = len(string)
927            if region.string:
928                string += region.string
929            mask = region.getAttributeMask(getLinkMask)
930            attributeMask += mask
931
932        words = [word.span() for word in re.finditer(r"(^\s+|\S+\s*)", string)]
933        span = []
934        for start, end in words:
935            if span and end - span[0] > _displaySize[0]:
936                ranges.append(span)
937                span = []
938            if not span:
939                # Subdivide long words that exceed the display width.
940                wordLength = end - start
941                if wordLength > _displaySize[0]:
942                    displayWidths = wordLength // _displaySize[0]
943                    if displayWidths:
944                        for i in range(displayWidths):
945                            ranges.append([start + i * _displaySize[0], start + (i+1) * _displaySize[0]])
946                        if wordLength % _displaySize[0]:
947                            span = [start + displayWidths * _displaySize[0], end]
948                        else:
949                            continue
950                else:
951                    span = [start, end]
952            else:
953                span[1] = end
954            if end == focusOffset:
955                ranges.append(span)
956                span = []
957        else:
958            if span:
959                ranges.append(span)
960
961        return [string, focusOffset, attributeMask, ranges]
962
963    def getRegionAtOffset(self, offset):
964        """Finds the Region at the given 0-based offset in this line.
965
966        Returns the [region, offsetinregion] where the region is
967        the region at the given offset, and offsetinregion is the
968        0-based offset from the beginning of the region, representing
969        where in the region the given offset is."""
970
971        # Translate the cursor offset for this line into a cursor offset
972        # for a region, and then pass the event off to the region for
973        # handling.
974        #
975        foundRegion = None
976        string = ""
977        pos = 0
978        for region in self.regions:
979            foundRegion = region
980            string = string + region.string
981            if len(string) > offset:
982                break
983            else:
984                pos = len(string)
985
986        if offset >= len(string):
987            return [None, -1]
988        else:
989            return [foundRegion, offset - pos]
990
991    def processRoutingKey(self, offset):
992        """Processes a cursor routing key press on this Component.  The offset
993        is 0-based, where 0 represents the leftmost character of string
994        associated with this line.  Note that the zeroeth character may have
995        been scrolled off the display."""
996
997        [region, regionOffset] = self.getRegionAtOffset(offset)
998        if region:
999            region.processRoutingKey(regionOffset)
1000
1001    def setContractedBraille(self, contracted):
1002        for region in self.regions:
1003            region.setContractedBraille(contracted)
1004
1005def getRegionAtCell(cell):
1006    """Given a 1-based cell offset, return the braille region
1007    associated with that cell in the form of [region, offsetinregion]
1008    where 'region' is the region associated with the cell and
1009    'offsetinregion' is the 0-based offset of where the cell is
1010    in the region, where 0 represents the beginning of the region.
1011    """
1012
1013    if len(_lines) > 0:
1014        offset = (cell - 1) + viewport[0]
1015        lineNum = viewport[1]
1016        return _lines[lineNum].getRegionAtOffset(offset)
1017    else:
1018        return [None, -1]
1019
1020def getCaretContext(event):
1021    """Gets the accesible and caret offset associated with the given
1022    event.  The event should have a BrlAPI event that contains an
1023    argument value that corresponds to a cell on the display.
1024
1025    Arguments:
1026    - event: an instance of input_event.BrailleEvent.  event.event is
1027    the dictionary form of the expanded BrlAPI event.
1028    """
1029
1030    offset = event.event["argument"]
1031    [region, regionOffset] = getRegionAtCell(offset + 1)
1032    if region and (isinstance(region, Text) or isinstance(region, ReviewText)):
1033        accessible = region.accessible
1034        caretOffset = region.getCaretOffset(regionOffset)
1035    else:
1036        accessible = None
1037        caretOffset = -1
1038
1039    return [accessible, caretOffset]
1040
1041def clear():
1042    """Clears the logical structure, but keeps the Braille display as is
1043    (until a refresh operation).
1044    """
1045
1046    global _lines
1047    global _regionWithFocus
1048    global viewport
1049
1050    _lines = []
1051    _regionWithFocus = None
1052    viewport = [0, 0]
1053
1054def setLines(lines):
1055    global _lines
1056    _lines = lines
1057
1058def addLine(line):
1059    """Adds a line to the logical display for painting.  The line is added to
1060    the end of the current list of known lines.  It is necessary for the
1061    viewport to be over the lines and for refresh to be called for the new
1062    line to be painted.
1063
1064    Arguments:
1065    - line: an instance of Line to add.
1066    """
1067
1068    _lines.append(line)
1069    line._index = len(_lines)
1070
1071def getShowingLine():
1072    """Returns the Line that is currently being painted on the display.
1073    """
1074    if len(_lines) > 0:
1075        return _lines[viewport[1]]
1076    else:
1077        return Line()
1078
1079def setFocus(region, panToFocus=True, getLinkMask=True):
1080    """Specififes the region with focus.  This region will be positioned
1081    at the home position if panToFocus is True.
1082
1083    Arguments:
1084    - region: the given region, which much be in a line that has been
1085      added to the logical display
1086    - panToFocus: whether or not to position the region at the home
1087      position
1088    - getLinkMask: Whether or not we should take the time to get the
1089      attributeMask for links. Reasons we might not want to include
1090      knowing that we will fail and/or it taking an unreasonable
1091      amount of time (AKA Gecko).
1092    """
1093
1094    global _regionWithFocus
1095
1096    _regionWithFocus = region
1097
1098    if not panToFocus or (not _regionWithFocus):
1099        return
1100
1101    # Adjust the viewport according to the new region with focus.
1102    # The goal is to have the first cell of the region be in the
1103    # home position, but we will give priority to make sure the
1104    # cursor for the region is on the display.  For example, when
1105    # faced with a long text area, we'll show the position with
1106    # the caret vs. showing the beginning of the region.
1107
1108    lineNum = 0
1109    done = False
1110    for line in _lines:
1111        for reg in line.regions:
1112            if reg == _regionWithFocus:
1113                viewport[1] = lineNum
1114                done = True
1115                break
1116        if done:
1117            break
1118        else:
1119            lineNum += 1
1120
1121    line = _lines[viewport[1]]
1122    [string, offset, attributeMask, ranges] = line.getLineInfo(getLinkMask)
1123
1124    # If the cursor is too far right, we scroll the viewport
1125    # so the cursor will be on the last cell of the display.
1126    #
1127    if _regionWithFocus.cursorOffset >= _displaySize[0]:
1128        offset += _regionWithFocus.cursorOffset - _displaySize[0] + 1
1129
1130    viewport[0] = max(0, offset)
1131
1132def _idleBraille():
1133    """Try to hand off control to other screen readers without completely
1134    shutting down the BrlAPI connection"""
1135
1136    global idle
1137
1138    if not idle:
1139        try:
1140            msg = "BRAILLE: Attempting to idle braille."
1141            debug.println(debug.LEVEL_INFO, msg, True)
1142            _brlAPI.setParameter(brlapi.PARAM_CLIENT_PRIORITY, 0, False, 0)
1143            idle = True
1144        except:
1145            msg = "BRAILLE: Idling braille failled. This requires BrlAPI >= 0.8."
1146            debug.println(debug.LEVEL_INFO, msg, True)
1147            pass
1148        else:
1149            msg = "BRAILLE: Idling braille succeeded."
1150            debug.println(debug.LEVEL_INFO, msg, True)
1151
1152    return idle
1153
1154def _clearBraille():
1155    """Clear Braille output, hand off control to other screen readers, without
1156    completely shutting down the BrlAPI connection"""
1157
1158    if not _brlAPIRunning:
1159        # We do want to try to clear the output we left on the device
1160        init(_callback)
1161
1162    if _brlAPIRunning:
1163        try:
1164            _brlAPI.writeText("", 0)
1165            _idleBraille()
1166        except:
1167            msg = "BRAILLE: BrlTTY seems to have disappeared."
1168            debug.println(debug.LEVEL_WARNING, msg, True)
1169            shutdown()
1170
1171def _enableBraille():
1172    """Re-enable Braille output after making it idle or clearing it"""
1173    global idle
1174
1175    msg = "BRAILLE: Enabling braille. BrlAPI running: %s" % _brlAPIRunning
1176    debug.println(debug.LEVEL_INFO, msg, True)
1177
1178    if not _brlAPIRunning:
1179        msg = "BRAILLE: Need to initialize first."
1180        debug.println(debug.LEVEL_INFO, msg, True)
1181        init(_callback)
1182
1183    if _brlAPIRunning:
1184        if idle:
1185            msg = "BRAILLE: Is running, but idling."
1186            debug.println(debug.LEVEL_INFO, msg, True)
1187            try:
1188                # Restore default priority
1189                msg = "BRAILLE: Attempting to de-idle braille."
1190                debug.println(debug.LEVEL_INFO, msg, True)
1191                _brlAPI.setParameter(brlapi.PARAM_CLIENT_PRIORITY, 0, False, 50)
1192                idle = False
1193            except:
1194                msg = "BRAILLE: could not restore priority"
1195                debug.println(debug.LEVEL_INFO, msg, True)
1196            else:
1197                msg = "BRAILLE: De-idle succeeded."
1198                debug.println(debug.LEVEL_INFO, msg, True)
1199
1200def disableBraille():
1201    """Hand off control to other screen readers, shutting down the BrlAPI
1202    connection if needed"""
1203
1204    global idle
1205
1206    msg = "BRAILLE: Disabling braille. BrlAPI running: %s" % _brlAPIRunning
1207    debug.println(debug.LEVEL_INFO, msg, True)
1208
1209    if _brlAPIRunning and not idle:
1210        msg = "BRAILLE: BrlApi running and not idle."
1211        debug.println(debug.LEVEL_INFO, msg, True)
1212
1213        if not _idleBraille() and not _settingsManager.getSetting('enableBraille'):
1214            # BrlAPI before 0.8 and we really want to shut down
1215            msg = "BRAILLE: could not go idle, completely shut down"
1216            debug.println(debug.LEVEL_INFO, msg, True)
1217            shutdown()
1218
1219def checkBrailleSetting():
1220    """Disable Braille if it got disabled in the preferences"""
1221
1222    msg = "BRAILLE: Checking braille setting."
1223    debug.println(debug.LEVEL_INFO, msg, True)
1224
1225    if not _settingsManager.getSetting('enableBraille'):
1226        disableBraille()
1227
1228def refresh(panToCursor=True, targetCursorCell=0, getLinkMask=True, stopFlash=True):
1229    """Repaints the Braille on the physical display.  This clips the entire
1230    logical structure by the viewport and also sets the cursor to the
1231    appropriate location.  [[[TODO: WDW - I'm not sure how BrlTTY handles
1232    drawing to displays with more than one line, so I'm only going to handle
1233    drawing one line right now.]]]
1234
1235    Arguments:
1236    - panToCursor: if True, will adjust the viewport so the cursor is showing.
1237    - targetCursorCell: Only effective if panToCursor is True.
1238      0 means automatically place the cursor somewhere on the display so
1239      as to minimize movement but show as much of the line as possible.
1240      A positive value is a 1-based target cell from the left side of
1241      the display and a negative value is a 1-based target cell from the
1242      right side of the display.
1243    - getLinkMask: Whether or not we should take the time to get the
1244      attributeMask for links. Reasons we might not want to include
1245      knowing that we will fail and/or it taking an unreasonable
1246      amount of time (AKA Gecko).
1247    - stopFlash: if True, kill any flashed message that may be showing.
1248    """
1249
1250    # TODO - JD: Split this work out into smaller methods.
1251
1252    global endIsShowing
1253    global beginningIsShowing
1254    global cursorCell
1255    global _monitor
1256    global _lastTextInfo
1257
1258    msg = "BRAILLE: Refresh. Pan: %s target: %i" % (panToCursor, targetCursorCell)
1259    debug.println(debug.LEVEL_INFO, msg, True)
1260
1261    if stopFlash:
1262        killFlash(restoreSaved=False)
1263
1264    # TODO - JD: This should be taken care of in orca.py.
1265    if not _settingsManager.getSetting('enableBraille') \
1266       and not _settingsManager.getSetting('enableBrailleMonitor'):
1267        if _brlAPIRunning:
1268            msg = "BRAILLE: FIXME - Braille disabled, but not properly shut down."
1269            debug.println(debug.LEVEL_INFO, msg, True)
1270            shutdown()
1271        _lastTextInfo = (None, 0, 0, 0)
1272        return
1273
1274    if len(_lines) == 0:
1275        _clearBraille()
1276        _lastTextInfo = (None, 0, 0, 0)
1277        return
1278
1279
1280    lastTextObj, lastCaretOffset, lastLineOffset, lastCursorCell = _lastTextInfo
1281    msg = "BRAILLE: Last text obj: %s (Caret: %i, Line: %i, Cell: %i)" % _lastTextInfo
1282    debug.println(debug.LEVEL_INFO, msg, True)
1283
1284    if _regionWithFocus and isinstance(_regionWithFocus, Text):
1285        currentTextObj = _regionWithFocus.accessible
1286        currentCaretOffset = _regionWithFocus.caretOffset
1287        currentLineOffset = _regionWithFocus.lineOffset
1288    else:
1289        currentTextObj = None
1290        currentCaretOffset = 0
1291        currentLineOffset = 0
1292
1293    onSameLine = currentTextObj and currentTextObj == lastTextObj \
1294        and currentLineOffset == lastLineOffset
1295
1296    msg = "BRAILLE: Current text obj: %s (Caret: %i, Line: %i). On same line: %s" % \
1297        (currentTextObj, currentCaretOffset, currentLineOffset, bool(onSameLine))
1298    debug.println(debug.LEVEL_INFO, msg, True)
1299
1300    if targetCursorCell < 0:
1301        targetCursorCell = _displaySize[0] + targetCursorCell + 1
1302        msg = "BRAILLE: Adjusted targetCursorCell to: %i" % targetCursorCell
1303        debug.println(debug.LEVEL_INFO, msg, True)
1304
1305    # If there is no target cursor cell and panning to cursor was
1306    # requested, then try to set one.  We
1307    # currently only do this for text objects, and we do so by looking
1308    # at the last position of the caret offset and cursor cell.  The
1309    # primary goal here is to keep the cursor movement on the display
1310    # somewhat predictable.
1311
1312    if panToCursor and targetCursorCell == 0 and onSameLine:
1313        if lastCursorCell == 0:
1314            msg = "BRAILLE: Not adjusting targetCursorCell. User panned caret out of view."
1315            debug.println(debug.LEVEL_INFO, msg, True)
1316        elif lastCaretOffset == currentCaretOffset:
1317            targetCursorCell = lastCursorCell
1318            msg = "BRAILLE: Setting targetCursorCell to previous value. Caret hasn't moved."
1319            debug.println(debug.LEVEL_INFO, msg, True)
1320        elif lastCaretOffset < currentCaretOffset:
1321            newLocation = lastCursorCell + (currentCaretOffset - lastCaretOffset)
1322            if newLocation <= _displaySize[0]:
1323                msg = "BRAILLE: Setting targetCursorCell based on offset: %i" % newLocation
1324                debug.println(debug.LEVEL_INFO, msg, True)
1325                targetCursorCell = newLocation
1326            else:
1327                msg = "BRAILLE: Setting targetCursorCell to end of display."
1328                debug.println(debug.LEVEL_INFO, msg, True)
1329                targetCursorCell = _displaySize[0]
1330        elif lastCaretOffset > currentCaretOffset:
1331            newLocation = lastCursorCell - (lastCaretOffset - currentCaretOffset)
1332            if newLocation >= 1:
1333                msg = "BRAILLE: Setting targetCursorCell based on offset: %i" % newLocation
1334                debug.println(debug.LEVEL_INFO, msg, True)
1335                targetCursorCell = newLocation
1336            else:
1337                msg = "BRAILLE: Setting targetCursorCell to start of display."
1338                debug.println(debug.LEVEL_INFO, msg, True)
1339                targetCursorCell = 1
1340
1341    # Now, we figure out the 0-based offset for where the cursor actually is in the string.
1342
1343    line = _lines[viewport[1]]
1344    [string, focusOffset, attributeMask, ranges] = line.getLineInfo(getLinkMask)
1345    msg = "BRAILLE: Line %i: '%s' focusOffset: %i %s" % (viewport[1], string, focusOffset, ranges)
1346    debug.println(debug.LEVEL_INFO, msg, True)
1347
1348    cursorOffset = -1
1349    if focusOffset >= 0:
1350        cursorOffset = focusOffset + _regionWithFocus.cursorOffset
1351        msg = "BRAILLE: Cursor offset in line string is: %i" % cursorOffset
1352        debug.println(debug.LEVEL_INFO, msg, True)
1353
1354    # Now, if desired, we'll automatically pan the viewport to show
1355    # the cursor.  If there's no targetCursorCell, then we favor the
1356    # left of the display if we need to pan left, or we favor the
1357    # right of the display if we need to pan right.
1358    #
1359    if panToCursor and (cursorOffset >= 0):
1360        if len(string) <= _displaySize[0] and cursorOffset < _displaySize[0]:
1361            msg = "BRAILLE: Not adjusting offset %i. Cursor offset fits on display." % viewport[0]
1362            debug.println(debug.LEVEL_INFO, msg, True)
1363        elif targetCursorCell:
1364            viewport[0] = max(0, cursorOffset - targetCursorCell + 1)
1365            msg = "BRAILLE: Adjusting offset to %i based on targetCursorCell" % viewport[0]
1366            debug.println(debug.LEVEL_INFO, msg, True)
1367        elif cursorOffset < viewport[0]:
1368            viewport[0] = max(0, cursorOffset)
1369            msg = "BRAILLE: Adjusting offset to %i (cursor on left)" % viewport[0]
1370            debug.println(debug.LEVEL_INFO, msg, True)
1371        elif cursorOffset >= (viewport[0] + _displaySize[0]):
1372            viewport[0] = max(0, cursorOffset - _displaySize[0] + 1)
1373            msg = "BRAILLE: Adjusting offset to %i (cursor beyond display end)" % viewport[0]
1374            debug.println(debug.LEVEL_INFO, msg, True)
1375        else:
1376            rangeForOffset = _getRangeForOffset(cursorOffset)
1377            viewport[0] = max(0, rangeForOffset[0])
1378            msg = "BRAILLE: Adjusting offset to %i (unhandled condition)" % viewport[0]
1379            debug.println(debug.LEVEL_INFO, msg, True)
1380            if cursorOffset >= (viewport[0] + _displaySize[0]):
1381                viewport[0] = max(0, cursorOffset - _displaySize[0] + 1)
1382                msg = "BRAILLE: Readjusting offset to %i (cursor beyond display end)" % viewport[0]
1383                debug.println(debug.LEVEL_INFO, msg, True)
1384
1385    startPos, endPos = _adjustForWordWrap(targetCursorCell)
1386    viewport[0] = startPos
1387
1388    # Now normalize the cursor position to BrlTTY, which uses 1 as
1389    # the first cursor position as opposed to 0.
1390    #
1391    cursorCell = cursorOffset - startPos
1392    if (cursorCell < 0) or (cursorCell >= _displaySize[0]):
1393        cursorCell = 0
1394    else:
1395        cursorCell += 1 # Normalize to 1-based offset
1396
1397    logLine = "BRAILLE LINE:  '%s'" % string
1398    debug.println(debug.LEVEL_INFO, logLine, True)
1399    log.info(logLine)
1400
1401    logLine = "     VISIBLE:  '%s', cursor=%d" % \
1402                    (string[startPos:endPos], cursorCell)
1403    debug.println(debug.LEVEL_INFO, logLine, True)
1404    log.info(logLine)
1405
1406    substring = string[startPos:endPos]
1407    if attributeMask:
1408        submask = attributeMask[startPos:endPos]
1409    else:
1410        submask = ""
1411
1412    submask += '\x00' * (len(substring) - len(submask))
1413
1414    if _settingsManager.getSetting('enableBraille'):
1415        _enableBraille()
1416
1417    if _settingsManager.getSetting('enableBraille') and _brlAPIRunning:
1418        writeStruct = brlapi.WriteStruct()
1419        writeStruct.regionBegin = 1
1420        writeStruct.regionSize = len(substring)
1421        while writeStruct.regionSize < _displaySize[0]:
1422            substring += " "
1423            if attributeMask:
1424                submask += '\x00'
1425            writeStruct.regionSize += 1
1426        writeStruct.text = substring
1427        writeStruct.cursor = cursorCell
1428
1429        # [[[WDW - if you want to muck around with the dots on the
1430        # display to do things such as add underlines, you can use
1431        # the attrOr field of the write structure to do so.  The
1432        # attrOr field is a string whose length must be the same
1433        # length as the display and whose dots will end up showing
1434        # up on the display.  Each character represents a bitfield
1435        # where each bit corresponds to a dot (i.e., bit 0 = dot 1,
1436        # bit 1 = dot 2, and so on).  Here's an example that underlines
1437        # all the text.]]]
1438        #
1439        #myUnderline = ""
1440        #for i in range(0, _displaySize[0]):
1441        #    myUnderline += '\xc0'
1442        #writeStruct.attrOr = myUnderline
1443
1444        if attributeMask:
1445            writeStruct.attrOr = submask
1446
1447        try:
1448            _brlAPI.write(writeStruct)
1449        except:
1450            msg = "BRAILLE: BrlTTY seems to have disappeared."
1451            debug.println(debug.LEVEL_WARNING, msg, True)
1452            shutdown()
1453
1454    if settings.enableBrailleMonitor:
1455        if not _monitor:
1456            try:
1457                _monitor = brlmon.BrlMon(_displaySize[0])
1458                _monitor.show_all()
1459            except:
1460                debug.println(debug.LEVEL_WARNING, "brlmon failed")
1461                _monitor = None
1462        if attributeMask:
1463            subMask = attributeMask[startPos:endPos]
1464        else:
1465            subMask = None
1466        if _monitor:
1467            _monitor.writeText(cursorCell, substring, subMask)
1468    elif _monitor:
1469        _monitor.destroy()
1470        _monitor = None
1471
1472    beginningIsShowing = startPos == 0
1473    endIsShowing = endPos >= len(string)
1474
1475    # Remember the text information we were presenting (if any)
1476    #
1477    if _regionWithFocus and isinstance(_regionWithFocus, Text):
1478        _lastTextInfo = (_regionWithFocus.accessible,
1479                         _regionWithFocus.caretOffset,
1480                         _regionWithFocus.lineOffset,
1481                         cursorCell)
1482    else:
1483        _lastTextInfo = (None, 0, 0, 0)
1484
1485def _flashCallback():
1486    global _lines
1487    global _regionWithFocus
1488    global viewport
1489    global _flashEventSourceId
1490
1491    if _flashEventSourceId:
1492        (_lines, _regionWithFocus, viewport, flashTime) = _saved
1493        refresh(panToCursor=False, stopFlash=False)
1494        _flashEventSourceId = 0
1495
1496    return False
1497
1498def killFlash(restoreSaved=True):
1499    global _flashEventSourceId
1500    global _lines
1501    global _regionWithFocus
1502    global viewport
1503    if _flashEventSourceId:
1504        if _flashEventSourceId > 0:
1505            GLib.source_remove(_flashEventSourceId)
1506        if restoreSaved:
1507            (_lines, _regionWithFocus, viewport, flashTime) = _saved
1508            refresh(panToCursor=False, stopFlash=False)
1509        _flashEventSourceId = 0
1510
1511def resetFlashTimer():
1512    global _flashEventSourceId
1513    if _flashEventSourceId > 0:
1514        GLib.source_remove(_flashEventSourceId)
1515        flashTime = _saved[3]
1516        _flashEventSourceId = GLib.timeout_add(flashTime, _flashCallback)
1517
1518def _initFlash(flashTime):
1519    """Sets up the state needed to flash a message or clears any existing
1520    flash if nothing is to be flashed.
1521
1522    Arguments:
1523    - flashTime:  if non-0, the number of milliseconds to display the
1524                  regions before reverting back to what was there before.
1525                  A 0 means to not do any flashing.  A negative number
1526                  means display the message until some other message
1527                  comes along or the user presses a cursor routing key.
1528    """
1529
1530    global _saved
1531    global _flashEventSourceId
1532
1533    if _flashEventSourceId:
1534        if _flashEventSourceId > 0:
1535            GLib.source_remove(_flashEventSourceId)
1536        _flashEventSourceId = 0
1537    else:
1538        _saved = (_lines, _regionWithFocus, viewport, flashTime)
1539
1540    if flashTime > 0:
1541        _flashEventSourceId = GLib.timeout_add(flashTime, _flashCallback)
1542    elif flashTime < 0:
1543        _flashEventSourceId = -666
1544
1545def displayRegions(regionInfo, flashTime=0):
1546    """Displays a list of regions on a single line, setting focus to the
1547       specified region.  The regionInfo parameter is something that is
1548       typically returned by a call to braille_generator.generateBraille.
1549
1550    Arguments:
1551    - regionInfo: a list where the first element is a list of regions
1552                  to display and the second element is the region
1553                  with focus (must be in the list from element 0)
1554    - flashTime:  if non-0, the number of milliseconds to display the
1555                  regions before reverting back to what was there before.
1556                  A 0 means to not do any flashing.  A negative number
1557                  means display the message until some other message
1558                  comes along or the user presses a cursor routing key.
1559    """
1560
1561    _initFlash(flashTime)
1562    regions = regionInfo[0]
1563    focusedRegion = regionInfo[1]
1564
1565    clear()
1566    line = Line()
1567    for item in regions:
1568        line.addRegion(item)
1569    addLine(line)
1570    setFocus(focusedRegion)
1571    refresh(stopFlash=False)
1572
1573def displayMessage(message, cursor=-1, flashTime=0):
1574    """Displays a single line, setting the cursor to the given position,
1575    ensuring that the cursor is in view.
1576
1577    Arguments:
1578    - message: the string to display
1579    - cursor: the 0-based cursor position, where -1 (default) means no cursor
1580    - flashTime:  if non-0, the number of milliseconds to display the
1581                  regions before reverting back to what was there before.
1582                  A 0 means to not do any flashing.  A negative number
1583                  means display the message until some other message
1584                  comes along or the user presses a cursor routing key.
1585    """
1586
1587    _initFlash(flashTime)
1588    clear()
1589    region = Region(message, cursor)
1590    addLine(Line(region))
1591    setFocus(region)
1592    refresh(True, stopFlash=False)
1593
1594def displayKeyEvent(event):
1595    """Displays a KeyboardEvent. Typically reserved for locking keys like
1596    Caps Lock and Num Lock."""
1597
1598    lockingStateString = event.getLockingStateString()
1599    if lockingStateString:
1600        keyname = event.getKeyName()
1601        msg = "%s %s" % (keyname, lockingStateString)
1602        displayMessage(msg, flashTime=settings.brailleFlashTime)
1603
1604def _adjustForWordWrap(targetCursorCell):
1605    startPos = viewport[0]
1606    endPos = startPos + _displaySize[0]
1607    msg = "BRAILLE: Current range: (%i, %i). Target cell: %i." % (startPos, endPos, targetCursorCell)
1608    debug.println(debug.LEVEL_INFO, msg, True)
1609
1610    if not _lines or not settings.enableBrailleWordWrap:
1611        return startPos, endPos
1612
1613    line = _lines[viewport[1]]
1614    lineString, focusOffset, attributeMask, ranges = line.getLineInfo()
1615    ranges = list(filter(lambda x: x[0] <= startPos + targetCursorCell < x[1], ranges))
1616    if ranges:
1617        msg = "BRAILLE: Adjusted range: (%i, %i)" % (ranges[0][0], ranges[-1][1])
1618        debug.println(debug.LEVEL_INFO, msg, True)
1619        if ranges[-1][1] - ranges[0][0] > _displaySize[0]:
1620            msg = "BRAILLE: Not adjusting range which is greater than display size"
1621            debug.println(debug.LEVEL_INFO, msg, True)
1622        else:
1623            startPos, endPos = ranges[0][0], ranges[-1][1]
1624
1625    return startPos, endPos
1626
1627def _getRangeForOffset(offset):
1628    string, focusOffset, attributeMask, ranges = _lines[viewport[1]].getLineInfo()
1629    for r in ranges:
1630        if r[0] <= offset < r[1]:
1631            return r
1632    for r in ranges:
1633        if offset == r[1]:
1634            return r
1635
1636    return [0, 0]
1637
1638def panLeft(panAmount=0):
1639    """Pans the display to the left, limiting the pan to the beginning
1640    of the line being displayed.
1641
1642    Arguments:
1643    - panAmount: the amount to pan.  A value of 0 means the entire
1644                 width of the physical display.
1645
1646    Returns True if a pan actually happened.
1647    """
1648
1649    oldX = viewport[0]
1650    if panAmount == 0:
1651        oldStart, oldEnd = _getRangeForOffset(oldX)
1652        newStart, newEnd = _getRangeForOffset(oldStart - _displaySize[0])
1653        panAmount = max(0, min(oldStart - newStart, _displaySize[0]))
1654
1655    viewport[0] = max(0, viewport[0] - panAmount)
1656    msg = "BRAILLE: Panning left. Amount: %i (from %i to %i)" % (panAmount, oldX, viewport[0])
1657    debug.println(debug.LEVEL_INFO, msg, True)
1658    return oldX != viewport[0]
1659
1660def panRight(panAmount=0):
1661    """Pans the display to the right, limiting the pan to the length
1662    of the line being displayed.
1663
1664    Arguments:
1665    - panAmount: the amount to pan.  A value of 0 means the entire
1666                 width of the physical display.
1667
1668    Returns True if a pan actually happened.
1669    """
1670
1671    oldX = viewport[0]
1672    if panAmount == 0:
1673        oldStart, oldEnd = _getRangeForOffset(oldX)
1674        newStart, newEnd = _getRangeForOffset(oldEnd)
1675        panAmount = max(0, min(newStart - oldStart, _displaySize[0]))
1676
1677    if len(_lines) > 0:
1678        lineNum = viewport[1]
1679        newX = viewport[0] + panAmount
1680        string, focusOffset, attributeMask, ranges = _lines[lineNum].getLineInfo()
1681        if newX < len(string):
1682            viewport[0] = newX
1683
1684    msg = "BRAILLE: Panning right. Amount: %i (from %i to %i)" % (panAmount, oldX, viewport[0])
1685    debug.println(debug.LEVEL_INFO, msg, True)
1686    return oldX != viewport[0]
1687
1688def panToOffset(offset):
1689    """Automatically pan left or right to make sure the current offset is
1690    showing."""
1691
1692    msg = "BRAILLE: Panning to offset %i. Current offset: %i." % (offset, viewport[0])
1693    debug.println(debug.LEVEL_INFO, msg, True)
1694
1695    while offset < viewport[0]:
1696        if not panLeft():
1697            break
1698
1699    while offset >= (viewport[0] + _displaySize[0]):
1700        if not panRight():
1701            break
1702
1703def returnToRegionWithFocus(inputEvent=None):
1704    """Pans the display so the region with focus is displayed.
1705
1706    Arguments:
1707    - inputEvent: the InputEvent instance that caused this to be called.
1708
1709    Returns True to mean the command should be consumed.
1710    """
1711
1712    setFocus(_regionWithFocus)
1713    refresh(True)
1714
1715    return True
1716
1717def setContractedBraille(event):
1718    """Turns contracted braille on or off based upon the event.
1719
1720    Arguments:
1721    - event: an instance of input_event.BrailleEvent.  event.event is
1722    the dictionary form of the expanded BrlAPI event.
1723    """
1724
1725    settings.enableContractedBraille = \
1726        (event.event["flags"] & brlapi.KEY_FLG_TOGGLE_ON) != 0
1727    for line in _lines:
1728        line.setContractedBraille(settings.enableContractedBraille)
1729    refresh()
1730
1731def processRoutingKey(event):
1732    """Processes a cursor routing key event.
1733
1734    Arguments:
1735    - event: an instance of input_event.BrailleEvent.  event.event is
1736    the dictionary form of the expanded BrlAPI event.
1737    """
1738
1739    # If a message is being flashed, we'll use a routing key to dismiss it.
1740    #
1741    if _flashEventSourceId:
1742        killFlash()
1743        return
1744
1745    cell = event.event["argument"]
1746
1747    if len(_lines) > 0:
1748        cursor = cell + viewport[0]
1749        lineNum = viewport[1]
1750        _lines[lineNum].processRoutingKey(cursor)
1751
1752    return True
1753
1754def _processBrailleEvent(event):
1755    """Handles BrlTTY command events.  This passes commands on to Orca for
1756    processing.
1757
1758    Arguments:
1759    - event: the BrlAPI input event (expanded)
1760    """
1761
1762    _printBrailleEvent(debug.LEVEL_FINE, event)
1763
1764    consumed = False
1765
1766    if settings.timeoutCallback and (settings.timeoutTime > 0):
1767        signal.signal(signal.SIGALRM, settings.timeoutCallback)
1768        signal.alarm(settings.timeoutTime)
1769
1770    if _callback:
1771        try:
1772            # Like key event handlers, a return value of True means
1773            # the command was consumed.
1774            #
1775            consumed = _callback(event)
1776        except:
1777            debug.println(debug.LEVEL_WARNING, "Issue processing event:")
1778            debug.printException(debug.LEVEL_WARNING)
1779            consumed = False
1780
1781    if settings.timeoutCallback and (settings.timeoutTime > 0):
1782        signal.alarm(0)
1783
1784    return consumed
1785
1786def _brlAPIKeyReader(source, condition):
1787    """Method to read a key from the BrlAPI bindings.  This is a
1788    gobject IO watch handler.
1789    """
1790    try:
1791        key = _brlAPI.readKey(False)
1792    except:
1793        debug.println(debug.LEVEL_WARNING, "BrlTTY seems to have disappeared:")
1794        debug.printException(debug.LEVEL_WARNING)
1795        shutdown()
1796        return
1797    if key:
1798        _processBrailleEvent(_brlAPI.expandKeyCode(key))
1799    return _brlAPIRunning
1800
1801def setupKeyRanges(keys):
1802    """Hacky method to tell BrlTTY what to send and not send us via
1803    the readKey method.  This only works with BrlTTY v3.8 and better.
1804
1805    Arguments:
1806    -keys: a list of BrlAPI commands.
1807    """
1808
1809    msg = "BRAILLE: Setting up key ranges."
1810    debug.println(debug.LEVEL_INFO, msg, True)
1811
1812    if not _brlAPIRunning:
1813        init(_callback)
1814
1815    if not _brlAPIRunning:
1816        msg = "BRAILLE: Not setting up key ranges: BrlAPI not running."
1817        debug.println(debug.LEVEL_INFO, msg, True)
1818        return
1819
1820    msg = "BRAILLE: Ignoring all key ranges."
1821    debug.println(debug.LEVEL_INFO, msg, True)
1822    _brlAPI.ignoreKeys(brlapi.rangeType_all, [0])
1823
1824    keySet = [brlapi.KEY_TYPE_CMD | brlapi.KEY_CMD_ROUTE]
1825
1826    msg = "BRAILLE: Enabling commands:"
1827    debug.println(debug.LEVEL_INFO, msg, True)
1828
1829    for key in keys:
1830        keySet.append(brlapi.KEY_TYPE_CMD | key)
1831
1832    msg = "BRAILLE: Sending keys to BrlAPI."
1833    debug.println(debug.LEVEL_INFO, msg, True)
1834    _brlAPI.acceptKeys(brlapi.rangeType_command, keySet)
1835
1836    msg = "BRAILLE: Key ranges set up."
1837    debug.println(debug.LEVEL_INFO, msg, True)
1838
1839def init(callback=None):
1840    """Initializes the braille module, connecting to the BrlTTY driver.
1841
1842    Arguments:
1843    - callback: the method to call with a BrlTTY input event.
1844    Returns False if BrlTTY cannot be accessed or braille has
1845    not been enabled.
1846    """
1847
1848    if not settings.enableBraille:
1849        return False
1850
1851    global _brlAPI
1852    global _brlAPIRunning
1853    global _brlAPISourceId
1854    global _displaySize
1855    global _callback
1856    global _monitor
1857
1858    msg = "BRAILLE: Initializing. Callback: %s" % callback
1859    debug.println(debug.LEVEL_INFO, msg, True)
1860
1861    if _brlAPIRunning:
1862        msg = "BRAILLE: BrlAPI is already running."
1863        debug.println(debug.LEVEL_INFO, msg, True)
1864        return True
1865
1866    _callback = callback
1867
1868    msg = "BRAILLE: WINDOWPATH=%s" % os.environ.get("WINDOWPATH")
1869    debug.println(debug.LEVEL_INFO, msg, True)
1870
1871    msg = "BRAILLE: XDG_VTNR=%s" % os.environ.get("XDG_VTNR")
1872    debug.println(debug.LEVEL_INFO, msg, True)
1873
1874    try:
1875        msg = "BRAILLE: Attempting connection with BrlAPI."
1876        debug.println(debug.LEVEL_INFO, msg, True)
1877
1878        _brlAPI = brlapi.Connection()
1879        msg = "BRAILLE: Connection established with BrlAPI: %s" % _brlAPI
1880        debug.println(debug.LEVEL_INFO, msg, True)
1881
1882        msg = "BRAILLE: Attempting to enter TTY mode."
1883        debug.println(debug.LEVEL_INFO, msg, True)
1884
1885        _brlAPI.enterTtyModeWithPath()
1886        msg = "BRAILLE: TTY mode entered."
1887        debug.println(debug.LEVEL_INFO, msg, True)
1888
1889        _brlAPIRunning = True
1890
1891        (x, y) = _brlAPI.displaySize
1892        msg = "BRAILLE: Display size: (%i,%i)" % (x, y)
1893        debug.println(debug.LEVEL_INFO, msg, True)
1894
1895        if x == 0:
1896            msg = "BRAILLE: Error - 0 cells suggests display is not yet plugged in."
1897            debug.println(debug.LEVEL_INFO, msg, True)
1898            raise Exception
1899
1900        _brlAPISourceId = GLib.io_add_watch(_brlAPI.fileDescriptor,
1901                                            GLib.PRIORITY_DEFAULT,
1902                                            GLib.IO_IN,
1903                                            _brlAPIKeyReader)
1904
1905    except NameError:
1906        msg = "BRAILLE: Initialization failed: BrlApi is not defined."
1907        debug.println(debug.LEVEL_INFO, msg, True)
1908        return False
1909    except:
1910        msg = "BRAILLE: Initialization failed."
1911        debug.println(debug.LEVEL_INFO, msg, True)
1912        debug.printException(debug.LEVEL_INFO)
1913
1914        _brlAPIRunning = False
1915
1916        if not _brlAPI:
1917            return False
1918
1919        try:
1920            msg = "BRAILLE: Attempting to leave TTY mode."
1921            debug.println(debug.LEVEL_INFO, msg, True)
1922            _brlAPI.leaveTtyMode()
1923            msg = "BRAILLE: TTY mode exited."
1924            debug.println(debug.LEVEL_INFO, msg, True)
1925        except:
1926            msg = "BRAILLE: Exception leaving TTY mode."
1927            debug.println(debug.LEVEL_INFO, msg, True)
1928
1929        try:
1930            msg = "BRAILLE: Attempting to close connection."
1931            debug.println(debug.LEVEL_INFO, msg, True)
1932            _brlAPI.closeConnection()
1933            msg = "BRAILLE: Connection closed."
1934            debug.println(debug.LEVEL_INFO, msg, True)
1935        except:
1936            msg = "BRAILLE: Exception closing connection."
1937            debug.println(debug.LEVEL_INFO, msg, True)
1938
1939        _brlAPI = None
1940        return False
1941
1942    _displaySize = [x, 1]
1943    idle = False
1944
1945    # The monitor will be created in refresh if needed.
1946    if _monitor:
1947        _monitor.destroy()
1948        _monitor = None
1949
1950    clear()
1951    refresh(True)
1952
1953    msg = "BRAILLE: Initialized"
1954    debug.println(debug.LEVEL_INFO, msg, True)
1955    return True
1956
1957def shutdown():
1958    """Shuts down the braille module.   Returns True if the shutdown procedure
1959    was run.
1960    """
1961
1962    msg = "BRAILLE: Attempting braille shutdown."
1963    debug.println(debug.LEVEL_INFO, msg, True)
1964
1965    global _brlAPI
1966    global _brlAPIRunning
1967    global _brlAPISourceId
1968    global _monitor
1969    global _displaySize
1970
1971    if _brlAPIRunning:
1972        _brlAPIRunning = False
1973
1974        msg = "BRAILLE: Removing BrlAPI Source ID."
1975        debug.println(debug.LEVEL_INFO, msg, True)
1976
1977        GLib.source_remove(_brlAPISourceId)
1978        _brlAPISourceId = 0
1979
1980        try:
1981            msg = "BRAILLE: Attempting to leave TTY mode."
1982            debug.println(debug.LEVEL_INFO, msg, True)
1983            _brlAPI.leaveTtyMode()
1984        except:
1985            msg = "BRAILLE: Exception leaving TTY mode."
1986            debug.println(debug.LEVEL_INFO, msg, True)
1987        else:
1988            msg = "BRAILLE: Leaving TTY mode succeeded."
1989            debug.println(debug.LEVEL_INFO, msg, True)
1990
1991        try:
1992            msg = "BRAILLE: Attempting to close connection."
1993            debug.println(debug.LEVEL_INFO, msg, True)
1994            _brlAPI.closeConnection()
1995        except:
1996            msg = "BRAILLE: Exception closing connection."
1997            debug.println(debug.LEVEL_INFO, msg, True)
1998        else:
1999            msg = "BRAILLE: Closing connection succeeded."
2000            debug.println(debug.LEVEL_INFO, msg, True)
2001
2002        _brlAPI = None
2003
2004        if _monitor:
2005            _monitor.destroy()
2006            _monitor = None
2007        _displaySize = [DEFAULT_DISPLAY_SIZE, 1]
2008    else:
2009        msg = "BRAILLE: Braille was not running."
2010        debug.println(debug.LEVEL_INFO, msg, True)
2011        return False
2012
2013    msg = "BRAILLE: Braille shutdown complete."
2014    debug.println(debug.LEVEL_INFO, msg, True)
2015    return True
2016