1# Orca
2#
3# Copyright 2011. Orca Team.
4# Author: Joanmarie Diggs <joanmarie.diggs@gmail.com>
5#
6# This library is free software; you can redistribute it and/or
7# modify it under the terms of the GNU Lesser General Public
8# License as published by the Free Software Foundation; either
9# version 2.1 of the License, or (at your option) any later version.
10#
11# This library is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14# Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public
17# License along with this library; if not, write to the
18# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
19# Boston MA  02110-1301 USA.
20
21__id__        = "$Id$"
22__version__   = "$Revision$"
23__date__      = "$Date$"
24__copyright__ = "Copyright (c) 2011. Orca Team."
25__license__   = "LGPL"
26
27import importlib
28import pyatspi
29
30from . import debug
31from . import orca_state
32from .scripts import apps, toolkits
33
34class ScriptManager:
35
36    def __init__(self):
37        debug.println(debug.LEVEL_INFO, 'SCRIPT MANAGER: Initializing', True)
38        self.appScripts = {}
39        self.toolkitScripts = {}
40        self.customScripts = {}
41        self._appModules = apps.__all__
42        self._toolkitModules = toolkits.__all__
43        self._defaultScript = None
44        self._scriptPackages = \
45            ["orca-scripts",
46             "orca.scripts",
47             "orca.scripts.apps",
48             "orca.scripts.toolkits"]
49        self._appNames = \
50            {'Firefox': 'Mozilla',
51             'Icedove': 'Thunderbird',
52             'Nereid': 'Banshee',
53             'empathy-chat': 'empathy',
54             'gnome-calculator': 'gcalctool',
55             'gtk-window-decorator': 'switcher',
56             'marco': 'switcher',
57             'mate-notification-daemon': 'notification-daemon',
58             'metacity': 'switcher',
59             'pluma': 'gedit',
60            }
61
62        self.setActiveScript(None, "__init__")
63        self._desktop = pyatspi.Registry.getDesktop(0)
64        self._active = False
65        debug.println(debug.LEVEL_INFO, 'SCRIPT MANAGER: Initialized', True)
66
67    def activate(self):
68        """Called when this script manager is activated."""
69
70        debug.println(debug.LEVEL_INFO, 'SCRIPT MANAGER: Activating', True)
71        self._defaultScript = self.getScript(None)
72        self._defaultScript.registerEventListeners()
73        self.setActiveScript(self._defaultScript, "activate")
74        self._active = True
75        debug.println(debug.LEVEL_INFO, 'SCRIPT MANAGER: Activated', True)
76
77    def deactivate(self):
78        """Called when this script manager is deactivated."""
79
80        debug.println(debug.LEVEL_INFO, 'SCRIPT MANAGER: Dectivating', True)
81        if self._defaultScript:
82            self._defaultScript.deregisterEventListeners()
83        self._defaultScript = None
84        self.setActiveScript(None, "deactivate")
85        self.appScripts = {}
86        self.toolkitScripts = {}
87        self.customScripts = {}
88        self._active = False
89        debug.println(debug.LEVEL_INFO, 'SCRIPT MANAGER: Deactivated', True)
90
91    def getModuleName(self, app):
92        """Returns the module name of the script to use for application app."""
93
94        try:
95            appAndNameExist = app is not None and app.name != ''
96        except (LookupError, RuntimeError):
97            appAndNameExist = False
98            msg = 'ERROR: %s no longer exists' % app
99            debug.println(debug.LEVEL_INFO, msg, True)
100
101        if not appAndNameExist:
102            return None
103
104        name = app.name
105        altNames = list(self._appNames.keys())
106        if name.endswith(".py") or name.endswith(".bin"):
107            name = name.split('.')[0]
108        elif name.startswith("org.") or name.startswith("com."):
109            name = name.split('.')[-1]
110
111        names = [n for n in altNames if n.lower() == name.lower()]
112        if names:
113            name = self._appNames.get(names[0])
114        else:
115            for nameList in (self._appModules, self._toolkitModules):
116                names = [n for n in nameList if n.lower() == name.lower()]
117                if names:
118                    name = names[0]
119                    break
120
121        msg = 'SCRIPT MANAGER: mapped %s to %s' % (app.name, name)
122        debug.println(debug.LEVEL_INFO, msg, True)
123        return name
124
125    def _toolkitForObject(self, obj):
126        """Returns the name of the toolkit associated with obj."""
127
128        name = ''
129        if obj:
130            try:
131                attributes = obj.getAttributes()
132            except (LookupError, RuntimeError):
133                pass
134            else:
135                attrs = dict([attr.split(':', 1) for attr in attributes])
136                name = attrs.get('toolkit', '')
137
138        return name
139
140    def _scriptForRole(self, obj):
141        try:
142            role = obj.getRole()
143        except:
144            return ''
145
146        if role == pyatspi.ROLE_TERMINAL:
147            return 'terminal'
148
149        return ''
150
151    def _newNamedScript(self, app, name):
152        """Attempts to locate and load the named module. If successful, returns
153        a script based on this module."""
154
155        if not (app and name):
156            return None
157
158        script = None
159        for package in self._scriptPackages:
160            moduleName = '.'.join((package, name))
161            try:
162                module = importlib.import_module(moduleName)
163            except ImportError:
164                continue
165            except OSError:
166                debug.examineProcesses()
167
168            debug.println(debug.LEVEL_INFO, 'SCRIPT MANAGER: Found %s' % moduleName, True)
169            try:
170                if hasattr(module, 'getScript'):
171                    script = module.getScript(app)
172                else:
173                    script = module.Script(app)
174                break
175            except:
176                debug.printException(debug.LEVEL_INFO)
177                msg = 'ERROR: Could not load %s' % moduleName
178                debug.println(debug.LEVEL_INFO, msg, True)
179
180        return script
181
182    def _createScript(self, app, obj=None):
183        """For the given application, create a new script instance."""
184
185        moduleName = self.getModuleName(app)
186        script = self._newNamedScript(app, moduleName)
187        if script:
188            return script
189
190        objToolkit = self._toolkitForObject(obj)
191        script = self._newNamedScript(app, objToolkit)
192        if script:
193            return script
194
195        try:
196            toolkitName = getattr(app, "toolkitName", None)
197        except (LookupError, RuntimeError):
198            msg = 'ERROR: Exception getting toolkitName for: %s' % app
199            debug.println(debug.LEVEL_INFO, msg, True)
200        else:
201            if app and toolkitName:
202                script = self._newNamedScript(app, toolkitName)
203
204        if not script:
205            script = self.getDefaultScript(app)
206            msg = 'SCRIPT MANAGER: Default script created'
207            debug.println(debug.LEVEL_INFO, msg, True)
208
209        return script
210
211    def getDefaultScript(self, app=None):
212        if not app and self._defaultScript:
213            return self._defaultScript
214
215        from .scripts import default
216        script = default.Script(app)
217
218        if not app:
219            self._defaultScript = script
220
221        return script
222
223    def sanityCheckScript(self, script):
224        if not self._active:
225            return script
226
227        try:
228            appInDesktop = script.app in self._desktop
229        except:
230            appInDesktop = False
231
232        if appInDesktop:
233            return script
234
235        msg = "WARNING: %s is not in the registry's desktop" % script.app
236        debug.println(debug.LEVEL_INFO, msg, True)
237
238        newScript = self._getScriptForAppReplicant(script.app)
239        if newScript:
240            msg = "SCRIPT MANAGER: Script for app replicant found: %s" % newScript
241            debug.println(debug.LEVEL_INFO, msg, True)
242            return newScript
243
244        msg = "WARNING: Failed to get a replacement script for %s" % script.app
245        debug.println(debug.LEVEL_INFO, msg, True)
246        return script
247
248    def getScript(self, app, obj=None, sanityCheck=False):
249        """Get a script for an app (and make it if necessary).  This is used
250        instead of a simple calls to Script's constructor.
251
252        Arguments:
253        - app: the Python app
254
255        Returns an instance of a Script.
256        """
257
258        customScript = None
259        appScript = None
260        toolkitScript = None
261
262        roleName = self._scriptForRole(obj)
263        if roleName:
264            customScripts = self.customScripts.get(app, {})
265            customScript = customScripts.get(roleName)
266            if not customScript:
267                customScript = self._newNamedScript(app, roleName)
268                customScripts[roleName] = customScript
269            self.customScripts[app] = customScripts
270
271        objToolkit = self._toolkitForObject(obj)
272        if objToolkit:
273            toolkitScripts = self.toolkitScripts.get(app, {})
274            toolkitScript = toolkitScripts.get(objToolkit)
275            if not toolkitScript:
276                toolkitScript = self._createScript(app, obj)
277                toolkitScripts[objToolkit] = toolkitScript
278            self.toolkitScripts[app] = toolkitScripts
279
280        try:
281            if not app:
282                appScript = self.getDefaultScript()
283            elif app in self.appScripts:
284                appScript = self.appScripts[app]
285            else:
286                appScript = self._createScript(app, None)
287                self.appScripts[app] = appScript
288        except:
289            msg = 'WARNING: Exception getting app script.'
290            debug.printException(debug.LEVEL_ALL)
291            debug.println(debug.LEVEL_WARNING, msg, True)
292            appScript = self.getDefaultScript()
293
294        if customScript:
295            return customScript
296
297        try:
298            role = obj.getRole()
299        except:
300            forceAppScript = False
301        else:
302            forceAppScript = role in [pyatspi.ROLE_FRAME, pyatspi.ROLE_STATUS_BAR]
303
304        # Only defer to the toolkit script for this object if the app script
305        # is based on a different toolkit.
306        if toolkitScript and not forceAppScript \
307           and not issubclass(appScript.__class__, toolkitScript.__class__):
308            return toolkitScript
309
310        if app and sanityCheck:
311            appScript = self.sanityCheckScript(appScript)
312
313        return appScript
314
315    def setActiveScript(self, newScript, reason=None):
316        """Set the new active script.
317
318        Arguments:
319        - newScript: the new script to be made active.
320        """
321
322        if orca_state.activeScript == newScript:
323            return
324
325        if orca_state.activeScript:
326            orca_state.activeScript.deactivate()
327
328        orca_state.activeScript = newScript
329        if not newScript:
330            return
331
332        newScript.activate()
333        msg = 'SCRIPT MANAGER: Setting active script: %s (reason=%s)' % \
334              (newScript.name, reason)
335        debug.println(debug.LEVEL_INFO, msg, True)
336
337    def _getScriptForAppReplicant(self, app):
338        if not self._active:
339            return None
340
341        def _pid(app):
342            try:
343                return app.get_process_id()
344            except:
345                msg = "SCRIPT MANAGER: Exception getting pid for %s" % app
346                debug.println(debug.LEVEL_INFO, msg, True)
347                return -1
348
349        def _isValidApp(app):
350            try:
351                return a in self._desktop
352            except:
353                msg = "SCRIPT MANAGER: Exception seeing if %s is in desktop" % app
354                debug.println(debug.LEVEL_INFO, msg, True)
355                return False
356
357        pid = _pid(app)
358        if pid == -1:
359            return None
360
361        items = self.appScripts.items()
362        for a, script in items:
363            if a != app and _pid(a) == pid and _isValidApp(a):
364                return script
365
366        return None
367
368    def reclaimScripts(self):
369        """Compares the list of known scripts to the list of known apps,
370        deleting any scripts as necessary.
371        """
372
373        appList = list(self.appScripts.keys())
374        try:
375            appList = [a for a in appList if a is not None and a not in self._desktop]
376        except:
377            debug.printException(debug.LEVEL_FINEST)
378            return
379
380        for app in appList:
381            msg = "SCRIPT MANAGER: %s is no longer in registry's desktop" % app
382            debug.println(debug.LEVEL_INFO, msg, True)
383
384            appScript = self.appScripts.pop(app)
385            newScript = self._getScriptForAppReplicant(app)
386            if newScript:
387                msg = "SCRIPT MANAGER: Script for app replicant found: %s" % newScript
388                debug.println(debug.LEVEL_INFO, msg, True)
389
390                attrs = appScript.getTransferableAttributes()
391                for attr, value in attrs.items():
392                    msg = "SCRIPT MANAGER: Setting %s to %s" % (attr, value)
393                    debug.println(debug.LEVEL_INFO, msg, True)
394                    setattr(newScript, attr, value)
395
396            del appScript
397
398            try:
399                toolkitScripts = self.toolkitScripts.pop(app)
400            except KeyError:
401                pass
402            else:
403                for toolkitScript in toolkitScripts.values():
404                    del toolkitScript
405
406            try:
407                customScripts = self.customScripts.pop(app)
408            except KeyError:
409                pass
410            else:
411                for customScript in customScripts.values():
412                    del customScript
413
414            del app
415
416_manager = ScriptManager()
417
418def getManager():
419    return _manager
420