diff --git a/src/orca/Makefile.am b/src/orca/Makefile.am index 7041dc3b2a67f5ee96bba824f0e45012378fecdb..89d79b896c98b95e3aca53e3db17b8453346c94a 100644 --- a/src/orca/Makefile.am +++ b/src/orca/Makefile.am @@ -40,6 +40,7 @@ orca_python_PYTHON = \ messages.py \ mouse_review.py \ notification_messages.py \ + object_navigator.py \ object_properties.py \ orca.py \ orca_gtkbuilder.py \ diff --git a/src/orca/cmdnames.py b/src/orca/cmdnames.py index 445015800b62ab71c0f40c5d368a3ba3c4099474..d052fe49e608180766765ca8812993a12b10b42f 100644 --- a/src/orca/cmdnames.py +++ b/src/orca/cmdnames.py @@ -1079,3 +1079,23 @@ VISITED_LINK_NEXT = _("Go to next visited link") # Translators: this is for navigating among visited links in a document. VISITED_LINK_LIST = _("Display a list of visited links") + +# Translators: the object navigator allows users to explore UI objects presented as a hierarchy. +NAVIGATOR_UP = _("Moves the object navigator to the parent of the object with navigator focus.") + +# Translators: the object navigator allows users to explore UI objects presented as a hierarchy. +NAVIGATOR_DOWN = _("Moves the object navigator to the first child of the object with navigator focus.") + +# Translators: the object navigator allows users to explore UI objects presented as a hierarchy. +NAVIGATOR_NEXT = _("Moves the object navigator to the next sibling of the object with navigator focus.") + +# Translators: the object navigator allows users to explore UI objects presented as a hierarchy. +NAVIGATOR_PREVIOUS = _("Moves the object navigator to the previous sibling of the object with navigator focus.") + +# Translators: the object navigator allows users to explore UI objects presented as a hierarchy. +# Users are also able to perform the usual action on an object encountered during navigation, which is usually a click, press, or causing it to grab keyboard focus. +NAVIGATOR_PERFORM_ACTION = _("Performs the usual action on the object with navigator focus.") + +# Translators: the object navigator allows users to explore UI objects presented as a hierarchy. +# This hierarchy can be simplified, and the simplification can be toggled on and off. +NAVIGATOR_TOGGLE_SIMPLIFIED = _("Toggles whether object navigation should be simplified.") diff --git a/src/orca/common_keyboardmap.py b/src/orca/common_keyboardmap.py index 246af2e18c939876f5f46eaf9ad7314660bec431..05ee37206e9a2f17ba5e8f6abcd2c3e82a971827 100644 --- a/src/orca/common_keyboardmap.py +++ b/src/orca/common_keyboardmap.py @@ -71,6 +71,18 @@ keymap = ( ("Up", defaultModifierMask, ORCA_SHIFT_MODIFIER_MASK, "whereAmISelectionHandler"), + ("Up", defaultModifierMask, ORCA_CTRL_MODIFIER_MASK, + "navigatorUpHandler"), + ("Down", defaultModifierMask, ORCA_CTRL_MODIFIER_MASK, + "navigatorDownHandler"), + ("Right", defaultModifierMask, ORCA_CTRL_MODIFIER_MASK, + "navigatorNextHandler"), + ("Left", defaultModifierMask, ORCA_CTRL_MODIFIER_MASK, + "navigatorPreviousHandler"), + ("Return", defaultModifierMask, ORCA_CTRL_MODIFIER_MASK, + "navigatorActionHandler"), + ("S", defaultModifierMask, ORCA_CTRL_MODIFIER_MASK, + "navigatorToggleSimplifyHandler"), ##################################################################### # # diff --git a/src/orca/messages.py b/src/orca/messages.py index 5186e76f51d700708e8cb9e70929b934001330f3..c0b191e2d679e394808505abb10cfe7d81c61999 100644 --- a/src/orca/messages.py +++ b/src/orca/messages.py @@ -1751,6 +1751,32 @@ MOUSE_REVIEW_ENABLED = _("Mouse review enabled.") # from getting these objects. NAVIGATION_DIALOG_ERROR = _("Error: Could not create list of objects.") +# Translators: the object navigator allows users to explore UI objects presented as a hierarchy. +# This message is spoken when the current node in the hierarchy has no children. +NAVIGATOR_NO_CHILDREN = _("No children.") + +# Translators: the object navigator allows users to explore UI objects presented as a hierarchy. +# This message is spoken when the current node in the hierarchy has no next sibling. +NAVIGATOR_NO_NEXT = _("No next.") + +# Translators: the object navigator allows users to explore UI objects presented as a hierarchy. +# This message is spoken when the current node in the hierarchy has no parent. +NAVIGATOR_NO_PARENT = _("No parent.") + +# Translators: the object navigator allows users to explore UI objects presented as a hierarchy. +# This message is spoken when the current node in the hierarchy has no previous sibling. +NAVIGATOR_NO_PREVIOUS = _("No previous.") + +# Translators: the object navigator allows users to explore UI objects presented as a hierarchy. +# This hirerarchy can be simplified to aid with navigation. +# This message is spoken when the simplified view is enabled. +NAVIGATOR_SIMPLIFIED_ENABLED = _("Simplified navigation enabled.") + +# Translators: the object navigator allows users to explore UI objects presented as a hierarchy. +# This hirerarchy can be simplified to aid with navigation. +# This message is spoken when the simplified view is disabled. +NAVIGATOR_SIMPLIFIED_DISABLED = _("Simplified navigation DISABLED.") + # Translators: This message describes a list item in a document. Nesting level # is how "deep" the item is (e.g., a level of 2 represents a list item inside a # list that's inside another list). diff --git a/src/orca/object_navigator.py b/src/orca/object_navigator.py new file mode 100644 index 0000000000000000000000000000000000000000..702cf82aa6b232b0988ce56089e223427ae45471 --- /dev/null +++ b/src/orca/object_navigator.py @@ -0,0 +1,149 @@ +# Orca +# +# Copyright 2020 The Orca Team +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. + +"""Object for maintaining the state of the object navigator.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2020 The Orca Team" +__license__ = "LGPL" + +import pyatspi + +from . import messages +from . import orca_state + +class ObjectNavigator: + def __init__(self): + self.navigatorFocus=None + self.lastNavigatorFocus=None + self.lastLocusOfFocus=None + self.simplify=True + self.inclusionList = [pyatspi.ROLE_PARAGRAPH] + self.exclusionList = [pyatspi.ROLE_SECTION] + + def _isEmpty(self, object): + """ Method for determining whether an object should be excluded from simple navigation. """ + if object.role in self.inclusionList: + return False + return orca_state.activeScript.utilities.isLayoutOnly(object) or object.getRole() in orca_state.activeScript.utilities.getCellRoles() or object.role in self.exclusionList + + def _children(self, object): + """ Returns a list of children for object, taking simple navigation into account if enabled. """ + if object.childCount==0: # if we have no children, return the empty list + return [] + if not self.simplify: # just return the usual children + return list(object) + children = list(object) # the normal list of children + resultingChildren = [] # the list we're constructing + for child in children: + if self._isEmpty(child): # add the children of empty objects to our list of children + resultingChildren.extend(self._children(child)) + else: + resultingChildren.append(child) + return resultingChildren + + def _parent(self, object): + """ Returns the parent for object, taking simple navigation into account if enabled. """ + if not self.simplify: # return the usual parent + return object.parent + while object.parent is not None and self._isEmpty(object.parent): # move up until we find an object with a suitible parent + object = object.parent + return object.parent + + def setNavigatorFocus(self, object): + """ Method for changing the navigator focus. Placing this in a method allow us to keep track of the previous focus. """ + self.lastNavigatorFocus=self.navigatorFocus + self.navigatorFocus=object + + def update(self): + """ Updates the navigator focus to the last keyboard focus, if it is different from before. """ + if orca_state.locusOfFocus==self.lastLocusOfFocus: + return + self.navigatorFocus=orca_state.locusOfFocus + self.lastLocusOfFocus=orca_state.locusOfFocus + + def present(self): + """ Presents the current navigator focus to the user. """ + generatorMap={} + if self.lastNavigatorFocus: + generatorMap['priorObj']=self.lastNavigatorFocus + utterances = orca_state.activeScript.speechGenerator.generateSpeech(self.navigatorFocus, **generatorMap) + if len(utterances)>0: + orca_state.activeScript.presentItemsInSpeech(utterances) + else: + orca_state.activeScript.speakMessage(self.navigatorFocus.getLocalizedRoleName()) + orca_state.activeScript.displayBrailleForObject(self.navigatorFocus) + + def up(self): + """ Moves the navigator focus to the parent of the current focus. """ + self.update() + parent = self._parent(self.navigatorFocus) + if parent is not None: + self.setNavigatorFocus(parent) + self.present() + else: + orca_state.activeScript.speakMessage(messages.NAVIGATOR_NO_PARENT) + + def down(self): + """ Moves the navigator focus to the first child of the current focus. """ + self.update() + children = self._children(self.navigatorFocus) + if len(children)>0: + self.setNavigatorFocus(children[0]) + self.present() + else: + orca_state.activeScript.speakMessage(messages.NAVIGATOR_NO_CHILDREN) + + def next(self): + """ Moves the navigator focus to the next sibling of the current focus. """ + self.update() + parent = self._parent(self.navigatorFocus) + if parent==None: + orca_state.activeScript.speakMessage(messages.NAVIGATOR_NO_NEXT) + return + siblings = self._children(parent) + if self.navigatorFocus in siblings: + index = siblings.index(self.navigatorFocus) + if index0: + self.setNavigatorFocus(siblings[index-1]) + self.present() + else: + orca_state.activeScript.speakMessage(messages.NAVIGATOR_NO_PREVIOUS) + else: + self.setNavigatorFocus(parent) diff --git a/src/orca/scripts/default.py b/src/orca/scripts/default.py index 4881fa8ef0d250ab7e7716a7fa3e33e83dad264b..d3f45451a90c35c094c82bcd2b57bf02da61285d 100644 --- a/src/orca/scripts/default.py +++ b/src/orca/scripts/default.py @@ -43,6 +43,7 @@ import orca.guilabels as guilabels import orca.input_event as input_event import orca.keybindings as keybindings import orca.messages as messages +import orca.object_navigator as object_navigator import orca.orca as orca import orca.orca_gui_commandlist as commandlist import orca.orca_state as orca_state @@ -91,6 +92,7 @@ class Script(script.Script): self.targetCursorCell = None self.justEnteredFlatReviewMode = False + self.navigator = object_navigator.ObjectNavigator() self.digits = '0123456789' self.whitespace = ' \t\n\r\v\f' @@ -519,6 +521,31 @@ class Script(script.Script): input_event.InputEventHandler( Script.presentSizeAndPosition, cmdnames.PRESENT_SIZE_AND_POSITION) + self.inputEventHandlers["navigatorUpHandler"]=\ + input_event.InputEventHandler( + Script.navigatorUp, + cmdnames.NAVIGATOR_UP) + self.inputEventHandlers["navigatorDownHandler"]=\ + input_event.InputEventHandler( + Script.navigatorDown, + cmdnames.NAVIGATOR_DOWN) + self.inputEventHandlers["navigatorNextHandler"]=\ + input_event.InputEventHandler( + Script.navigatorNext, + cmdnames.NAVIGATOR_NEXT) + self.inputEventHandlers["navigatorPreviousHandler"]=\ + input_event.InputEventHandler( + Script.navigatorPrevious, + cmdnames.NAVIGATOR_PREVIOUS) + self.inputEventHandlers["navigatorActionHandler"]=\ + input_event.InputEventHandler( + Script.navigatorAction, + cmdnames.NAVIGATOR_PERFORM_ACTION) + self.inputEventHandlers["navigatorToggleSimplifyHandler"]=\ + input_event.InputEventHandler( + Script.navigatorToggleSimplify, + cmdnames.NAVIGATOR_TOGGLE_SIMPLIFIED) + self.inputEventHandlers.update(notification_messages.inputEventHandlers) @@ -2616,6 +2643,7 @@ class Script(script.Script): orca.setLocusOfFocus(event, obj) + def onShowingChanged(self, event): """Callback for object:state-changed:showing accessibility events.""" @@ -4445,3 +4473,47 @@ class Script(script.Script): brief = messages.SIZE_AND_POSITION_BRIEF % (width, height, x, y) self.presentMessage(full, brief) return True + + def navigatorUp(self, inputEvent): + """ Moves the object navigator to the parent of the current object. """ + self.navigator.up() + return True + + def navigatorDown(self, inputEvent): + """ Moves the object navigator to the first child of the current object. """ + self.navigator.down() + return True + + def navigatorNext(self, inputEvent): + """ Moves the object navigator to the next object. """ + self.navigator.next() + return True + + def navigatorPrevious(self, inputEvent): + """ Moves the object navigator to the previous object. """ + self.navigator.previous() + return True + + def navigatorAction(self, inputEvent): + """ Performs an action (a click, press or focus) on the object with navigator focus. """ + obj = self.navigator.navigatorFocus + if not obj: + return False + if eventsynthesizer.clickActionOn(obj): + return True + if eventsynthesizer.pressActionOn(obj): + return True + if eventsynthesizer.grabFocusOn(obj): + return True + return False + + def navigatorToggleSimplify(self, inputEvent): + """ Handles toggling simplified navigation with the object navigator. """ + self.navigator.simplify = not self.navigator.simplify + if self.navigator.simplify: + self.speakMessage(messages.NAVIGATOR_SIMPLIFIED_ENABLED) + self.displayBrailleMessage(messages.NAVIGATOR_SIMPLIFIED_ENABLED, flashTime=settings.brailleFlashTime) + else: + self.speakMessage(messages.NAVIGATOR_SIMPLIFIED_DISABLED) + self.displayBrailleMessage(messages.NAVIGATOR_SIMPLIFIED_DISABLED, flashTime=settings.brailleFlashTime) + return True