%PDF- %PDF-
Direktori : /lib/python3/dist-packages/orca/scripts/toolkits/Chromium/ |
Current File : //lib/python3/dist-packages/orca/scripts/toolkits/Chromium/script_utilities.py |
# Orca # # Copyright 2018-2019 Igalia, S.L. # # Author: Joanmarie Diggs <jdiggs@igalia.com> # # 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. """Custom script utilities for Chromium""" __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" __copyright__ = "Copyright (c) 2018-2019 Igalia, S.L." __license__ = "LGPL" import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi import re from orca import debug from orca import focus_manager from orca.scripts import web from orca.ax_object import AXObject from orca.ax_utilities import AXUtilities class Utilities(web.Utilities): def __init__(self, script): super().__init__(script) self._isStaticTextLeaf = {} self._isPseudoElement = {} self._isListItemMarker = {} self._topLevelObject = {} def clearCachedObjects(self): super().clearCachedObjects() self._isStaticTextLeaf = {} self._isPseudoElement = {} self._isListItemMarker = {} self._topLevelObject = {} def isStaticTextLeaf(self, obj): if not (obj and self.inDocumentContent(obj)): return super().isStaticTextLeaf(obj) if AXObject.get_child_count(obj): return False if self.isListItemMarker(obj): return False rv = self._isStaticTextLeaf.get(hash(obj)) if rv is not None: return rv roles = [Atspi.Role.STATIC, Atspi.Role.TEXT] rv = AXObject.get_role(obj) in roles and self._getTag(obj) in (None, "", "br") if rv: tokens = ["CHROMIUM:", obj, "believed to be static text leaf"] debug.printTokens(debug.LEVEL_INFO, tokens, True) self._isStaticTextLeaf[hash(obj)] = rv return rv def isPseudoElement(self, obj): if not (obj and self.inDocumentContent(obj)): return super().isPseudoElement(obj) rv = self._isPseudoElement.get(hash(obj)) if rv is not None: return rv rv = self._getTag(obj) in ["<pseudo:before>", "<pseudo:after>"] if rv: tokens = ["CHROMIUM:", obj, "believed to be pseudo element"] debug.printTokens(debug.LEVEL_INFO, tokens, True) self._isPseudoElement[hash(obj)] = rv return rv def isListItemMarker(self, obj): if not (obj and self.inDocumentContent(obj)): return False rv = self._isListItemMarker.get(hash(obj)) if rv is not None: return rv rv = False parent = AXObject.get_parent(obj) if AXUtilities.is_list_item(parent): tag = self._getTag(obj) if tag == "::marker": rv = True elif tag: rv = False elif AXObject.get_child_count(parent) > 1: rv = AXObject.get_child(parent, 0) == obj else: rv = AXObject.get_name(obj) != self.displayedText(parent) self._isListItemMarker[hash(obj)] = rv return rv def isMenuInCollapsedSelectElement(self, obj): if not AXUtilities.is_menu(obj): return False parent = AXObject.get_parent(obj) if self._getTag(parent) != 'select': return False return not AXUtilities.is_expanded(parent) def treatAsMenu(self, obj): # Unlike other apps and toolkits, submenus in Chromium have the menu item # role rather than the menu role, but we can identify them as submenus via # the has-popup state. return AXUtilities.is_menu_item(obj) and AXUtilities.has_popup(obj) def isPopupMenuForCurrentItem(self, obj): # When a submenu is closed, it has role menu item. But when that submenu # is opened/expanded, a menu with that same name appears. It would be # nice if there were a connection (parent/child or an accessible relation) # between the two.... return self.treatAsMenu(focus_manager.getManager().get_locus_of_focus()) \ and super().isPopupMenuForCurrentItem(obj) def isFrameForPopupMenu(self, obj): # The ancestry of a popup menu appears to be a menu bar (even though # one is not actually showing) contained in a nameless frame. It would # be nice if these things were pruned from the accessibility tree.... if not AXUtilities.is_frame(obj): return False if AXObject.get_name(obj): return False if AXObject.get_child_count(obj) != 1: return False return AXUtilities.is_menu_bar(AXObject.get_child(obj, 0)) def isTopLevelMenu(self, obj): return AXUtilities.is_menu(obj) and self.isFrameForPopupMenu(self.topLevelObject(obj)) def popupMenuForFrame(self, obj): if not self.isFrameForPopupMenu(obj): return None menu = AXObject.find_descendant(obj, AXUtilities.is_menu) tokens = ["CHROMIUM: Popup menu for", obj, ":", menu] debug.printTokens(debug.LEVEL_INFO, tokens, True) return menu def topLevelObject(self, obj, useFallbackSearch=False): if not obj: return None result = super().topLevelObject(obj) if AXObject.get_role(result) in self._topLevelRoles(): if not self.isFindContainer(result): return result else: parent = AXObject.get_parent(result) tokens = ["CHROMIUM: Top level object for", obj, "is", parent] debug.printTokens(debug.LEVEL_INFO, tokens, True) return parent cached = self._topLevelObject.get(hash(obj)) if cached is not None: return cached tokens = ["CHROMIUM: WARNING: Top level object for", obj, "is", result] debug.printTokens(debug.LEVEL_INFO, tokens, True) # The only (known) object giving us a broken ancestry is the omnibox popup. if not (AXUtilities.is_list_item(obj or AXUtilities.is_list_box(obj))): return result listbox = obj if AXUtilities.is_list_item(obj): listbox = AXObject.get_parent(listbox) if listbox is None: return result # The listbox sometimes claims to be a redundant object rather than a listbox. # Clearing the AT-SPI2 cache seems to be the trigger. if not AXUtilities.is_list_box(listbox): if AXUtilities.is_redundant_object(listbox): tokens = ["CHROMIUM: WARNING: Suspected bogus role on listbox", listbox] debug.printTokens(debug.LEVEL_INFO, tokens, True) else: return result autocomplete = self.autocompleteForPopup(listbox) if autocomplete: result = self.topLevelObject(autocomplete) tokens = ["CHROMIUM: Top level object for", autocomplete, "is", result] debug.printTokens(debug.LEVEL_INFO, tokens, True) self._topLevelObject[hash(obj)] = result return result def autocompleteForPopup(self, obj): relation = AXObject.get_relation(obj, Atspi.RelationType.POPUP_FOR) if not relation: return None target = relation.get_target(0) if AXUtilities.is_autocomplete(target): return target return None def isBrowserAutocompletePopup(self, obj): if not obj or self.inDocumentContent(obj): return False return self.autocompleteForPopup(obj) is not None def isRedundantAutocompleteEvent(self, event): if not AXUtilities.is_autocomplete(event.source): return False if event.type.startswith("object:text-caret-moved"): lastKey, mods = self.lastKeyAndModifiers() if lastKey in ["Down", "Up"]: return True return False def setCaretPosition(self, obj, offset, documentFrame=None): super().setCaretPosition(obj, offset, documentFrame) # TODO - JD: Is this hack still needed? link = AXObject.find_ancestor(obj, AXUtilities.is_link) if link is not None: tokens = ["CHROMIUM: HACK: Grabbing focus on", obj, "'s ancestor", link] debug.printTokens(debug.LEVEL_INFO, tokens, True) AXObject.grab_focus(link) def handleAsLiveRegion(self, event): if not super().handleAsLiveRegion(event): return False if not event.type.startswith("object:children-changed:add"): return True # At least some of the time, we're getting text insertion events immediately # followed by children-changed events to tell us that the object whose text # changed is now being added to the accessibility tree. Furthermore the # additions are not always coming to us in presentational order, whereas # the text changes appear to be. So most of the time, we can ignore the # children-changed events. Except for when we can't. if AXUtilities.is_table(event.any_data): return True msg = "CHROMIUM: Event is believed to be redundant live region notification" debug.printMessage(debug.LEVEL_INFO, msg, True) return False def getFindResultsCount(self, root=None): root = root or self._findContainer if not root: return "" statusBars = AXUtilities.find_all_status_bars(root) if len(statusBars) != 1: return "" bar = statusBars[0] # TODO - JD: Is this still needed? AXObject.clear_cache(bar, False, "Ensuring we have correct name for find results.") if len(re.findall(r"\d+", AXObject.get_name(bar))) == 2: return AXObject.get_name(bar) return "" def isFindContainer(self, obj): if not obj or self.inDocumentContent(obj): return False if obj == self._findContainer: return True if not AXUtilities.is_dialog(obj): return False result = self.getFindResultsCount(obj) if result: tokens = ["CHROMIUM:", obj, "believed to be find-in-page container (", result, ")"] debug.printTokens(debug.LEVEL_INFO, tokens, True) self._findContainer = obj return True # When there are no results due to the absence of a search term, the status # bar lacks a name. When there are no results due to lack of match, the name # of the status bar is "No results" (presumably localized). Therefore fall # back on the widgets. TODO: This would be far easier if Chromium gave us an # object attribute we could look for.... if len(AXUtilities.find_all_entries(obj)) != 1: tokens = ["CHROMIUM:", obj, "not believed to be find-in-page container (entry count)"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return False if len(AXUtilities.find_all_push_buttons(obj)) != 3: tokens = ["CHROMIUM:", obj, "not believed to be find-in-page container (button count)"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return False if len(AXUtilities.find_all_separators(obj)) != 1: tokens = ["CHROMIUM:", obj, "not believed to be find-in-page container (separator count)"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return False tokens = ["CHROMIUM:", obj, "believed to be find-in-page container (accessibility tree)"] debug.printTokens(debug.LEVEL_INFO, tokens, True) self._findContainer = obj return True def inFindContainer(self, obj=None): obj = obj or focus_manager.getManager().get_locus_of_focus() if not (AXUtilities.is_entry(obj) or AXUtilities.is_push_button(obj)): return False if self.inDocumentContent(obj): return False result = self.isFindContainer(AXObject.find_ancestor(obj, AXUtilities.is_dialog)) if result: tokens = ["CHROMIUM:", obj, "believed to be find-in-page widget"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return result def isHidden(self, obj): if not super().isHidden(obj): return False if self.isMenuInCollapsedSelectElement(obj): return False return True def findAllDescendants(self, root, includeIf=None, excludeIf=None): if not root: return [] # Don't bother if the root is a 'pre' or 'code' element. Those often have # nothing but a TON of static text leaf nodes, which we want to ignore. if self._getTag(root) in ('pre', 'code'): tokens = ["CHROMIUM: Returning 0 descendants for pre/code", root] debug.printTokens(debug.LEVEL_INFO, tokens, True) return [] return super().findAllDescendants(root, includeIf, excludeIf) def _shouldCalculatePositionAndSetSize(self, obj): # Chromium calculates posinset and setsize for description lists based on the # number of terms present. If we want to present the number of values associated # with a given term, we need to work those values out ourselves. if self.isDescriptionListDescription(obj): return True if self.inDocumentContent(obj): return super()._shouldCalculatePositionAndSetSize(obj) # Chromium has accessible menu items which are not focusable and therefore do not # have a posinset and setsize calculated. But they may claim to be the selected # item when an accessible child is selected (e.g. "zoom" when "+" or "-" gains focus. # Normally we calculate posinset and setsize when the application hasn't provided it. # We don't want to do that in the case of menu items like "zoom" because our result # will not jibe with the values of its siblings. Thus if a sibling has a value, # assume that the missing attributes are missing on purpose. for sibling in AXObject.iter_children(AXObject.get_parent(obj)): if self.getPositionInSet(sibling) is not None: tokens = ["CHROMIUM:", obj, "'s sibling", sibling, "has posinset."] debug.printTokens(debug.LEVEL_INFO, tokens, True) return False return True