[orca] Fix for bgo#354471 - Text selection from braille input device



commit 3b8c276d81147c8dce6dfd8e4e1478c9ab6dad58
Author: Willie Walker <william walker sun com>
Date:   Sat May 9 21:24:23 2009 -0400

    Fix for bgo#354471 - Text selection from braille input device
    
    This is the first step of implementing this feature.  Here's the behavior:
    
    KEY_CMD_CUTBEGIN (Dot 1 + cursor routing key on my display) - this will specify
    the start of a selection.  Orca will merely move the caret to the given spot
    and will clear any existing selection.
    
    KEY_CMD_CUTLINE (Dot 4 + cursor routing key on my display) - this will specify
    the end of a selection and the selected text is automatically copied to the
    system clipboard.  If a selection doesn't exist, Orca creates a new one where
    the other endpoint of the selection is where the caret is.  If a selection
    exists and the selection point is outside the existing selection, Orca extends
    the existing one.  If a selection exists and the selection point is inside the
    existing selection, Orca trims the selection from the right (i.e., the selected
    text that's after the selection point becomes unselected).
    
    Known issues that need to be resolved:
    
    1) This only works in text areas.  It doesn't work across things such as
       paragraphs in OpenOffice.
    
    2) There's some strangeness with speech feedback: it sometimes says
       "unselected" when the text is selected.  This should be fixable, but
       there also probably shouldn't be any speech feedback when doing this
       from the braille display.
---
 src/orca/braille.py |  141 ++++++++++++++++++++++++++++++++++++++++++---------
 src/orca/default.py |   84 +++++++++++++++++++++++++++++-
 2 files changed, 198 insertions(+), 27 deletions(-)

diff --git a/src/orca/braille.py b/src/orca/braille.py
index 5109ac8..193622e 100644
--- a/src/orca/braille.py
+++ b/src/orca/braille.py
@@ -1,6 +1,6 @@
 # Orca
 #
-# Copyright 2005-2008 Sun Microsystems Inc.
+# Copyright 2005-2009 Sun Microsystems Inc.
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Library General Public
@@ -28,7 +28,7 @@ moving the text caret.
 __id__        = "$Id$"
 __version__   = "$Revision$"
 __date__      = "$Date$"
-__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc."
+__copyright__ = "Copyright (c) 2005-2009 Sun Microsystems Inc."
 __license__   = "LGPL"
 
 import logging
@@ -149,6 +149,24 @@ command_name[brlapi.KEY_CMD_SIXDOTS]  = _("Six Dots")
 #
 command_name[brlapi.KEY_CMD_ROUTE]    = _("Cursor Routing")
 
+# Translators: this is a command for a button on a refreshable braille
+# display (an external hardware device used by people who are blind).
+# This command represents the start of a selection operation.  It is
+# called "Cut Begin" to map to what BrlTTY users are used to:  in
+# character cell mode operation on virtual consoles, the act of copying
+# text is erroneously called a "cut" operation.
+#
+command_name[brlapi.KEY_CMD_CUTBEGIN] = _("Cut Begin")
+
+# Translators: this is a command for a button on a refreshable braille
+# display (an external hardware device used by people who are blind).
+# This command represents marking the endpoint of a selection.  It is
+# called "Cut Line" to map to what BrlTTY users are used to:  in
+# character cell mode operation on virtual consoles, the act of copying
+# text is erroneously called a "cut" operation.
+#
+command_name[brlapi.KEY_CMD_CUTLINE] = _("Cut Line")
+
 # The size of the physical display (width, height).  The coordinate system of
 # the display is set such that the upper left is (0,0), x values increase from
 # left to right, and y values increase from top to bottom.
@@ -224,9 +242,9 @@ class Region:
         # If louis is None, then we don't go into contracted mode.
         self.contracted = settings.enableContractedBraille and \
                           louis is not None
-        
+
         self.expandOnCursor = expandOnCursor
-        
+
         # The uncontracted string for the line.
         #
         self.rawLine = string.decode("UTF-8").strip("\n")
@@ -241,7 +259,7 @@ class Region:
         else:
             self.string = self.rawLine
             self.cursorOffset = cursorOffset
-            
+
     def processRoutingKey(self, offset):
         """Processes a cursor routing key press on this Component.  The offset
         is 0-based, where 0 represents the leftmost character of string
@@ -269,7 +287,7 @@ class Region:
         #
         mask = ['\x00'] * maskSize
         return "".join(mask)
-    
+
     def repositionCursor(self):
         """Reposition the cursor offset for contracted mode.
         """
@@ -293,7 +311,7 @@ class Region:
             cursorOnSpace = line[cursorOffset] == ' '
         except IndexError:
             cursorOnSpace = False
-            
+
         if not expandOnCursor or cursorOnSpace:
             contracted, inPos, outPos, cursorPos = \
                              louis.translate([self.contractionTable],
@@ -307,7 +325,7 @@ class Region:
                                              mode=louis.MODE.compbrlAtCursor)
 
         return contracted, inPos, outPos, cursorPos
-    
+
     def displayToBufferOffset(self, display_offset):
         try:
             offset = self.inPos[display_offset]
@@ -336,7 +354,7 @@ class Region:
                                        self.cursorOffset,
                                        self.expandOnCursor)
         self.contracted = True
-        
+
     def expandRegion(self):
         if not self.contracted:
             return
@@ -346,7 +364,7 @@ class Region:
         except IndexError:
             self.cursorOffset = len(self.string)
         self.contracted = False
-        
+
 class Component(Region):
     """A subclass of Region backed by an accessible.  This Region will react
     to any cursor routing key events and perform the default action on the
@@ -373,6 +391,15 @@ class Component(Region):
 
         self.accessible = accessible
 
+    def getCaretOffset(self, offset):
+        """Returns the caret position of the given offset if the object
+        has text with a caret.  Otherwise, returns -1.
+
+        Arguments:
+        - offset: 0-based offset of the cell on the physical display
+        """
+        return -1
+
     def processRoutingKey(self, offset):
         """Processes a cursor routing key press on this Component.  The offset
         is 0-based, where 0 represents the leftmost character of string
@@ -429,7 +456,7 @@ class Text(Region):
     [[[TODO: WDW - need to add in text selection capabilities.  Logged
     as bugzilla bug 319754.]]]"""
 
-    def __init__(self, accessible, label="", eol="", 
+    def __init__(self, accessible, label="", eol="",
                  startOffset=None, endOffset=None):
         """Creates a new Text region.
 
@@ -499,7 +526,7 @@ class Text(Region):
         string = string.decode("UTF-8")
 
         cursorOffset = min(caretOffset - lineOffset, len(string))
-        
+
         if lineOffset != self.lineOffset:
             return False
 
@@ -516,20 +543,33 @@ class Text(Region):
 
         return True
 
+    def getCaretOffset(self, offset):
+        """Returns the caret position of the given offset if the object
+        has text with a caret.  Otherwise, returns -1.
+
+        Arguments:
+        - offset: 0-based offset of the cell on the physical display
+        """
+        offset = self.displayToBufferOffset(offset)
+
+        if offset < 0:
+            return -1
+
+        return min(self.lineOffset + offset, self._maxCaretOffset)
+
     def processRoutingKey(self, offset):
         """Processes a cursor routing key press on this Component.  The offset
         is 0-based, where 0 represents the leftmost character of text
         associated with this region.  Note that the zeroeth character may have
-        been scrolled off the display."""
-        
-        offset = self.displayToBufferOffset(offset)
+        been scrolled off the display.
+        """
 
-        if offset < 0:
+        caretOffset = self.getCaretOffset(offset)
+
+        if caretOffset < 0:
             return
 
-        newCaretOffset = min(self.lineOffset + offset, self._maxCaretOffset)
-        orca_state.activeScript.setCaretOffset(
-            self.accessible, newCaretOffset)
+        orca_state.activeScript.setCaretOffset(self.accessible, caretOffset)
 
     def getAttributeMask(self, getLinkMask=True):
         """Creates a string which can be used as the attrOr field of brltty's
@@ -636,7 +676,7 @@ class Text(Region):
     def contractLine(self, line, cursorOffset=0, expandOnCursor=True):
         contracted, inPos, outPos, cursorPos = Region.contractLine(
             self, line, cursorOffset, expandOnCursor)
-        
+
         return contracted + self.eol, inPos, outPos, cursorPos
 
     def displayToBufferOffset(self, display_offset):
@@ -688,15 +728,28 @@ class ReviewText(Region):
         self.lineOffset = lineOffset
         self.zone = zone
 
+    def getCaretOffset(self, offset):
+        """Returns the caret position of the given offset if the object
+        has text with a caret.  Otherwise, returns -1.
+
+        Arguments:
+        - offset: 0-based offset of the cell on the physical display
+        """
+        offset = self.displayToBufferOffset(offset)
+
+        if offset < 0:
+            return -1
+
+        return self.lineOffset + offset
+
     def processRoutingKey(self, offset):
         """Processes a cursor routing key press on this Component.  The offset
         is 0-based, where 0 represents the leftmost character of text
         associated with this region.  Note that the zeroeth character may have
         been scrolled off the display."""
 
-        offset = self.displayToBufferOffset(offset)
-        newCaretOffset = self.lineOffset + offset
-        orca_state.activeScript.setCaretOffset(self.accessible, newCaretOffset)
+        caretOffset = self.getCaretOffset(offset)
+        orca_state.activeScript.setCaretOffset(self.accessible, caretOffset)
 
 class Line:
     """A horizontal line on the display.  Each Line is composed of a sequential
@@ -794,7 +847,8 @@ def getRegionAtCell(cell):
     associated with that cell in the form of [region, offsetinregion]
     where 'region' is the region associated with the cell and
     'offsetinregion' is the 0-based offset of where the cell is
-    in the region, where 0 represents the beginning of the region, """
+    in the region, where 0 represents the beginning of the region.
+    """
 
     if len(_lines) > 0:
         offset = (cell - 1) + viewport[0]
@@ -803,6 +857,27 @@ def getRegionAtCell(cell):
     else:
         return [None, -1]
 
+def getCaretContext(event):
+    """Gets the accesible and caret offset associated with the given
+    event.  The event should have a BrlAPI event that contains an
+    argument value that corresponds to a cell on the display.
+
+    Arguments:
+    - event: an instance of input_event.BrailleEvent.  event.event is
+    the dictionary form of the expanded BrlAPI event.
+    """
+
+    offset = event.event["argument"]
+    [region, regionOffset] = getRegionAtCell(offset + 1)
+    if region and (isinstance(region, Text) or isinstance(region, ReviewText)):
+        accessible = region.accessible
+        caretOffset = region.getCaretOffset(regionOffset)
+    else:
+        accessible = None
+        caretOffset = -1
+
+    return [accessible, caretOffset]
+
 def clear():
     """Clears the logical structure, but keeps the Braille display as is
     (until a refresh operation).
@@ -1139,6 +1214,13 @@ def returnToRegionWithFocus(inputEvent=None):
     return True
 
 def setContractedBraille(event):
+    """Turns contracted braille on or off based upon the event.
+
+    Arguments:
+    - event: an instance of input_event.BrailleEvent.  event.event is
+    the dictionary form of the expanded BrlAPI event.
+    """
+
     settings.enableContractedBraille = \
         (event.event["flags"] & brlapi.KEY_FLG_TOGGLE_ON) != 0
     for line in _lines:
@@ -1146,11 +1228,20 @@ def setContractedBraille(event):
     refresh()
 
 def processRoutingKey(event):
+    """Processes a cursor routing key event.
+
+    Arguments:
+    - event: an instance of input_event.BrailleEvent.  event.event is
+    the dictionary form of the expanded BrlAPI event.
+    """
+
     cell = event.event["argument"]
+
     if len(_lines) > 0:
         cursor = cell + viewport[0]
         lineNum = viewport[1]
         _lines[lineNum].processRoutingKey(cursor)
+
     return True
 
 def _processBrailleEvent(event):
@@ -1215,7 +1306,7 @@ def setupKeyRanges(keys):
     #
     for key in keys:
         keySet.append(brlapi.KEY_TYPE_CMD | key)
-            
+
     _brlAPI.acceptKeys(brlapi.rangeType_command, keySet)
 
 def init(callback=None, tty=7):
diff --git a/src/orca/default.py b/src/orca/default.py
index bf597af..60a5195 100644
--- a/src/orca/default.py
+++ b/src/orca/default.py
@@ -1,6 +1,6 @@
 # Orca
 #
-# Copyright 2004-2008 Sun Microsystems Inc.
+# Copyright 2004-2009 Sun Microsystems Inc.
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Library General Public
@@ -25,7 +25,7 @@ for GTK."""
 __id__        = "$Id$"
 __version__   = "$Revision$"
 __date__      = "$Date$"
-__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc."
+__copyright__ = "Copyright (c) 2005-2009 Sun Microsystems Inc."
 __license__   = "LGPL"
 
 import locale
@@ -693,6 +693,22 @@ class Script(script.Script):
                 #
                 _("Processes a cursor routing key."))
 
+        self.inputEventHandlers["processBrailleCutBeginHandler"] = \
+            input_event.InputEventHandler(
+                Script.processBrailleCutBegin,
+                # Translators: this is used to indicate the start point
+                # of a text selection.
+                #
+                _("Marks the beginning of a text selection."))
+
+        self.inputEventHandlers["processBrailleCutLineHandler"] = \
+            input_event.InputEventHandler(
+                Script.processBrailleCutLine,
+                # Translators: this is used to indicate the end point
+                # of a text selection.
+                #
+                _("Marks the end of a text selection."))
+
         self.inputEventHandlers["enterLearnModeHandler"] = \
             input_event.InputEventHandler(
                 Script.enterLearnMode,
@@ -1961,6 +1977,10 @@ class Script(script.Script):
             self.inputEventHandlers["contractedBrailleHandler"]
         brailleBindings[braille.brlapi.KEY_CMD_ROUTE]   = \
             self.inputEventHandlers["processRoutingKeyHandler"]
+        brailleBindings[braille.brlapi.KEY_CMD_CUTBEGIN] = \
+            self.inputEventHandlers["processBrailleCutBeginHandler"]
+        brailleBindings[braille.brlapi.KEY_CMD_CUTLINE] = \
+            self.inputEventHandlers["processBrailleCutLineHandler"]
 
         return brailleBindings
 
@@ -4968,6 +4988,7 @@ class Script(script.Script):
 
     def setContractedBraille(self, inputEvent=None):
         """Toggles contracted braille."""
+
         braille.setContractedBraille(inputEvent)
         return True
 
@@ -4977,6 +4998,35 @@ class Script(script.Script):
         braille.processRoutingKey(inputEvent)
         return True
 
+    def processBrailleCutBegin(self, inputEvent=None):
+        """Clears the selection and moves the caret offset in the currently
+        active text area.
+        """
+
+        obj, caretOffset = braille.getCaretContext(inputEvent)
+
+        if caretOffset >= 0:
+            self.clearTextSelection(obj)
+            self.setCaretOffset(obj, caretOffset)
+
+        return True
+
+    def processBrailleCutLine(self, inputEvent=None):
+        """Extends the text selection in the currently active text
+        area and also copies the selected text to the system clipboard."""
+
+        obj, caretOffset = braille.getCaretContext(inputEvent)
+
+        if caretOffset >= 0:
+            self.adjustTextSelection(obj, caretOffset)
+            texti = obj.queryText()
+            startOffset, endOffset = texti.getSelection(0)
+            import gtk
+            clipboard = gtk.clipboard_get()
+            clipboard.set_text(texti.getText(startOffset, endOffset))
+
+        return True
+
     def leftClickReviewItem(self, inputEvent=None):
         """Performs a left mouse button click on the current item."""
 
@@ -7596,6 +7646,36 @@ class Script(script.Script):
         """
         print "\a"
 
+    def clearTextSelection(self, obj):
+        """Clears the text selection if the object supports it."""
+        try:
+            texti = obj.queryText()
+        except:
+            return
+
+        for i in range(0, texti.getNSelections()):
+            texti.removeSelection(0)
+
+    def adjustTextSelection(self, obj, offset):
+        """Adjusts the end point of a text selection"""
+        try:
+            texti = obj.queryText()
+        except:
+            return
+
+        if not texti.getNSelections():
+            caretOffset = texti.caretOffset
+            startOffset = min(offset, caretOffset)
+            endOffset = max(offset, caretOffset)
+            texti.addSelection(startOffset, endOffset)
+        else:
+            startOffset, endOffset = texti.getSelection(0)
+            if offset < startOffset:
+                startOffset = offset
+            else:
+                endOffset = offset
+            texti.setSelection(0, startOffset, endOffset)
+
     def setCaretOffset(self, obj, offset):
         """Set the caret offset on a given accessible. Similar to
         Accessible.setCaretOffset()



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]