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