Coverage Report - orca.braille

ModuleCoverage %
orca.braille
52%
1
# Orca
2
#
3
# Copyright 2005-2007 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 Library General Public
7
# License as published by the Free Software Foundation; either
8
# version 2 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
# Library General Public License for more details.
14
#
15
# You should have received a copy of the GNU Library General Public
16
# License along with this library; if not, write to the
17
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18
# Boston, MA 02111-1307, USA.
19
20
"""A very experimental approach to the refreshable Braille display.  This
21
module treats each line of the display as a sequential set of regions, where
22
each region can potentially backed by an Accessible object.  Depending upon
23
the Accessible object, the cursor routing keys can be used to perform
24
operations on the Accessible object, such as invoking default actions or
25
moving the text caret.
26 1
"""
27
28 1
__id__        = "$Id: braille.py 2602 2007-08-08 17:15:07Z shaeger $"
29 1
__version__   = "$Revision: 2602 $"
30 1
__date__      = "$Date: 2007-08-08 13:15:07 -0400 (Wed, 08 Aug 2007) $"
31 1
__copyright__ = "Copyright (c) 2005-2007 Sun Microsystems Inc."
32 1
__license__   = "LGPL"
33
34 1
import logging
35 1
log = logging.getLogger("braille")
36
37 1
import signal
38
39 1
import atspi
40
41
# We'll use the official BrlAPI pythons (as of BrlTTY 3.8) if they
42
# are available.  Otherwise, we'll fall back to our own bindings.
43
#
44 1
try:
45 1
    import brlapi
46 0
    import gobject
47
48 0
    brlAPI = None
49 0
    useBrlAPIBindings = True
50 0
    brlAPIRunning = False
51 0
    brlAPISourceId = 0
52 1
except:
53 1
    import brl
54 1
    useBrlAPIBindings = False
55 1
    brlAPIRunning = False
56
57 1
try:
58
    # This can fail due to gtk not being available.  We want to
59
    # be able to recover from that if possible.  The main driver
60
    # for this is to allow "orca --text-setup" to work even if
61
    # the desktop is not running.
62
    #
63 1
    import brlmon
64 0
except:
65 0
    pass
66 1
import debug
67 1
import eventsynthesizer
68 1
import orca_state
69 1
import settings
70
71 1
from orca_i18n import _                          # for gettext support
72 1
from rolenames import getShortBrailleForRoleName # localized role names
73 1
from rolenames import getLongBrailleForRoleName  # localized role names
74
75
# If True, this module has been initialized.
76
#
77 1
_initialized = False
78
79
# The braille monitor
80
#
81 1
monitor = None
82
83
# Each of these maps to BrlAPI's brldefs.h file.
84
#
85 1
CMD_NOOP              = 0x00
86 1
CMD_LNUP              = 0x01
87 1
CMD_LNDN              = 0x02
88 1
CMD_WINUP             = 0x03
89 1
CMD_WINDN             = 0x04
90 1
CMD_PRDIFLN           = 0x05
91 1
CMD_NXDIFLN           = 0x06
92 1
CMD_ATTRUP            = 0x07
93 1
CMD_ATTRDN            = 0x08
94 1
CMD_TOP               = 0x09
95 1
CMD_BOT               = 0x0a
96 1
CMD_TOP_LEFT          = 0x0b
97 1
CMD_BOT_LEFT          = 0x0c
98 1
CMD_PRPGRPH           = 0x0d
99 1
CMD_NXPGRPH           = 0x0e
100 1
CMD_PRPROMPT          = 0x0f
101 1
CMD_NXPROMPT          = 0x10
102 1
CMD_PRSEARCH          = 0x11
103 1
CMD_NXSEARCH          = 0x12
104 1
CMD_CHRLT             = 0x13
105 1
CMD_CHRRT             = 0x14
106 1
CMD_HWINLT            = 0x15
107 1
CMD_HWINRT            = 0x16
108 1
CMD_FWINLT            = 0x17
109 1
CMD_FWINRT            = 0x18
110 1
CMD_FWINLTSKIP        = 0x19
111 1
CMD_FWINRTSKIP        = 0x1a
112 1
CMD_LNBEG             = 0x1b
113 1
CMD_LNEND             = 0x1c
114 1
CMD_HOME              = 0x1d
115 1
CMD_BACK              = 0x1e
116 1
CMD_FREEZE            = 0x1f
117 1
CMD_DISPMD            = 0x20
118 1
CMD_SIXDOTS           = 0x21
119 1
CMD_SLIDEWIN          = 0x22
120 1
CMD_SKPIDLNS          = 0x23
121 1
CMD_SKPBLNKWINS       = 0x24
122 1
CMD_CSRVIS            = 0x25
123 1
CMD_CSRHIDE           = 0x26
124 1
CMD_CSRTRK            = 0x27
125 1
CMD_CSRSIZE           = 0x28
126 1
CMD_CSRBLINK          = 0x29
127 1
CMD_ATTRVIS           = 0x2a
128 1
CMD_ATTRBLINK         = 0x2b
129 1
CMD_CAPBLINK          = 0x2c
130 1
CMD_TUNES             = 0x2d
131 1
CMD_HELP              = 0x2e
132 1
CMD_INFO              = 0x2f
133 1
CMD_LEARN             = 0x30
134 1
CMD_PREFMENU          = 0x31
135 1
CMD_PREFSAVE          = 0x32
136 1
CMD_PREFLOAD          = 0x33
137 1
CMD_MENU_FIRST_ITEM   = 0x34
138 1
CMD_MENU_LAST_ITEM    = 0x35
139 1
CMD_MENU_PREV_ITEM    = 0x36
140 1
CMD_MENU_NEXT_ITEM    = 0x37
141 1
CMD_MENU_PREV_SETTING = 0x38
142 1
CMD_MENU_NEXT_SETTING = 0x39
143 1
CMD_SAY_LINE          = 0x3a
144 1
CMD_SAY_ABOVE         = 0x3b
145 1
CMD_SAY_BELOW         = 0x3c
146 1
CMD_MUTE              = 0x3d
147 1
CMD_SPKHOME           = 0x3e
148 1
CMD_SWITCHVT_PREV     = 0x3f
149 1
CMD_SWITCHVT_NEXT     = 0x40
150 1
CMD_CSRJMP_VERT       = 0x41
151 1
CMD_PASTE             = 0x42
152 1
CMD_RESTARTBRL        = 0x43
153 1
CMD_RESTARTSPEECH     = 0x44
154 1
CMD_MAX               = 0x44
155
156 1
BRL_FLG_REPEAT_INITIAL= 0x800000
157 1
BRL_FLG_REPEAT_DELAY  = 0x400000
158
159
# Common names for most used BrlTTY commands, to be shown in the GUI:
160
# ATM, the ones used in default.py are:
161
#
162 1
command_name = {}
163
164
# Translators: this is a command for a button on a refreshable braille
165
# display (an external hardware device used by people who are blind).
166
# When pressing the button, the display scrolls to the left.
167
#
168 1
command_name[CMD_FWINLT]   = _("Line Left")
169
170
# Translators: this is a command for a button on a refreshable braille
171
# display (an external hardware device used by people who are blind).
172
# When pressing the button, the display scrolls to the right.
173
#
174 1
command_name[CMD_FWINRT]   = _("Line Right")
175
176
# Translators: this is a command for a button on a refreshable braille
177
# display (an external hardware device used by people who are blind).
178
# When pressing the button, the display scrolls up.
179
#
180 1
command_name[CMD_LNUP]     = _("Line Up")
181
182
# Translators: this is a command for a button on a refreshable braille
183
# display (an external hardware device used by people who are blind).
184
# When pressing the button, the display scrolls down.
185
#
186 1
command_name[CMD_LNDN]     = _("Line Down")
187
188
# Translators: this is a command for a button on a refreshable braille
189
# display (an external hardware device used by people who are blind).
190
# When pressing the button, the display scrolls to the top left of the
191
# window.
192
#
193 1
command_name[CMD_TOP_LEFT] = _("Top Left")
194
195
# Translators: this is a command for a button on a refreshable braille
196
# display (an external hardware device used by people who are blind).
197
# When pressing the button, the display scrolls to the bottom right of
198
# the window.
199
#
200 1
command_name[CMD_BOT_LEFT] = _("Bottom Right")
201
202
# Translators: this is a command for a button on a refreshable braille
203
# display (an external hardware device used by people who are blind).
204
# When pressing the button, the display scrolls to position containing
205
# the cursor.
206
#
207 1
command_name[CMD_HOME]     = _("Cursor Position")
208
209
# The size of the physical display (width, height).  The coordinate system of
210
# the display is set such that the upper left is (0,0), x values increase from
211
# left to right, and y values increase from top to bottom.
212
#
213
# For the purposes of testing w/o a braille display, we'll set the display
214
# size to width=32 and height=1.
215
#
216
# [[[TODO: WDW - Only a height of 1 is support at this time.]]]
217
#
218 1
_displaySize = [32, 1]
219
220
# The list of lines on the display.  This represents the entire amount of data
221
# to be drawn on the display.  It will be clipped by the viewport if too large.
222
#
223 1
_lines = []
224
225
# The region with focus.  This will be displayed at the home position.
226
#
227 1
_regionWithFocus = None
228
229
# The viewport is a rectangular region of size _displaySize whose upper left
230
# corner is defined by the point (x, line number).  As such, the viewport is
231
# identified solely by its upper left point.
232
#
233 1
_viewport = [0, 0]
234
235
# The callback to call on a BrlTTY input event.  This is passed to
236
# the init method.
237
#
238 1
_callback = None
239
240
# If True, the given portion of the currently displayed line is showing
241
# on the display.
242
#
243 1
endIsShowing = False
244 1
beginningIsShowing = False
245
246
# 1-based offset saying which braille cell has the cursor.  A value
247
# of 0 means no cell has the cursor.
248
#
249 1
cursorCell = 0
250
251 1
def _printBrailleEvent(level, command):
252
    """Prints out a Braille event.  The given level may be overridden
253
    if the eventDebugLevel (see debug.setEventDebugLevel) is greater in
254
    debug.py.
255
256
    Arguments:
257
    - command: the BrlAPI command for the key that was pressed.
258
    """
259
260 0
    debug.printInputEvent(
261 0
        level,
262 0
        "BRAILLE EVENT: %x" % command)
263
264 2
class Region:
265
    """A Braille region to be displayed on the display.  The width of
266
    each region is determined by its string.
267 1
    """
268
269 1
    def __init__(self, string, cursorOffset=0):
270
        """Creates a new Region containing the given string.
271
272
        Arguments:
273
        - string: the string to be displayed
274
        - cursorOffset: a 0-based index saying where to draw the cursor
275
                        for this Region if it gets focus.
276
        """
277
278 11969
        if not string:
279 473
            string = ""
280
281 11969
        string = string.decode("UTF-8")
282 11969
        if string[-1:] == "\n":
283 0
            string = string[:-1]
284 11969
        self.string = string.encode("UTF-8")
285
286 11969
        self.cursorOffset = cursorOffset
287
288 1
    def processRoutingKey(self, offset):
289
        """Processes a cursor routing key press on this Component.  The offset
290
        is 0-based, where 0 represents the leftmost character of string
291
        associated with this region.  Note that the zeroeth character may have
292
        been scrolled off the display."""
293 0
        pass
294
295 1
    def getAttributeMask(self):
296
        """Creates a string which can be used as the attrOr field of brltty's
297
        write structure for the purpose of indicating text attributes and
298
        selection."""
299
300
        # Create an empty mask.
301
        #
302 24410
        mask = ['\x00'] * len(self.string)
303 24410
        return "".join(mask)
304
305 2
class Component(Region):
306
    """A subclass of Region backed by an accessible.  This Region will react
307
    to any cursor routing key events and perform the default action on the
308
    accessible, if a default action exists.
309 1
    """
310
311 1
    def __init__(self, accessible, string, cursorOffset=0):
312
        """Creates a new Component.
313
314
        Arguments:
315
        - accessible: the accessible
316
        - string: the string to use to represent the component
317
        - cursorOffset: a 0-based index saying where to draw the cursor
318
                        for this Region if it gets focus.
319
        """
320
321 5446
        Region.__init__(self, string, cursorOffset)
322 5446
        self.accessible = accessible
323
324 1
    def processRoutingKey(self, offset):
325
        """Processes a cursor routing key press on this Component.  The offset
326
        is 0-based, where 0 represents the leftmost character of string
327
        associated with this region.  Note that the zeroeth character may have
328
        been scrolled off the display."""
329
330 0
        actions = self.accessible.action
331 0
        if actions:
332 0
            actions.doAction(0)
333
        else:
334
335
            # [[[WDW - HACK to do a mouse button 1 click if we have
336
            # to.  For example, page tabs don't have any actions but
337
            # we want to be able to select them with the cursor
338
            # routing key.]]]
339
            #
340 0
            debug.println(debug.LEVEL_FINEST,
341 0
                          "braille.Component.processRoutingKey: no action")
342 0
            try:
343 0
                eventsynthesizer.clickObject(self.accessible, 1)
344 0
            except:
345 0
                debug.printException(debug.LEVEL_SEVERE)
346
347 2
class Text(Region):
348
    """A subclass of Region backed by a Text object.  This Region will
349
    react to any cursor routing key events by positioning the caret in
350
    the associated text object. The line displayed will be the
351
    contents of the text object preceded by an optional label.
352
    [[[TODO: WDW - need to add in text selection capabilities.  Logged
353 1
    as bugzilla bug 319754.]]]"""
354
355 1
    def __init__(self, accessible, label=None, eol=""):
356
        """Creates a new Text region.
357
358
        Arguments:
359
        - accessible: the accessible that implements AccessibleText
360
        - label: an optional label to display
361
        """
362
363 1320
        self.accessible = accessible
364 1320
        if orca_state.activeScript:
365
            [string, self.caretOffset, self.lineOffset] = \
366 1320
                 orca_state.activeScript.getTextLineAtCaret(self.accessible)
367
368
        # Sometimes, gnome-terminal will give us very odd values when
369
        # the user is editing using 'vi' and has positioned the caret
370
        # at the first character of the first line.  In this case, we
371
        # end up getting a very large negative number for the line offset.
372
        # So, we just assume the user is at the first character.
373
        #
374 1320
        if self.lineOffset < 0:
375 0
            self.caretOffset = 0
376 0
            self.lineOffset = 0
377
            [string, startOffset, endOffset] = \
378 0
                self.accessible.text.getTextAtOffset(
379 0
                    0,
380 0
                    atspi.Accessibility.TEXT_BOUNDARY_LINE_START)
381
382 1320
        cursorOffset = min(self.caretOffset - self.lineOffset, len(string))
383
384 1320
        self._maxCaretOffset = self.lineOffset + len(string.decode("UTF-8"))
385
386 1320
        self.eol = eol
387 1320
        string = string + self.eol
388
389 1320
        self.label = label
390 1320
        if self.label:
391 105
            string = self.label + " " + string
392 105
            cursorOffset += len(self.label.decode("UTF-8")) + 1
393
394 1320
        Region.__init__(self, string, cursorOffset)
395
396 1
    def repositionCursor(self):
397
        """Attempts to reposition the cursor in response to a new
398
        caret position.  If it is possible (i.e., the caret is on
399
        the same line as it was), reposition the cursor and return
400
        True.  Otherwise, return False.
401
        """
402
403
        [string, caretOffset, lineOffset] = \
404 1135
                 orca_state.activeScript.getTextLineAtCaret(self.accessible)
405 1135
        cursorOffset = min(caretOffset - lineOffset, len(string))
406 1135
        if self.label:
407 22
            cursorOffset += len(self.label.decode("UTF-8")) + 1
408
409 1135
        if lineOffset != self.lineOffset:
410 8
            return False
411
        else:
412 1127
            self.caretOffset = caretOffset
413 1127
            self.lineOffset = lineOffset
414 1127
            self.cursorOffset = cursorOffset
415
416 1127
        return True
417
418 1
    def processRoutingKey(self, offset):
419
        """Processes a cursor routing key press on this Component.  The offset
420
        is 0-based, where 0 represents the leftmost character of text
421
        associated with this region.  Note that the zeroeth character may have
422
        been scrolled off the display."""
423
424 0
        if self.label:
425 0
            offset = offset - len(self.label.decode("UTF-8")) - 1
426 0
            if offset < 0:
427 0
                return
428
429 0
        newCaretOffset = min(self.lineOffset + offset, self._maxCaretOffset)
430 0
        self.accessible.text.setCaretOffset(newCaretOffset)
431
432 1
    def getAttributeMask(self):
433
        """Creates a string which can be used as the attrOr field of brltty's
434
        write structure for the purpose of indicating text attributes and
435
        selection."""
436
437 3767
        text = self.accessible.text
438 3767
        if not text:
439 0
            return ''
440
441
        # Start with an empty mask.
442
        #
443 3767
        stringLength = len(self.string) - len(self.eol)
444 3767
        attrMask = ['\x00']*stringLength
445
446 3767
        attrIndicator = settings.textAttributesBrailleIndicator
447 3767
        selIndicator = settings.brailleSelectorIndicator
448 3767
        indicateAttr = (attrIndicator != settings.TEXT_ATTR_BRAILLE_NONE)
449 3767
        indicateSel = (selIndicator != settings.BRAILLE_SEL_NONE)
450
451 3767
        if indicateAttr:
452
            # Identify what attributes the user cares about.  Also get the
453
            # default attributes for the text object because those attributes
454
            # may not be included as attributes for the character.
455
            #
456 0
            script = orca_state.activeScript
457 0
            enabledAttributes = settings.enabledBrailledTextAttributes
458
            [userAttrList, userAttrDict] = \
459 0
                script.textAttrsToDictionary(enabledAttributes)
460 0
            defaultAttributes = text.getDefaultAttributes()
461
            [defaultAttrList, defaultAttrDict] = \
462 0
                script.textAttrsToDictionary(defaultAttributes)
463
464
            # [[[TODO - HACK - JD: The end offset returned by SO/OOo is the
465
            # offset of the first character that doesn't have the attribute
466
            # in question; all other apps return the offset of the last
467
            # character that has that attribute]]]
468
            #
469 0
            adjustment = 0
470 0
            if orca_state.locusOfFocus.app.name == "soffice.bin":
471 0
                adjustment += 1
472
473 0
            i = self.lineOffset
474 0
            endOffset = self.lineOffset + stringLength
475 0
            while i < endOffset - 1:
476 0
                for attribute in userAttrList:
477 0
                    defaultValue = None
478 0
                    if attribute in defaultAttrList:
479 0
                        defaultValue = defaultAttrDict[attribute]
480
                    [value, start, end, defined] = \
481 0
                            text.getAttributeValue(i, attribute)
482
483 0
                    notOfInterest = userAttrDict[attribute]
484 0
                    if defined:
485 0
                        weCare = (value != notOfInterest)
486 0
                    elif defaultValue and len(notOfInterest):
487 0
                        weCare = (defaultValue != notOfInterest)
488
                    else:
489 0
                        weCare = False
490
491 0
                    if weCare and start >= 0:
492 0
                        maskStart = start - self.lineOffset
493 0
                        maskEnd = end - self.lineOffset - adjustment
494 0
                        maskLength = maskEnd - maskStart
495
                        attrMask[maskStart:maskEnd] = \
496 0
                                                  [attrIndicator] * maskLength
497 0
                if (end <= i):
498 0
                    break
499
500 0
                i = end
501
502 3767
        if indicateSel:
503 3767
            nSelections = text.getNSelections()
504 3767
            if nSelections:
505 88
                for i in range(0, nSelections):
506 44
                    [start, end] = text.getSelection(i)
507 44
                    maskStart = max(self.lineOffset, start) - self.lineOffset
508 44
                    maskEnd = min(end - self.lineOffset, stringLength)
509
                    # Combine the selection indicator with the attribute
510
                    # indicator.
511
                    #
512 627
                    for j in range(maskStart, maskEnd):
513 583
                        if attrMask[j] != '\x00' \
514 0
                           and attrMask[j] != selIndicator:
515 0
                            attrMask[j] = '\xc0'
516
                        else:
517 583
                            attrMask[j] = selIndicator
518
519 3767
        mask = "".join(attrMask)
520
521
        # Add empty mask characters for the EOL character as well as for
522
        # any label that might be present.
523
        #
524 15068
        for i in range (0, len(self.eol)):
525 11301
            mask += '\x00'
526 3767
        if self.label:
527 1929
            for i in range (0, len(self.label) + 1):
528 1697
                mask = '\x00' + mask
529
530 3767
        return mask
531
532 2
class ReviewComponent(Component):
533 1
    """A subclass of Component that is to be used for flat review mode."""
534
535 1
    def __init__(self, accessible, string, cursorOffset, zone):
536
        """Creates a new Component.
537
538
        Arguments:
539
        - accessible: the accessible
540
        - string: the string to use to represent the component
541
        - cursorOffset: a 0-based index saying where to draw the cursor
542
                        for this Region if it gets focus.
543
        - zone: the flat review Zone associated with this component
544
        """
545
546 0
        Component.__init__(self, accessible, string, cursorOffset)
547 0
        self.zone = zone
548
549 2
class ReviewText(Region):
550
    """A subclass of Region backed by a Text object.  This Region will
551
    does not react to the caret changes, but will react if one updates
552
    the cursorPosition.  This class is meant to be used by flat review
553
    mode to show the current character position.
554 1
    """
555
556 1
    def __init__(self, accessible, string, lineOffset, zone):
557
        """Creates a new Text region.
558
559
        Arguments:
560
        - accessible: the accessible that implements AccessibleText
561
        - string: the string to use to represent the component
562
        - lineOffset: the character offset into where the text line starts
563
        - zone: the flat review Zone associated with this component
564
        """
565
566 0
        Region.__init__(self, string)
567 0
        self.accessible = accessible
568 0
        self.lineOffset = lineOffset
569 0
        self.zone = zone
570
571 1
    def processRoutingKey(self, offset):
572
        """Processes a cursor routing key press on this Component.  The offset
573
        is 0-based, where 0 represents the leftmost character of text
574
        associated with this region.  Note that the zeroeth character may have
575
        been scrolled off the display."""
576
577 0
        newCaretOffset = self.lineOffset + offset
578 0
        self.accessible.text.setCaretOffset(newCaretOffset)
579
580 2
class Line:
581
    """A horizontal line on the display.  Each Line is composed of a sequential
582
    set of Regions.
583 1
    """
584
585 1
    def __init__(self, region=None):
586 2217
        self.regions = []
587 2217
        self.string = ""
588 2217
        if region:
589 2
            self.addRegion(region)
590
591 1
    def addRegion(self, region):
592 2
        self.regions.append(region)
593
594 1
    def addRegions(self, regions):
595 4060
        self.regions.extend(regions)
596
597 1
    def getLineInfo(self):
598
        """Computes the complete string for this line as well as a
599
        0-based index where the focused region starts on this line.
600
        If the region with focus is not on this line, then the index
601
        will be -1.
602
603
        Returns [string, offsetIndex, attributeMask]
604
        """
605
606 5561
        string = ""
607 5561
        focusOffset = -1
608 5561
        attributeMask = ""
609 33738
        for region in self.regions:
610 28177
            if region == _regionWithFocus:
611 5561
                focusOffset = len(string.decode("UTF-8"))
612 28177
            if region.string:
613
                # [[[TODO: WDW - HACK: Replace UTF-8 ellipses with "..."
614
                # The ultimate solution is to get i18n support into
615
                # BrlTTY.]]]
616
                #
617 27064
                string += region.string.replace("\342\200\246", "...")
618 28177
            mask = region.getAttributeMask()
619 28177
            attributeMask += mask
620
621 5561
        return [string, focusOffset, attributeMask]
622
623 1
    def getRegionAtOffset(self, offset):
624
        """Finds the Region at the given 0-based offset in this line.
625
626
        Returns the [region, offsetinregion] where the region is
627
        the region at the given offset, and offsetinregion is the
628
        0-based offset from the beginning of the region, representing
629
        where in the region the given offset is."""
630
631
        # Translate the cursor offset for this line into a cursor offset
632
        # for a region, and then pass the event off to the region for
633
        # handling.
634
        #
635 0
        region = None
636 0
        string = ""
637 0
        pos = 0
638 0
        for region in self.regions:
639 0
            string = string + region.string
640 0
            if len(string.decode("UTF-8")) > offset:
641 0
                break
642
            else:
643 0
                pos = len(string.decode("UTF-8"))
644
645 0
        if offset >= len(string.decode("UTF-8")):
646 0
            return [None, -1]
647
        else:
648 0
            return [region, offset - pos]
649
650 1
    def processRoutingKey(self, offset):
651
        """Processes a cursor routing key press on this Component.  The offset
652
        is 0-based, where 0 represents the leftmost character of string
653
        associated with this line.  Note that the zeroeth character may have
654
        been scrolled off the display."""
655
656 0
        [region, regionOffset] = self.getRegionAtOffset(offset)
657 0
        if region:
658 0
            region.processRoutingKey(regionOffset)
659
660 1
def getRegionAtCell(cell):
661
    """Given a 1-based cell offset, return the braille region
662
    associated with that cell in the form of [region, offsetinregion]
663
    where 'region' is the region associated with the cell and
664
    'offsetinregion' is the 0-based offset of where the cell is
665
    in the region, where 0 represents the beginning of the region, """
666
667 0
    if len(_lines) > 0:
668 0
        offset = (cell - 1) + _viewport[0]
669 0
        lineNum = _viewport[1]
670 0
        return _lines[lineNum].getRegionAtOffset(offset)
671
    else:
672 0
        return [None, -1]
673
674 1
def clear():
675
    """Clears the logical structure, but keeps the Braille display as is
676
    (until a refresh operation).
677
    """
678
679
    global _lines
680
    global _regionWithFocus
681
    global _viewport
682
683 2217
    _lines = []
684 2217
    _regionWithFocus = None
685 2217
    _viewport = [0, 0]
686
687 1
def setLines(lines):
688
    global _lines
689 0
    _lines = lines
690
691 1
def addLine(line):
692
    """Adds a line to the logical display for painting.  The line is added to
693
    the end of the current list of known lines.  It is necessary for the
694
    viewport to be over the lines and for refresh to be called for the new
695
    line to be painted.
696
697
    Arguments:
698
    - line: an instance of Line to add.
699
    """
700
701
    global _lines
702
703 2217
    _lines.append(line)
704 2217
    line._index = len(_lines)
705
706 1
def getShowingLine():
707
    """Returns the Line that is currently being painted on the display.
708
    """
709 1141
    return _lines[_viewport[1]]
710
711 1
def setFocus(region, panToFocus=True):
712
    """Specififes the region with focus.  This region will be positioned
713
    at the home position if panToFocus is True.
714
715
    Arguments:
716
    - region: the given region, which much be in a line that has been
717
              added to the logical display
718
    """
719
720
    global _regionWithFocus
721
722 2217
    _regionWithFocus = region
723
724 2217
    if not panToFocus or (not _regionWithFocus):
725 0
        return
726
727
    # Adjust the viewport according to the new region with focus.
728
    # The goal is to have the first cell of the region be in the
729
    # home position, but we will give priority to make sure the
730
    # cursor for the region is on the display.  For example, when
731
    # faced with a long text area, we'll show the position with
732
    # the caret vs. showing the beginning of the region.
733
734 2217
    lineNum = 0
735 2217
    done = False
736 2217
    for line in _lines:
737 11399
        for reg in line.regions:
738 11399
            if reg == _regionWithFocus:
739 2217
                _viewport[1] = lineNum
740 2217
                done = True
741 2217
                break
742 2217
        if done:
743 2217
            break
744
        else:
745 0
            lineNum += 1
746
747 2217
    line = _lines[_viewport[1]]
748 2217
    [string, offset, attributeMask] = line.getLineInfo()
749
750
    # If the cursor is too far right, we scroll the viewport
751
    # so the cursor will be on the last cell of the display.
752
    #
753 2217
    if _regionWithFocus.cursorOffset >= _displaySize[0]:
754 165
        offset += _regionWithFocus.cursorOffset - _displaySize[0] + 1
755
756 2217
    _viewport[0] = max(0, offset)
757
758 1
def refresh(panToCursor=True, targetCursorCell=0):
759
    """Repaints the Braille on the physical display.  This clips the entire
760
    logical structure by the viewport and also sets the cursor to the
761
    appropriate location.  [[[TODO: WDW - I'm not sure how BrlTTY handles
762
    drawing to displays with more than one line, so I'm only going to handle
763
    drawing one line right now.]]]
764
765
    Arguments:
766
767
    - panToCursor: if True, will adjust the viewport so the cursor is
768
                   showing.
769
    - targetCursorCell: Only effective if panToCursor is True.
770
                        0 means automatically place the cursor somewhere
771
                        on the display so as to minimize movement but
772
                        show as much of the line as possible.  A positive
773
                        value is a 1-based target cell from the left side
774
                        of the display and a negative value is a 1-based
775
                        target cell from the right side of the display.
776
    """
777
778
    global endIsShowing
779
    global beginningIsShowing
780
    global cursorCell
781
    global monitor
782
783 3344
    if len(_lines) == 0:
784 0
        if useBrlAPIBindings:
785 0
            if brlAPIRunning:
786 0
                brlAPI.writeText("", 0)
787
        else:
788 0
            brl.writeText(0, "")
789 0
        return
790
791
    # Now determine the location of the cursor.  First, we'll figure
792
    # out the 1-based offset for where we want the cursor to be.  If
793
    # the target cell is less than zero, it means an offset from the
794
    # right hand side of the display.
795
    #
796 3344
    if targetCursorCell < 0:
797 0
        targetCursorCell = _displaySize[0] + targetCursorCell + 1
798
799
    # Now, we figure out the 0-based offset for where the cursor
800
    # actually is in the string.
801
    #
802 3344
    line = _lines[_viewport[1]]
803 3344
    [string, focusOffset, attributeMask] = line.getLineInfo()
804 3344
    cursorOffset = -1
805 3344
    if focusOffset >= 0:
806 3344
        cursorOffset = focusOffset + _regionWithFocus.cursorOffset
807
808
    # Now, if desired, we'll automatically pan the viewport to show
809
    # the cursor.  If there's no targetCursorCell, then we favor the
810
    # left of the display if we need to pan left, or we favor the
811
    # right of the display if we need to pan right.
812
    #
813 3344
    if panToCursor and (cursorOffset >= 0):
814 3342
        if len(string.decode("UTF-8")) <= _displaySize[0]:
815 958
            _viewport[0] = 0
816 2384
        elif targetCursorCell:
817 0
            _viewport[0] = max(0, cursorOffset - targetCursorCell + 1)
818 2384
        elif cursorOffset < _viewport[0]:
819 0
            _viewport[0] = max(0, cursorOffset)
820 2384
        elif cursorOffset >= (_viewport[0] + _displaySize[0]):
821 2
            _viewport[0] = max(0, cursorOffset - _displaySize[0] + 1)
822
823 3344
    startPos = _viewport[0]
824 3344
    endPos = startPos + _displaySize[0]
825
826
    # Now normalize the cursor position to BrlTTY, which uses 1 as
827
    # the first cursor position as opposed to 0.
828
    #
829 3344
    cursorCell = cursorOffset - startPos
830 3344
    if (cursorCell < 0) or (cursorCell >= _displaySize[0]):
831 2
        cursorCell = 0
832
    else:
833 3342
        cursorCell += 1 # Normalize to 1-based offset
834
835 3344
    debug.println(debug.LEVEL_INFO, "BRAILLE LINE:  '%s'" % string)
836 3344
    debug.println(debug.LEVEL_INFO, "     VISIBLE:  '%s', cursor=%d" \
837 3344
                  % (string[startPos:endPos], cursorCell))
838
839 3344
    log.info("refresh line='%s', visible='%s', cursor=%d" \
840 3344
             % (string, string[startPos:endPos], cursorCell))
841
842 3344
    string = string.decode("UTF-8")
843 3344
    substring = string[startPos:endPos].encode("UTF-8")
844 3344
    if useBrlAPIBindings:
845 0
        if brlAPIRunning:
846 0
            try:
847
                # The name after (and including) BrlTTY v3.8 revision 2810
848
                #
849 0
                writeStruct = brlapi.WriteStruct()
850 0
            except:
851
                # The name before BrlTTY v3.8 revision 2810
852
                #
853 0
                writeStruct = brlapi.Write()
854 0
            writeStruct.regionBegin = 1
855 0
            writeStruct.regionSize = len(substring.decode("UTF-8"))
856 0
            while writeStruct.regionSize < _displaySize[0]:
857 0
                substring += " "
858 0
                if attributeMask:
859 0
                    attributeMask += '\x00'
860 0
                writeStruct.regionSize += 1
861 0
            writeStruct.text = substring
862 0
            writeStruct.cursor = cursorCell
863 0
            writeStruct.charset = "UTF-8"
864
865
            # [[[WDW - if you want to muck around with the dots on the
866
            # display to do things such as add underlines, you can use
867
            # the attrOr field of the write structure to do so.  The
868
            # attrOr field is a string whose length must be the same
869
            # length as the display and whose dots will end up showing
870
            # up on the display.  Each character represents a bitfield
871
            # where each bit corresponds to a dot (i.e., bit 0 = dot 1,
872
            # bit 1 = dot 2, and so on).  Here's an example that underlines
873
            # all the text.]]]
874
            #
875
            #myUnderline = ""
876
            #for i in range(0, _displaySize[0]):
877
            #    myUnderline += '\xc0'
878
            #writeStruct.attrOr = myUnderline
879
880 0
            if attributeMask:
881 0
                writeStruct.attrOr = attributeMask[startPos:endPos]
882
883 0
            brlAPI.write(writeStruct)
884
    else:
885 3344
        brl.writeText(cursorCell, substring)
886
887 3344
    if settings.enableBrailleMonitor:
888 0
        if not monitor:
889 0
            monitor = brlmon.BrlMon(_displaySize[0])
890 0
            monitor.show_all()
891 0
        if attributeMask:
892 0
            subMask = attributeMask[startPos:endPos]
893
        else:
894 0
            subMask = None
895 0
        monitor.writeText(cursorCell, substring, subMask)
896 3344
    elif monitor:
897 0
        monitor.destroy()
898
899 3344
    beginningIsShowing = startPos == 0
900 3344
    endIsShowing = endPos >= len(string)
901
902 1
def displayRegions(regionInfo):
903
    """Displays a list of regions on a single line, setting focus to the
904
       specified region.  The regionInfo parameter is something that is
905
       typically returned by a call to braillegenerator.getBrailleRegions.
906
907
    Arguments:
908
    - regionInfo: a list where the first element is a list of regions
909
                  to display and the second element is the region
910
                  with focus (must be in the list from element 0)
911
    """
912
913 0
    regions = regionInfo[0]
914 0
    focusedRegion = regionInfo[1]
915
916 0
    clear()
917 0
    line = Line()
918 0
    for item in regions:
919 0
        line.addRegion(item)
920 0
    addLine(line)
921 0
    setFocus(focusedRegion)
922 0
    refresh()
923
924 1
def displayMessage(message, cursor=-1):
925
    """Displays a single line, setting the cursor to the given position,
926
    ensuring that the cursor is in view.
927
928
    Arguments:
929
    - message: the string to display
930
    - cursor: the 0-based cursor position, where -1 (default) means no cursor
931
    """
932
933 2
    clear()
934 2
    region = Region(message, cursor)
935 2
    addLine(Line(region))
936 2
    setFocus(region)
937 2
    refresh(True)
938
939 1
def panLeft(panAmount=0):
940
    """Pans the display to the left, limiting the pan to the beginning
941
    of the line being displayed.
942
943
    Arguments:
944
    - panAmount: the amount to pan.  A value of 0 means the entire
945
                 width of the physical display.
946
947
    Returns True if a pan actually happened.
948
    """
949
950 0
    oldX = _viewport[0]
951
952 0
    if panAmount == 0:
953 0
        panAmount = _displaySize[0]
954
955 0
    if _viewport[0] > 0:
956 0
        _viewport[0] = max(0, _viewport[0] - panAmount)
957
958 0
    return oldX != _viewport[0]
959
960 1
def panRight(panAmount=0):
961
    """Pans the display to the right, limiting the pan to the length
962
    of the line being displayed.
963
964
    Arguments:
965
    - panAmount: the amount to pan.  A value of 0 means the entire
966
                 width of the physical display.
967
968
    Returns True if a pan actually happened.
969
    """
970
971 0
    oldX = _viewport[0]
972
973 0
    if panAmount == 0:
974 0
        panAmount = _displaySize[0]
975
976 0
    if len(_lines) > 0:
977 0
        lineNum = _viewport[1]
978 0
        newX = _viewport[0] + panAmount
979 0
        [string, focusOffset, attributeMask] = _lines[lineNum].getLineInfo()
980 0
        if newX < len(string.decode("UTF-8")):
981 0
            _viewport[0] = newX
982
983 0
    return oldX != _viewport[0]
984
985 1
def panToOffset(offset):
986
    """Automatically pan left or right to make sure the current offset is
987
    showing."""
988
989 0
    while offset < _viewport[0]:
990 0
        debug.println(debug.LEVEL_FINEST,
991 0
                      "braille.panToOffset (left) %d" % offset)
992 0
        if not panLeft():
993 0
            break
994
995 0
    while offset >= (_viewport[0] + _displaySize[0]):
996 0
        debug.println(debug.LEVEL_FINEST,
997 0
                      "braille.panToOffset (right) %d" % offset)
998 0
        if not panRight():
999 0
            break
1000
1001 1
def returnToRegionWithFocus(inputEvent=None):
1002
    """Pans the display so the region with focus is displayed.
1003
1004
    Arguments:
1005
    - inputEvent: the InputEvent instance that caused this to be called.
1006
1007
    Returns True to mean the command should be consumed.
1008
    """
1009
1010 0
    setFocus(_regionWithFocus)
1011 0
    refresh(True)
1012
1013 0
    return True
1014
1015 1
def _processBrailleEvent(command):
1016
    """Handles BrlTTY command events.  This passes commands on to Orca for
1017
    processing.  If Orca does not handle them (as indicated by a return value
1018
    of false from the callback passed to init, it will attempt to handle the
1019
    command itself - either by panning the viewport or passing cursor routing
1020
    keys to the Regions for handling.
1021
1022
    Arguments:
1023
    - command: the BrlAPI command for the key that was pressed.
1024
    """
1025
1026 0
    _printBrailleEvent(debug.LEVEL_FINE, command)
1027
1028
    # [[[TODO: WDW - DaveM suspects the Alva driver is sending us a
1029
    # repeat flag.  So...let's kill a couple birds here until BrlTTY
1030
    # 3.8 fixes the problem: we'll disable autorepeat and we'll also
1031
    # strip out the autorepeat flag if this is the first press of a
1032
    # button.]]]
1033
    #
1034 0
    if command & BRL_FLG_REPEAT_INITIAL:
1035 0
        command &= ~(BRL_FLG_REPEAT_INITIAL | BRL_FLG_REPEAT_DELAY)
1036 0
    elif command & BRL_FLG_REPEAT_DELAY:
1037 0
        return True
1038
1039 0
    consumed = False
1040
1041 0
    if settings.timeoutCallback and (settings.timeoutTime > 0):
1042 0
        signal.signal(signal.SIGALRM, settings.timeoutCallback)
1043 0
        signal.alarm(settings.timeoutTime)
1044
1045 0
    if _callback:
1046 0
        try:
1047
            # Like key event handlers, a return value of True means
1048
            # the command was consumed.
1049
            #
1050 0
            consumed = _callback(command)
1051 0
        except:
1052 0
            debug.printException(debug.LEVEL_WARNING)
1053 0
            consumed = False
1054
1055 0
    if (command >= 0x100) and (command < (0x100 + _displaySize[0])):
1056 0
        if len(_lines) > 0:
1057 0
            cursor = (command - 0x100) + _viewport[0]
1058 0
            lineNum = _viewport[1]
1059 0
            _lines[lineNum].processRoutingKey(cursor)
1060 0
            consumed = True
1061
1062 0
    if settings.timeoutCallback and (settings.timeoutTime > 0):
1063 0
        signal.alarm(0)
1064
1065 0
    return consumed
1066
1067 1
def _brlAPIKeyReader(source, condition):
1068
        """Method to read a key from the BrlAPI bindings.  This is a
1069
        gobject IO watch handler.
1070
        """
1071 0
        key = brlAPI.readKey(False)
1072 0
        if key:
1073 0
            flags = key >> 32
1074 0
            lower = key & 0xFFFFFFFF
1075 0
            keyType = lower >> 29
1076 0
            keyCode = lower & 0x1FFFFFFF
1077
1078
            # [[TODO: WDW - HACK If we have a cursor routing key, map
1079
            # it back to the code we used to get with earlier versions
1080
            # of BrlAPI (i.e., bit 0x100 was the indicator of a cursor
1081
            # routing key instead of 0x1000).  This may change before
1082
            # the offical BrlAPI Python bindings are released.]]]
1083
            #
1084 0
            if keyCode & 0x10000:
1085 0
                keyCode = 0x100 | (keyCode & 0xFF)
1086 0
            if keyCode:
1087 0
                _processBrailleEvent(keyCode)
1088 0
        return brlAPIRunning
1089
1090 1
def setupKeyRanges(keys):
1091
    """Hacky method to tell BrlTTY what to send and not send us via
1092
    the readKey method.  This only works with BrlTTY v3.8 and better.
1093
1094
    Arguments:
1095
    -keys: a list of BrlAPI commands.
1096
    """
1097 358
    if not brlAPIRunning:
1098 358
        return
1099
1100 0
    try:
1101
        # First, start by ignoring everything.
1102
        #
1103 0
        brlAPI.ignoreKeys(brlapi.rangeType_all, [0])
1104
1105
        # Next, enable cursor routing keys.
1106
        #
1107 0
        keySet = [brlapi.KEY_TYPE_CMD | brlapi.KEY_CMD_ROUTE]
1108
1109
        # Finally, enable the commands we care about.
1110
        #
1111 0
        for key in keys:
1112 0
            keySet.append(brlapi.KEY_TYPE_CMD | key)
1113
1114 0
        brlAPI.acceptKeys(brlapi.rangeType_command, keySet)
1115
1116 0
        debug.println(debug.LEVEL_FINEST, "Using BrlAPI v0.5.0+")
1117 0
    except:
1118 0
        debug.printException(debug.LEVEL_FINEST)
1119 0
        try:
1120
            # Old, incompatible way that was in v3.8 devel, but
1121
            # changed prior to release.  We need this just in case
1122
            # people have not updated yet.
1123
1124
            # First, start by ignoring everything.
1125
            #
1126 0
            brlAPI.ignoreKeyRange(0,
1127
                                  brlapi.KEY_FLAGS_MASK \
1128
                                  | brlapi.KEY_TYPE_MASK \
1129 0
                                  | brlapi.KEY_CODE_MASK)
1130
1131
            # Next, enable cursor routing keys.
1132
            #
1133 0
            brlAPI.acceptKeyRange(brlapi.KEY_TYPE_CMD | brlapi.KEY_CMD_ROUTE,
1134
                                  brlapi.KEY_TYPE_CMD \
1135
                                  | brlapi.KEY_CMD_ROUTE \
1136 0
                                  | brlapi.KEY_CMD_ARG_MASK)
1137
1138
            # Finally, enable the commands we care about.
1139
            #
1140 0
            keySet = []
1141 0
            for key in keys:
1142 0
                keySet.append(brlapi.KEY_TYPE_CMD | key)
1143 0
            if len(keySet):
1144 0
                brlAPI.acceptKeySet(keySet)
1145
1146 0
            debug.println(debug.LEVEL_FINEST,
1147 0
                          "Using BrlAPI pre-release v0.5.0")
1148 0
        except:
1149 0
            debug.printException(debug.LEVEL_FINEST)
1150 0
            debug.println(
1151 0
                debug.LEVEL_WARNING,
1152 0
                "Braille module cannot listen for braille input events")
1153
1154 1
def init(callback=None, tty=7):
1155
    """Initializes the braille module, connecting to the BrlTTY driver.
1156
1157
    Arguments:
1158
    - callback: the method to call with a BrlTTY input event.
1159
    - tty: the tty port to take ownership of (default = 7)
1160
    Returns True if the initialization procedure was run or False if this
1161
    module has already been initialized.
1162
    """
1163
1164
    global _initialized
1165
    global _displaySize
1166
    global _callback
1167
1168 1
    if _initialized:
1169 0
        return False
1170
1171 1
    _callback = callback
1172
1173 1
    if useBrlAPIBindings:
1174 0
        try:
1175
            global brlAPI
1176
            global brlAPIRunning
1177
            global brlAPISourceId
1178
1179 0
            gobject.threads_init()
1180 0
            brlAPI = brlapi.Connection()
1181
1182 0
            try:
1183 0
                import os
1184 0
                windowPath = os.environ["WINDOWPATH"]
1185 0
                brlAPI.enterTtyModeWithPath()
1186 0
                brlAPIRunning = True
1187 0
                debug.println(\
1188 0
                    debug.LEVEL_CONFIGURATION,
1189 0
                    "Braille module has been initialized using WINDOWPATH=" \
1190 0
                    + "%s" % windowPath)
1191 0
            except:
1192 0
                brlAPI.enterTtyMode(tty)
1193 0
                brlAPIRunning = True
1194 0
                debug.println(\
1195 0
                    debug.LEVEL_CONFIGURATION,
1196 0
                    "Braille module has been initialized using tty=%d" % tty)
1197 0
            brlAPISourceId = gobject.io_add_watch(brlAPI.fileDescriptor,
1198 0
                                                  gobject.IO_IN,
1199 0
                                                  _brlAPIKeyReader)
1200 0
        except:
1201 0
            debug.printException(debug.LEVEL_FINEST)
1202 0
            return False
1203
    else:
1204 1
        if brl.init(tty):
1205 0
            debug.println(debug.LEVEL_CONFIGURATION,
1206 0
                          "Braille module has been initialized.")
1207 0
            brl.registerCallback(_processBrailleEvent)
1208
        else:
1209 0
            debug.println(debug.LEVEL_CONFIGURATION,
1210 0
                          "Braille module has NOT been initialized.")
1211 0
            return False
1212
1213
    # [[[TODO: WDW - For some reason, BrlTTY wants to say the height of the
1214
    # Vario is 40 so we hardcode it to 1 for now.]]]
1215
    #
1216
    #_displaySize = (brl.getDisplayWidth(), brl.getDisplayHeight())
1217 0
    if useBrlAPIBindings:
1218 0
        (x, y) = brlAPI.displaySize
1219 0
        _displaySize = [x, 1]
1220
    else:
1221 0
        _displaySize = [brl.getDisplayWidth(), 1]
1222
1223 0
    debug.println(debug.LEVEL_CONFIGURATION,
1224 0
                  "braille display size = (%d, %d)" \
1225 0
                  % (_displaySize[0], _displaySize[1]))
1226
1227 0
    clear()
1228 0
    refresh(True)
1229
1230 0
    _initialized = True
1231
1232 0
    return True
1233
1234 1
def shutdown():
1235
    """Shuts down the braille module.   Returns True if the shutdown procedure
1236
    was run or False if this module has not been initialized.
1237
    """
1238
1239
    global _initialized
1240
1241 2
    if not _initialized:
1242 2
        return False
1243
1244
    global brlAPIRunning
1245
    global brlAPISourceId
1246
1247 0
    if useBrlAPIBindings:
1248 0
        if brlAPIRunning:
1249 0
            brlAPIRunning = False
1250 0
            gobject.source_remove(brlAPISourceId)
1251 0
            brlAPISourceId = 0
1252 0
            brlAPI.leaveTtyMode()
1253
    else:
1254 0
        brl.shutdown()
1255
1256 0
    _initialized = False
1257
1258 0
    return True