Coverage Report - orca.braille

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