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