File: magFocusTracker.py

package info (click to toggle)
pyatspi 2.14.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 1,792 kB
  • ctags: 859
  • sloc: sh: 11,803; python: 2,124; makefile: 68
file content (290 lines) | stat: -rwxr-xr-x 9,771 bytes parent folder | download | duplicates (7)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
#!/usr/bin/python

# magFocusTracker
#
# Copyright 2009 Sun Microsystems Inc.
# Copyright 2010 Willie Walker
# Copyright 2011-2012 Igalia, S. L.
# Copyright 2011-2012 Inclusive Design Research Centre, OCAD University
#
# 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.
#
# * Contributor: Joanie Diggs <diggs@igalia.com>
# * Contributor: Joseph Scheuhammer <clown@alum.mit.edu>

"""Proof-of-concept standalone application that shows how:
1. to track keyboard focus and the caret using AT-SPI events, and
2. use D-Bus to drive the magnifier to insure the tracked object is
   within the magnified view.
"""

__copyright__ = \
  "Copyright (c) 2009 Sun Microsystems Inc." \
  "Copyright (c) 2010 Willie Walker" \
  "Copyright (c) 2011-2012 Igalia, S.L." \
  "Copyright (c) 2011-2012 Inclusive Design Research Centre, OCAD University"
__license__   = "LGPL"

import dbus
import pyatspi
import sys
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import Gdk
from gi.repository.Gio import Settings

_screenWidth = 0
_screenHeight = 0
_magnifier = None
_zoomer = None

class RoiHandler:
    """For handling D-Bus calls to zoomRegion.getRoi() asynchronously"""

    def __init__(self, left=0, top=0, width=0, height=0, centerX=0, centerY=0,
                 edgeMarginX=0, edgeMarginY=0):
        self.left = left
        self.top = top
        self.width = width
        self.height = height
        self.centerX = centerX
        self.centerY = centerY
        self.edgeMarginX = edgeMarginX
        self.edgeMarginY = edgeMarginY

    def setRoiCenter(self, reply):
        """Given a region of interest, put that at the center of the magnifier.

        Arguments:
        - reply:  an array defining a rectangle [left, top, right, bottom]
        """
        roiWidth = reply[2] - reply[0]
        roiHeight = reply[3] - reply[1]
        if self.width > roiWidth:
            self.centerX = self.left
        if self.height > roiHeight:
            self.centerY = self.top
        _setROICenter(self.centerX, self.centerY)

    def setRoiCursorPush(self, reply):
        """Given a region of interest, nudge it if the caret or control is not
        visible.

        Arguments:
        - reply:  an array defining a rectangle [left, top, right, bottom]
        """

        roiLeft = reply[0]
        roiTop = reply[1]
        roiWidth = reply[2] - roiLeft
        roiHeight = reply[3] - roiTop
        leftOfROI = (self.left - self.edgeMarginX) <= roiLeft
        rightOfROI = \
            (self.left + self.width + self.edgeMarginX) >= (roiLeft + roiWidth)
        aboveROI = (self.top - self.edgeMarginY)  <= roiTop
        belowROI = \
            (self.top + self.height + self.edgeMarginY) >= (roiTop + roiHeight)

        x1 = roiLeft
        x2 = roiLeft + roiWidth
        y1 = roiTop
        y2 = roiTop + roiHeight

        if leftOfROI:
            x1 = max(0, self.left - self.edgeMarginX)
            x2 = x1 + roiWidth
        elif rightOfROI:
            self.left = min(_screenWidth, self.left + self.edgeMarginX)
            if self.width > roiWidth:
                x1 = self.left
                x2 = x1 + roiWidth
            else:
                x2 = self.left + self.width
                x1 = x2 - roiWidth

        if aboveROI:
            y1 = max(0, self.top - self.edgeMarginY)
            y2 = y1 + roiHeight
        elif belowROI:
            self.top = min(_screenHeight, self.top + self.edgeMarginY)
            if self.height > roiHeight:
                y1 = self.top
                y2 = y1 + roiHeight
            else:
                y2 = self.top + self.height
                y1 = y2 - roiHeight

        _setROICenter((x1 + x2) / 2, (y1 + y2) / 2)

    def setRoiCenterErr(self, error):
        _dbusCallbackError('_setROICenter()', error)

    def setRoiCursorPushErr(self, error):
        _dbusCallbackError('_setROICursorPush()', error)

    def magnifyAccessibleErr(self, error):
        _dbusCallbackError('magnifyAccessible()', error)

def _dbusCallbackError(funcName, error):
    """Log D-Bus errors

    Arguments:
    - funcName: The name of the gsmag function that made the D-Bus call.
    - error: The error that D-Bus returned.
    """
    logLine = funcName + ' failed: ' + str(error)
    debug.println(debug.LEVEL_WARNING, logLine)

def _setROICenter(x, y):
    """Centers the region of interest around the given point.

    Arguments:
    - x: integer in unzoomed system coordinates representing x component
    - y: integer in unzoomed system coordinates representing y component
    """
    _zoomer.shiftContentsTo(x, y, ignore_reply=True)

def _setROICursorPush(x, y, width, height):
    """Nudges the ROI if the caret or control is not visible.

    Arguments:
    - x: integer in unzoomed system coordinates representing x component
    - y: integer in unzoomed system coordinates representing y component
    - width: integer in unzoomed system coordinates representing the width
    - height: integer in unzoomed system coordinates representing the height
    """

    roiPushHandler = RoiHandler(x, y, width, height)
    _zoomer.getRoi(reply_handler=roiPushHandler.setRoiCursorPush,
                   error_handler=roiPushHandler.setRoiCursorPushErr)

def magnifyAccessible(event, obj=None, extents=None):
    """Sets the region of interest to the upper left of the given
    accessible, if it implements the Component interface.  Otherwise,
    does nothing.

    Arguments:
    - event: the Event that caused this to be called
    - obj: the accessible
    """

    if event.type.startswith("object:state-changed") and not event.detail1:
        # This object just became unselected or unfocused, and we're not
        # big on nostalgia.
        return

    obj = obj or event.source

    haveSomethingToMagnify = False

    if extents:
        [x, y, width, height] = extents
        haveSomethingToMagnify = True
    elif event and event.type.startswith("object:text-caret-moved"):
        try:
            text = obj.queryText()
            if text and (text.caretOffset >= 0):
                offset = text.caretOffset
                if offset == text.characterCount:
                    offset -= 1
                [x, y, width, height] = \
                    text.getCharacterExtents(offset, 0)
                haveSomethingToMagnify = (width + height > 0)
        except:
            haveSomethingToMagnify = False

        if haveSomethingToMagnify:
            _setROICursorPush(x, y, width, height)
            return

    if not haveSomethingToMagnify:
        try:
            extents = obj.queryComponent().getExtents(0)
            [x, y, width, height] = \
                [extents.x, extents.y, extents.width, extents.height]
            haveSomethingToMagnify = True
        except:
            haveSomethingToMagnify = False

    if haveSomethingToMagnify:
        _setROICursorPush(x, y, width, height)

def startTracking():
    global _screenWidth
    global _screenHeight
    global _magnifier
    global _zoomer

    if _magnifier and _zoomer:
        screen = Gdk.Screen.get_default()
        _screenWidth = screen.width()
        _screenHeight = screen.height()

        pyatspi.Registry.registerEventListener(magnifyAccessible,
                                               "object:text-caret-moved",
                                               "object:state-changed:focused",
                                               "object:state-changed:selected")

def stopTracking():
    pyatspi.Registry.deregisterEventListener(magnifyAccessible,
                                             "object:text-caret-moved",
                                             "object:state-changed:focused",
                                             "object:state-changed:selected")

def onEnabledChanged(gsetting, key):
    if key != 'screen-magnifier-enabled':
        return

    enabled = gsetting.get_boolean(key)
    if enabled:
        startTracking()
    else:
        stopTracking()

def _initMagDbus():
    global _magnifier
    global _zoomer

    available = False
    try:
        bus = dbus.SessionBus(mainloop=DBusGMainLoop())
        proxy = \
          bus.get_object('org.gnome.Magnifier', '/org/gnome/Magnifier')
        _magnifier = dbus.Interface(proxy, 'org.gnome.Magnifier')
        zoomerPaths = _magnifier.getZoomRegions()
        if zoomerPaths:
            proxy = bus.get_object('org.gnome.Magnifier', zoomerPaths[0])
            _zoomer = dbus.Interface(proxy, 'org.gnome.Magnifier.ZoomRegion')
            available = True
    except:
        available = False

    return available

def main():
    magServiceAvailable = _initMagDbus()
    if magServiceAvailable:
        a11yAppSettings = Settings('org.gnome.desktop.a11y.applications')
        a11yAppSettings.connect('changed', onEnabledChanged)
        if a11yAppSettings.get_boolean('screen-magnifier-enabled'):
            startTracking()
        pyatspi.Registry.start()
    else:
        print('Magnification service not available. Exiting.')

    return 0

if __name__ == "__main__":
    sys.exit(main())