1# Orca 2# 3# Copyright (C) 2013-2014 Igalia, S.L. 4# 5# Author: Joanmarie Diggs <jdiggs@igalia.com> 6# 7# This library is free software; you can redistribute it and/or 8# modify it under the terms of the GNU Lesser General Public 9# License as published by the Free Software Foundation; either 10# version 2.1 of the License, or (at your option) any later version. 11# 12# This library is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15# Lesser General Public License for more details. 16# 17# You should have received a copy of the GNU Lesser General Public 18# License along with this library; if not, write to the 19# Free Software Foundation, Inc., Franklin Street, Fifth Floor, 20# Boston MA 02110-1301 USA. 21 22__id__ = "$Id$" 23__version__ = "$Revision$" 24__date__ = "$Date$" 25__copyright__ = "Copyright (c) 2013-2014 Igalia, S.L." 26__license__ = "LGPL" 27 28import pyatspi 29import re 30 31import orca.debug as debug 32import orca.script_utilities as script_utilities 33import orca.orca_state as orca_state 34 35class Utilities(script_utilities.Utilities): 36 37 def __init__(self, script): 38 super().__init__(script) 39 self._isComboBoxWithToggleDescendant = {} 40 self._isToggleDescendantOfComboBox = {} 41 self._isTypeahead = {} 42 self._isUselessPanel = {} 43 44 def clearCachedObjects(self): 45 self._isComboBoxWithToggleDescendant = {} 46 self._isToggleDescendantOfComboBox = {} 47 self._isTypeahead = {} 48 self._isUselessPanel = {} 49 50 def infoBar(self, root): 51 isInfoBar = lambda x: x and x.getRole() == pyatspi.ROLE_INFO_BAR 52 return pyatspi.findDescendant(root, isInfoBar) 53 54 def isComboBoxWithToggleDescendant(self, obj): 55 if not (obj and obj.getRole() == pyatspi.ROLE_COMBO_BOX): 56 return False 57 58 rv = self._isComboBoxWithToggleDescendant.get(hash(obj)) 59 if rv is not None: 60 return rv 61 62 isToggle = lambda x: x and x.getRole() == pyatspi.ROLE_TOGGLE_BUTTON 63 64 for child in obj: 65 if child.getRole() != pyatspi.ROLE_FILLER: 66 continue 67 68 toggle = pyatspi.findDescendant(child, isToggle) 69 rv = toggle is not None 70 if toggle: 71 self._isToggleDescendantOfComboBox[hash(toggle)] = True 72 break 73 74 self._isComboBoxWithToggleDescendant[hash(obj)] = rv 75 return rv 76 77 def isToggleDescendantOfComboBox(self, obj): 78 if not (obj and obj.getRole() == pyatspi.ROLE_TOGGLE_BUTTON): 79 return False 80 81 rv = self._isToggleDescendantOfComboBox.get(hash(obj)) 82 if rv is not None: 83 return rv 84 85 isComboBox = lambda x: x and x.getRole() == pyatspi.ROLE_COMBO_BOX 86 comboBox = pyatspi.findAncestor(obj, isComboBox) 87 if comboBox: 88 self._isComboBoxWithToggleDescendant[hash(comboBox)] = True 89 90 rv = comboBox is not None 91 self._isToggleDescendantOfComboBox[hash(obj)] = rv 92 return rv 93 94 def isTypeahead(self, obj): 95 if not obj or self.isDead(obj): 96 return False 97 98 if obj.getRole() != pyatspi.ROLE_TEXT: 99 return False 100 101 rv = self._isTypeahead.get(hash(obj)) 102 if rv is not None: 103 return rv 104 105 parent = obj.parent 106 while parent and self.isLayoutOnly(parent): 107 parent = parent.parent 108 109 rv = parent and parent.getRole() == pyatspi.ROLE_WINDOW 110 self._isTypeahead[hash(obj)] = rv 111 return rv 112 113 def isSearchEntry(self, obj, focusedOnly=False): 114 # Another example of why we need subrole support in ATK and AT-SPI2. 115 try: 116 name = obj.name 117 state = obj.getState() 118 except: 119 return False 120 121 if not (name and state.contains(pyatspi.STATE_SINGLE_LINE)): 122 return False 123 124 if focusedOnly and not state.contains(pyatspi.STATE_FOCUSED): 125 return False 126 127 isIcon = lambda x: x and x.getRole() == pyatspi.ROLE_ICON 128 icons = list(filter(isIcon, [x for x in obj])) 129 if icons: 130 return True 131 132 return False 133 134 def isEntryCompletionPopupItem(self, obj): 135 if obj.getRole() == pyatspi.ROLE_TABLE_CELL: 136 isWindow = lambda x: x and x.getRole() == pyatspi.ROLE_WINDOW 137 window = pyatspi.findAncestor(obj, isWindow) 138 if window: 139 return True 140 141 return False 142 143 def isPopOver(self, obj): 144 try: 145 relations = obj.getRelationSet() 146 except: 147 return False 148 149 for relation in relations: 150 if relation.getRelationType() == pyatspi.RELATION_POPUP_FOR: 151 return True 152 153 return False 154 155 def isUselessPanel(self, obj): 156 if not (obj and obj.getRole() == pyatspi.ROLE_PANEL): 157 return False 158 159 rv = self._isUselessPanel.get(hash(obj)) 160 if rv is not None: 161 return rv 162 163 try: 164 name = obj.name 165 childCount = obj.childCount 166 supportsText = "Text" in pyatspi.listInterfaces(obj) 167 except: 168 rv = True 169 else: 170 rv = not (name or childCount or supportsText) 171 172 self._isUselessPanel[hash(obj)] = rv 173 return rv 174 175 def rgbFromString(self, attributeValue): 176 regex = re.compile(r"rgb|[^\w,]", re.IGNORECASE) 177 string = re.sub(regex, "", attributeValue) 178 red, green, blue = string.split(",") 179 180 return int(red) >> 8, int(green) >> 8, int(blue) >> 8 181 182 def isZombie(self, obj): 183 rv = super().isZombie(obj) 184 if rv and self.isLink(obj) and obj.getIndexInParent() == -1: 185 msg = 'INFO: Hacking around bug 759736 for %s' % obj 186 debug.println(debug.LEVEL_INFO, msg, True) 187 return False 188 189 return rv 190 191 def eventIsCanvasNoise(self, event): 192 if event.source.getRole() != pyatspi.ROLE_CANVAS: 193 return False 194 195 if not orca_state.activeWindow: 196 msg = 'INFO: No active window' 197 debug.println(debug.LEVEL_INFO, msg, True) 198 return False 199 200 topLevel = self.topLevelObject(event.source) 201 if not self.isSameObject(topLevel, orca_state.activeWindow): 202 msg = 'INFO: Event is believed to be canvas noise' 203 debug.println(debug.LEVEL_INFO, msg, True) 204 return True 205 206 return False 207 208 def _adjustPointForObj(self, obj, x, y, coordType): 209 try: 210 singleLine = obj.getState().contains(pyatspi.STATE_SINGLE_LINE) 211 except: 212 singleLine = False 213 214 if not singleLine or "EditableText" not in pyatspi.listInterfaces(obj): 215 return x, y 216 217 text = self.queryNonEmptyText(obj) 218 if not text: 219 return x, y 220 221 objBox = obj.queryComponent().getExtents(coordType) 222 stringBox = text.getRangeExtents(0, text.characterCount, coordType) 223 if self.intersection(objBox, stringBox) != (0, 0, 0, 0): 224 return x, y 225 226 msg = "ERROR: text bounds %s not in obj bounds %s" % (stringBox, objBox) 227 debug.println(debug.LEVEL_INFO, msg, True) 228 229 # This is where the string starts; not the widget. 230 boxX, boxY = stringBox[0], stringBox[1] 231 232 # Window Coordinates should be relative to the window; not the widget. 233 # But broken interface is broken, and this appears to be what is being 234 # exposed. And we need this information to get the widget's x and y. 235 charExtents = text.getCharacterExtents(0, pyatspi.WINDOW_COORDS) 236 if 0 < charExtents[0] < charExtents[2]: 237 boxX -= charExtents[0] 238 if 0 < charExtents[1] < charExtents[3]: 239 boxY -= charExtents[1] 240 241 # The point relative to the widget: 242 relX = x - objBox[0] 243 relY = y - objBox[1] 244 245 # The point relative to our adjusted bounding box: 246 newX = boxX + relX 247 newY = boxY + relY 248 249 msg = "INFO: Adjusted (%i, %i) to (%i, %i)" % (x, y, newX, newY) 250 debug.println(debug.LEVEL_INFO, msg, True) 251 return newX, newY 252