[orca] Add structural navigation commands to move to start and end of current container



commit 7dd90e5691def1d93799299a1cd7e10b833f6e84
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Mon Feb 6 10:56:14 2017 -0500

    Add structural navigation commands to move to start and end of current container
    
    * Shift+comma: start of container
    * comma: end of container

 help/C/commands_structural_navigation.page    |    7 ++
 src/orca/cmdnames.py                          |   10 ++
 src/orca/messages.py                          |   15 +++
 src/orca/script_utilities.py                  |    8 ++
 src/orca/scripts/toolkits/WebKitGtk/script.py |    1 +
 src/orca/scripts/web/script.py                |    3 +-
 src/orca/structural_navigation.py             |  115 ++++++++++++++++++++++++-
 7 files changed, 156 insertions(+), 3 deletions(-)
---
diff --git a/help/C/commands_structural_navigation.page b/help/C/commands_structural_navigation.page
index 4401d28..4dd3dc2 100644
--- a/help/C/commands_structural_navigation.page
+++ b/help/C/commands_structural_navigation.page
@@ -431,6 +431,13 @@
             <keyseq><key>Alt</key><key>Shift</key><key>G</key></keyseq>
           </p>
         </item>
+        <item>
+          <p>
+            Start and end of current container:
+            <keyseq><key>Shift</key><key>comma</key></keyseq> and
+            <keyseq><key>comma</key></keyseq>
+          </p>
+        </item>
       </list>
     </section>
 </page>
diff --git a/src/orca/cmdnames.py b/src/orca/cmdnames.py
index 5e61d1a..7e50b54 100644
--- a/src/orca/cmdnames.py
+++ b/src/orca/cmdnames.py
@@ -817,6 +817,16 @@ COMBO_BOX_PREV = _("Goes to previous combo box.")
 # Translators: this is for navigating among combo boxes in a document.
 COMBO_BOX_NEXT = _("Goes to next combo box.")
 
+# Translators: This string describes a document navigation command which moves
+# to the start of the current container. Examples of containers include tables,
+# lists, and blockquotes.
+CONTAINER_START = _("Goes to start of container.")
+
+# Translators: This string describes a document navigation command which moves
+# to the end of the current container. Examples of containers include tables,
+# lists, and blockquotes.
+CONTAINER_END = _("Goes to end of container.")
+
 # Translators: this is for navigating among combo boxes in a document.
 COMBO_BOX_LIST = _("Displays a list of combo boxes.")
 
diff --git a/src/orca/messages.py b/src/orca/messages.py
index 86b4b32..a2a3abb 100644
--- a/src/orca/messages.py
+++ b/src/orca/messages.py
@@ -386,6 +386,21 @@ DATE_FORMAT_ABBREVIATED_DMY = "%a, %-d %b, %Y"
 DATE_FORMAT_ABBREVIATED_MDY = "%a, %b %-d, %Y"
 DATE_FORMAT_ABBREVIATED_YMD = "%Y. %b %-d, %a."
 
+# Translators: This is for navigating document content by moving to the start
+# or end of a container. Examples of containers include tables, lists, and
+# blockquotes. When moving to the end of a container, Orca attempts to place
+# the caret at the content which follows that container. If this is cannot be
+# done (e.g. because the container is the last element on the page), Orca will
+# instead present this message as an indication that the container was not
+# exited as expected.
+CONTAINER_END = _("End of container.")
+
+# Translators: This is for navigating document content by moving to the start
+# or end of a container. Examples of containers include tables, lists, and
+# blockquotes. If the user attempts to use this command in an object which is
+# not a container, this message will be presented.
+CONTAINER_NOT_IN_A = _("Not in a container.")
+
 # Translators: The "default" button in a dialog box is the button that gets
 # activated when Enter is pressed anywhere within that dialog box.
 DEFAULT_BUTTON_IS = _("Default button is %s")
diff --git a/src/orca/script_utilities.py b/src/orca/script_utilities.py
index 1bce4e0..f9dd001 100644
--- a/src/orca/script_utilities.py
+++ b/src/orca/script_utilities.py
@@ -3264,6 +3264,14 @@ class Utilities:
 
         return obj, offset + 1
 
+    def lastContext(self, root):
+        offset = 0
+        text = self.queryNonEmptyText(root)
+        if text:
+            offset = text.characterCount - 1
+
+        return root, offset
+
     @staticmethod
     def getHyperlinkRange(obj):
         """Returns the start and end indices associated with the embedded
diff --git a/src/orca/scripts/toolkits/WebKitGtk/script.py b/src/orca/scripts/toolkits/WebKitGtk/script.py
index 641728c..6207d98 100644
--- a/src/orca/scripts/toolkits/WebKitGtk/script.py
+++ b/src/orca/scripts/toolkits/WebKitGtk/script.py
@@ -148,6 +148,7 @@ class Script(default.Script):
                 structural_navigation.StructuralNavigation.CHUNK,
                 structural_navigation.StructuralNavigation.CLICKABLE,
                 structural_navigation.StructuralNavigation.COMBO_BOX,
+                structural_navigation.StructuralNavigation.CONTAINER,
                 structural_navigation.StructuralNavigation.ENTRY,
                 structural_navigation.StructuralNavigation.FORM_FIELD,
                 structural_navigation.StructuralNavigation.HEADING,
diff --git a/src/orca/scripts/web/script.py b/src/orca/scripts/web/script.py
index c8ca248..53c0354 100644
--- a/src/orca/scripts/web/script.py
+++ b/src/orca/scripts/web/script.py
@@ -254,6 +254,7 @@ class Script(default.Script):
                 structural_navigation.StructuralNavigation.CHUNK,
                 structural_navigation.StructuralNavigation.CLICKABLE,
                 structural_navigation.StructuralNavigation.COMBO_BOX,
+                structural_navigation.StructuralNavigation.CONTAINER,
                 structural_navigation.StructuralNavigation.ENTRY,
                 structural_navigation.StructuralNavigation.FORM_FIELD,
                 structural_navigation.StructuralNavigation.HEADING,
@@ -801,7 +802,7 @@ class Script(default.Script):
 
     def presentObject(self, obj, **args):
         priorObj = None
-        if self._lastCommandWasCaretNav:
+        if self._lastCommandWasCaretNav or args.get("includeContext"):
             priorObj, priorOffset = self.utilities.getPriorContext()
 
         offset = args.get("offset", 0)
diff --git a/src/orca/structural_navigation.py b/src/orca/structural_navigation.py
index 0cc9b0c..0ebea92 100644
--- a/src/orca/structural_navigation.py
+++ b/src/orca/structural_navigation.py
@@ -295,6 +295,8 @@ class StructuralNavigationObject:
         directions["Down"]  = self.bindings.get("down")
         directions["First"] = self.bindings.get("first")
         directions["Last"]  = self.bindings.get("last")
+        directions["Start"]  = self.bindings.get("start")
+        directions["End"]  = self.bindings.get("end")
 
         for direction in directions:
             binding = directions.get(direction)
@@ -510,6 +512,12 @@ class StructuralNavigationObject:
             else:
                 script.presentMessage(messages.LIVE_REGIONS_OFF)
 
+        def goContainerEdge(script, inputEvent):
+            isStart = direction == "Start"
+            self.structuralNavigation.goEdge(self, isStart)
+
+        if self.objType == StructuralNavigation.CONTAINER:
+            return goContainerEdge
         if self.objType == StructuralNavigation.TABLE_CELL:
             return goCell
         elif self.objType == StructuralNavigation.LIVE_REGION \
@@ -551,6 +559,7 @@ class StructuralNavigation:
     CHUNK           = "chunk"
     CLICKABLE       = "clickable"
     COMBO_BOX       = "comboBox"
+    CONTAINER       = "container"
     ENTRY           = "entry"
     FORM_FIELD      = "formField"
     HEADING         = "heading"
@@ -601,6 +610,16 @@ class StructuralNavigation:
                     pyatspi.ROLE_DOCUMENT_TEXT,
                     pyatspi.ROLE_DOCUMENT_WEB]
 
+    CONTAINER_ROLES = [pyatspi.ROLE_BLOCK_QUOTE,
+                       pyatspi.ROLE_FORM,
+                       pyatspi.ROLE_FOOTER,
+                       pyatspi.ROLE_HEADER,
+                       pyatspi.ROLE_LANDMARK,
+                       pyatspi.ROLE_LIST,
+                       pyatspi.ROLE_PANEL,
+                       pyatspi.ROLE_SECTION,
+                       pyatspi.ROLE_TABLE]
+
     IMAGE_ROLES = [pyatspi.ROLE_IMAGE,
                    pyatspi.ROLE_IMAGE_MAP]
 
@@ -865,6 +884,36 @@ class StructuralNavigation:
         self._objectCache[hash(document)] = cache
         return rv
 
+    def goEdge(self, structuralNavigationObject, isStart, container=None, arg=None):
+        if container is None:
+            obj, offset = self._script.utilities.getCaretContext()
+            container = self.getContainerForObject(obj)
+
+        if container is None or self._script.utilities.isDead(container):
+            structuralNavigationObject.present(None, arg)
+            return
+
+        if isStart:
+            obj, offset = self._script.utilities.nextContext(container, -1)
+            structuralNavigationObject.present(obj, offset)
+            return
+
+        obj, offset = self._script.utilities.lastContext(container)
+        newObj, newOffset = self._script.utilities.nextContext(obj, offset)
+        if not newObj:
+            document = self._script.utilities.getDocumentForObject(obj)
+            newObj = self._script.utilities.getNextObjectInDocument(obj, document)
+
+        newContainer = self.getContainerForObject(newObj)
+        if newObj and newContainer != container:
+            structuralNavigationObject.present(newObj)
+            return
+
+        if obj == container:
+            obj = obj[-1]
+
+        structuralNavigationObject.present(obj, sameContainer=True)
+
     def goObject(self, structuralNavigationObject, isNext, obj=None, arg=None):
         """The method used for navigation among StructuralNavigationObjects
         which are not table cells.
@@ -1004,6 +1053,31 @@ class StructuralNavigation:
 
         return obj
 
+    def _isContainer(self, obj):
+        try:
+            role = obj.getRole()
+        except:
+            return False
+
+        if role not in self.CONTAINER_ROLES:
+            return False
+
+        if role == pyatspi.ROLE_SECTION \
+           and not self._script.utilities.isLandmark(obj) \
+           and not self._script.utilities.isBlockquote(obj):
+            return False
+
+        return self._script.utilities.inDocumentContent(obj)
+
+    def getContainerForObject(self, obj):
+        if not obj:
+            return None
+
+        if self._isContainer(obj):
+            return obj
+
+        return pyatspi.utils.findAncestor(obj, self._isContainer)
+
     def getTableForCell(self, obj):
         """Looks for a table in the ancestry of obj, if obj is not a table.
 
@@ -1143,7 +1217,7 @@ class StructuralNavigation:
         self._script.updateBraille(obj)
         self._script.sayLine(obj)
 
-    def _presentObject(self, obj, offset):
+    def _presentObject(self, obj, offset, includeContext=False):
         """Presents the entire object to the user.
 
         Arguments:
@@ -1157,7 +1231,7 @@ class StructuralNavigation:
         if self._presentWithSayAll(obj, offset):
             return
 
-        self._script.presentObject(obj, offset=offset)
+        self._script.presentObject(obj, offset=offset, includeContext=includeContext)
 
     def _presentWithSayAll(self, obj, offset):
         if self._script.inSayAll() \
@@ -3211,3 +3285,40 @@ class StructuralNavigation:
             return [self._getText(obj), self._getRoleName(obj)]
 
         return guilabels.SN_TITLE_CLICKABLE, columnHeaders, rowData
+
+    ########################
+    #                      #
+    # Containers           #
+    #                      #
+    ########################
+
+    def _containerBindings(self):
+        bindings = {}
+        desc = cmdnames.CONTAINER_START
+        bindings["start"] = ["comma", keybindings.SHIFT_MODIFIER_MASK, desc]
+
+        desc = cmdnames.CONTAINER_END
+        bindings["end"] = ["comma", keybindings.NO_MODIFIER_MASK, desc]
+
+        return bindings
+
+    def _containerCriteria(self, collection, arg=None):
+        return MatchCriteria(collection, roles=self.CONTAINER_ROLES, applyPredicate=True)
+
+    def _containerPredicate(self, obj, arg=None):
+        return self._isContainer(obj)
+
+    def _containerPresentation(self, obj, arg=None, **kwargs):
+        if not obj:
+            self._script.presentMessage(messages.CONTAINER_NOT_IN_A)
+            return
+
+        if kwargs.get("sameContainer"):
+            self._script.presentMessage(messages.CONTAINER_END)
+
+        characterOffset = arg
+        if characterOffset is None:
+            obj, characterOffset = self._getCaretPosition(obj)
+
+        self._setCaretPosition(obj, characterOffset)
+        self._presentObject(obj, characterOffset, True)


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