1# Orca
2#
3# Copyright 2010 Consorcio Fernando de los Rios.
4# Author: Javier Hernandez Antunez <jhernandez@emergya.es>
5# Author: Alejandro Leiva <aleiva@emergya.es>
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"""Settings manager module. This will load/save user settings from a
23defined settings backend."""
24
25__id__        = "$Id$"
26__version__   = "$Revision$"
27__date__      = "$Date$"
28__copyright__ = "Copyright (c) 2010 Consorcio Fernando de los Rios."
29__license__   = "LGPL"
30
31import imp
32import importlib
33import os
34from gi.repository import Gio, GLib
35
36from . import debug
37from . import orca_i18n
38from . import script_manager
39from . import settings
40from . import pronunciation_dict
41from .acss import ACSS
42from .keybindings import KeyBinding
43
44try:
45    _proxy = Gio.DBusProxy.new_for_bus_sync(
46        Gio.BusType.SESSION,
47        Gio.DBusProxyFlags.NONE,
48        None,
49        'org.a11y.Bus',
50        '/org/a11y/bus',
51        'org.freedesktop.DBus.Properties',
52        None)
53except:
54    _proxy = None
55
56_scriptManager = script_manager.getManager()
57
58class SettingsManager(object):
59    """Settings backend manager. This class manages orca user's settings
60    using different backends"""
61    _instance = None
62
63    def __new__(cls, *args, **kwargs):
64        if '__instance' not in vars(cls):
65            cls.__instance = object.__new__(cls, *args, **kwargs)
66        return cls.__instance
67
68    def __init__(self, backend='json'):
69        """Initialize a SettingsManager Object.
70        If backend isn't defined then uses default backend, in this
71        case json-backend.
72        backend parameter can use the follow values:
73        backend='json'
74        """
75
76        debug.println(debug.LEVEL_INFO, 'SETTINGS MANAGER: Initializing', True)
77
78        self.backendModule = None
79        self._backend = None
80        self.profile = None
81        self.backendName = backend
82        self._prefsDir = None
83
84        # Dictionaries for store the default values
85        # The keys and values are defined at orca.settings
86        #
87        self.defaultGeneral = {}
88        self.defaultPronunciations = {}
89        self.defaultKeybindings = {}
90
91        # Dictionaries that store the key:value pairs which values are
92        # different from the current profile and the default ones
93        #
94        self.profileGeneral = {}
95        self.profilePronunciations = {}
96        self.profileKeybindings = {}
97
98        # Dictionaries that store the current settings.
99        # They are result to overwrite the default values with
100        # the ones from the current active profile
101        self.general = {}
102        self.pronunciations = {}
103        self.keybindings = {}
104
105        self._activeApp = ""
106        self._appGeneral = {}
107        self._appPronunciations = {}
108        self._appKeybindings = {}
109
110        if not self._loadBackend():
111            raise Exception('SettingsManager._loadBackend failed.')
112
113        self.customizedSettings = {}
114        self._customizationCompleted = False
115
116        # For handling the currently-"classic" application settings
117        self.settingsPackages = ["app-settings"]
118
119        debug.println(debug.LEVEL_INFO, 'SETTINGS MANAGER: Initialized', True)
120
121    def activate(self, prefsDir=None, customSettings={}):
122        debug.println(debug.LEVEL_INFO, 'SETTINGS MANAGER: Activating', True)
123
124        self.customizedSettings.update(customSettings)
125        self._prefsDir = prefsDir \
126            or os.path.join(GLib.get_user_data_dir(), "orca")
127
128        # Load the backend and the default values
129        self._backend = self.backendModule.Backend(self._prefsDir)
130        self._setDefaultGeneral()
131        self._setDefaultPronunciations()
132        self._setDefaultKeybindings()
133        self.general = self.defaultGeneral.copy()
134        if not self.isFirstStart():
135            self.general.update(self._backend.getGeneral())
136        self.pronunciations = self.defaultPronunciations.copy()
137        self.keybindings = self.defaultKeybindings.copy()
138
139        # If this is the first time we launch Orca, there is no user settings
140        # yet, so we need to create the user config directories and store the
141        # initial default settings
142        #
143        self._createDefaults()
144
145        debug.println(debug.LEVEL_INFO, 'SETTINGS MANAGER: Activated', True)
146
147        # Set the active profile and load its stored settings
148        msg = 'SETTINGS MANAGER: Current profile is %s' % self.profile
149        debug.println(debug.LEVEL_INFO, msg, True)
150
151        if self.profile is None:
152            self.profile = self.general.get('startingProfile')[1]
153            msg = 'SETTINGS MANAGER: Current profile is now %s' % self.profile
154            debug.println(debug.LEVEL_INFO, msg, True)
155
156        self.setProfile(self.profile)
157
158    def _loadBackend(self):
159        """Load specific backend for manage user settings"""
160
161        try:
162            backend = '.backends.%s_backend' % self.backendName
163            self.backendModule = importlib.import_module(backend, 'orca')
164            return True
165        except:
166            return False
167
168    def _createDefaults(self):
169        """Let the active backend to create the initial structure
170        for storing the settings and save the default ones from
171        orca.settings"""
172        def _createDir(dirName):
173            if not os.path.isdir(dirName):
174                os.makedirs(dirName)
175
176        # Set up the user's preferences directory
177        # ($XDG_DATA_HOME/orca by default).
178        #
179        orcaDir = self._prefsDir
180        _createDir(orcaDir)
181
182        # Set up $XDG_DATA_HOME/orca/orca-scripts as a Python package
183        #
184        orcaScriptDir = os.path.join(orcaDir, "orca-scripts")
185        _createDir(orcaScriptDir)
186        initFile = os.path.join(orcaScriptDir, "__init__.py")
187        if not os.path.exists(initFile):
188            os.close(os.open(initFile, os.O_CREAT, 0o700))
189
190        orcaSettingsDir = os.path.join(orcaDir, "app-settings")
191        _createDir(orcaSettingsDir)
192
193        orcaSoundsDir = os.path.join(orcaDir, "sounds")
194        _createDir(orcaSoundsDir)
195
196        # Set up $XDG_DATA_HOME/orca/orca-customizations.py empty file and
197        # define orcaDir as a Python package.
198        initFile = os.path.join(orcaDir, "__init__.py")
199        if not os.path.exists(initFile):
200            os.close(os.open(initFile, os.O_CREAT, 0o700))
201
202        userCustomFile = os.path.join(orcaDir, "orca-customizations.py")
203        if not os.path.exists(userCustomFile):
204            os.close(os.open(userCustomFile, os.O_CREAT, 0o700))
205
206        if self.isFirstStart():
207            self._backend.saveDefaultSettings(self.defaultGeneral,
208                                              self.defaultPronunciations,
209                                              self.defaultKeybindings)
210
211    def _setDefaultPronunciations(self):
212        """Get the pronunciations by default from orca.settings"""
213        self.defaultPronunciations = {}
214
215    def _setDefaultKeybindings(self):
216        """Get the keybindings by default from orca.settings"""
217        self.defaultKeybindings = {}
218
219    def _setDefaultGeneral(self):
220        """Get the general settings by default from orca.settings"""
221        self._getCustomizedSettings()
222        self.defaultGeneral = {}
223        for key in settings.userCustomizableSettings:
224            value = self.customizedSettings.get(key)
225            if value is None:
226                try:
227                    value = getattr(settings, key)
228                except:
229                    pass
230            self.defaultGeneral[key] = value
231
232    def _getCustomizedSettings(self):
233        if self._customizationCompleted:
234            return self.customizedSettings
235
236        originalSettings = {}
237        for key, value in settings.__dict__.items():
238            originalSettings[key] = value
239
240        self._customizationCompleted = self._loadUserCustomizations()
241
242        for key, value in originalSettings.items():
243            customValue = settings.__dict__.get(key)
244            if value != customValue:
245                self.customizedSettings[key] = customValue
246
247    def _loadUserCustomizations(self):
248        """Attempt to load the user's orca-customizations. Returns a boolean
249        indicating our success at doing so, where success is measured by the
250        likelihood that the results won't be different if we keep trying."""
251
252        success = False
253        pathList = [self._prefsDir]
254        try:
255            msg = "SETTINGS MANAGER: Attempt to load orca-customizations "
256            (fileHnd, moduleName, desc) = \
257                imp.find_module("orca-customizations", pathList)
258            msg += "from %s " % moduleName
259            imp.load_module("orca-customizations", fileHnd, moduleName, desc)
260        except ImportError:
261            success = True
262            msg += "failed due to ImportError. Giving up."
263        except AttributeError:
264            return False
265        else:
266            msg += "succeeded."
267            fileHnd.close()
268            success = True
269        debug.println(debug.LEVEL_ALL, msg, True)
270        return success
271
272    def getPrefsDir(self):
273        return self._prefsDir
274
275    def setSetting(self, settingName, settingValue):
276        self._setSettingsRuntime({settingName:settingValue})
277
278    def getSetting(self, settingName):
279        return getattr(settings, settingName, None)
280
281    def getVoiceLocale(self, voice='default'):
282        voices = self.getSetting('voices')
283        v = ACSS(voices.get(voice, {}))
284        lang = v.getLocale()
285        dialect = v.getDialect()
286        if dialect and len(str(dialect)) == 2:
287            lang = "%s_%s" % (lang, dialect.upper())
288        return lang
289
290    def _loadProfileSettings(self, profile=None):
291        """Get from the active backend all the settings for the current
292        profile and store them in the object's attributes.
293        A profile can be passed as a parameter. This could be useful for
294        change from one profile to another."""
295
296        msg = 'SETTINGS MANAGER: Loading settings for %s profile' % profile
297        debug.println(debug.LEVEL_INFO, msg, True)
298
299        if profile is None:
300            profile = self.profile
301        self.profileGeneral = self.getGeneralSettings(profile) or {}
302        self.profilePronunciations = self.getPronunciations(profile) or {}
303        self.profileKeybindings = self.getKeybindings(profile) or {}
304
305        msg = 'SETTINGS MANAGER: Settings for %s profile loaded' % profile
306        debug.println(debug.LEVEL_INFO, msg, True)
307
308    def _mergeSettings(self):
309        """Update the changed values on the profile settings
310        over the current and active settings"""
311
312        msg = 'SETTINGS MANAGER: Merging settings.'
313        debug.println(debug.LEVEL_INFO, msg, True)
314
315        self.profileGeneral.update(self._appGeneral)
316        self.profilePronunciations.update(self._appPronunciations)
317        self.profileKeybindings.update(self._appKeybindings)
318
319        self.general.update(self.profileGeneral)
320        self.pronunciations.update(self.profilePronunciations)
321        self.keybindings.update(self.profileKeybindings)
322
323        msg = 'SETTINGS MANAGER: Settings merged.'
324        debug.println(debug.LEVEL_INFO, msg, True)
325
326    def _enableAccessibility(self):
327        """Enables the GNOME accessibility flag.  Users need to log out and
328        then back in for this to take effect.
329
330        Returns True if an action was taken (i.e., accessibility was not
331        set prior to this call).
332        """
333
334        alreadyEnabled = self.isAccessibilityEnabled()
335        if not alreadyEnabled:
336            self.setAccessibility(True)
337
338        return not alreadyEnabled
339
340    def isAccessibilityEnabled(self):
341        msg = 'SETTINGS MANAGER: Checking if accessibility is enabled.'
342        debug.println(debug.LEVEL_INFO, msg, True)
343
344        msg = 'SETTINGS MANAGER: Accessibility enabled: '
345        if not _proxy:
346            rv = False
347            msg += 'Error (no proxy)'
348        else:
349            rv = _proxy.Get('(ss)', 'org.a11y.Status', 'IsEnabled')
350            msg += str(rv)
351
352        debug.println(debug.LEVEL_INFO, msg, True)
353        return rv
354
355    def setAccessibility(self, enable):
356        msg = 'SETTINGS MANAGER: Attempting to set accessibility to %s.' % enable
357        debug.println(debug.LEVEL_INFO, msg, True)
358
359        if not _proxy:
360            msg = 'SETTINGS MANAGER: Error (no proxy)'
361            debug.println(debug.LEVEL_INFO, msg, True)
362            return False
363
364        vEnable = GLib.Variant('b', enable)
365        _proxy.Set('(ssv)', 'org.a11y.Status', 'IsEnabled', vEnable)
366
367        msg = 'SETTINGS MANAGER: Finished setting accessibility to %s.' % enable
368        debug.println(debug.LEVEL_INFO, msg, True)
369
370    def isScreenReaderServiceEnabled(self):
371        """Returns True if the screen reader service is enabled. Note that
372        this does not necessarily mean that Orca (or any other screen reader)
373        is running at the moment."""
374
375        msg = 'SETTINGS MANAGER: Is screen reader service enabled? '
376
377        if not _proxy:
378            rv = False
379            msg += 'Error (no proxy)'
380        else:
381            rv = _proxy.Get('(ss)', 'org.a11y.Status', 'ScreenReaderEnabled')
382            msg += str(rv)
383
384        debug.println(debug.LEVEL_INFO, msg, True)
385        return rv
386
387    def setStartingProfile(self, profile=None):
388        if profile is None:
389            profile = settings.profile
390        self._backend._setProfileKey('startingProfile', profile)
391
392    def getProfile(self):
393        return self.profile
394
395    def setProfile(self, profile='default', updateLocale=False):
396        """Set a specific profile as the active one.
397        Also the settings from that profile will be loading
398        and updated the current settings with them."""
399
400        msg = 'SETTINGS MANAGER: Setting profile to: %s' % profile
401        debug.println(debug.LEVEL_INFO, msg, True)
402
403        oldVoiceLocale = self.getVoiceLocale('default')
404
405        self.profile = profile
406        self._loadProfileSettings(profile)
407        self._mergeSettings()
408        self._setSettingsRuntime(self.general)
409
410        if not updateLocale:
411            return
412
413        newVoiceLocale = self.getVoiceLocale('default')
414        if oldVoiceLocale != newVoiceLocale:
415            orca_i18n.setLocaleForNames(newVoiceLocale)
416            orca_i18n.setLocaleForMessages(newVoiceLocale)
417            orca_i18n.setLocaleForGUI(newVoiceLocale)
418
419        msg = 'SETTINGS MANAGER: Profile set to: %s' % profile
420        debug.println(debug.LEVEL_INFO, msg, True)
421
422    def removeProfile(self, profile):
423        self._backend.removeProfile(profile)
424
425    def _setSettingsRuntime(self, settingsDict):
426        msg = 'SETTINGS MANAGER: Setting runtime settings.'
427        debug.println(debug.LEVEL_INFO, msg, True)
428
429        for key, value in settingsDict.items():
430            setattr(settings, str(key), value)
431        self._getCustomizedSettings()
432        for key, value in self.customizedSettings.items():
433            setattr(settings, str(key), value)
434
435        msg = 'SETTINGS MANAGER: Runtime settings set.'
436        debug.println(debug.LEVEL_INFO, msg, True)
437
438    def _setPronunciationsRuntime(self, pronunciationsDict):
439        pronunciation_dict.pronunciation_dict = {}
440        for key, value in pronunciationsDict.values():
441            if key and value:
442                pronunciation_dict.setPronunciation(key, value)
443
444    def getGeneralSettings(self, profile='default'):
445        """Return the current general settings.
446        Those settings comes from updating the default settings
447        with the profiles' ones"""
448        return self._backend.getGeneral(profile)
449
450    def getPronunciations(self, profile='default'):
451        """Return the current pronunciations settings.
452        Those settings comes from updating the default settings
453        with the profiles' ones"""
454        return self._backend.getPronunciations(profile)
455
456    def getKeybindings(self, profile='default'):
457        """Return the current keybindings settings.
458        Those settings comes from updating the default settings
459        with the profiles' ones"""
460        return self._backend.getKeybindings(profile)
461
462    def _setProfileGeneral(self, general):
463        """Set the changed general settings from the defaults' ones
464        as the profile's."""
465
466        msg = 'SETTINGS MANAGER: Setting general settings for profile'
467        debug.println(debug.LEVEL_INFO, msg, True)
468
469        self.profileGeneral = {}
470
471        for key, value in general.items():
472            if key in ['startingProfile', 'activeProfile']:
473                continue
474            elif key == 'profile':
475                self.profileGeneral[key] = value
476            elif value != self.defaultGeneral.get(key):
477                self.profileGeneral[key] = value
478            elif self.general.get(key) != value:
479                self.profileGeneral[key] = value
480
481        msg = 'SETTINGS MANAGER: General settings for profile set'
482        debug.println(debug.LEVEL_INFO, msg, True)
483
484    def _setProfilePronunciations(self, pronunciations):
485        """Set the changed pronunciations settings from the defaults' ones
486        as the profile's."""
487
488        msg = 'SETTINGS MANAGER: Setting pronunciation settings for profile.'
489        debug.println(debug.LEVEL_INFO, msg, True)
490
491        self.profilePronunciations = self.defaultPronunciations.copy()
492        self.profilePronunciations.update(pronunciations)
493
494        msg = 'SETTINGS MANAGER: Pronunciation settings for profile set.'
495        debug.println(debug.LEVEL_INFO, msg, True)
496
497    def _setProfileKeybindings(self, keybindings):
498        """Set the changed keybindings settings from the defaults' ones
499        as the profile's."""
500
501        msg = 'SETTINGS MANAGER: Setting keybindings settings for profile.'
502        debug.println(debug.LEVEL_INFO, msg, True)
503
504        self.profileKeybindings = self.defaultKeybindings.copy()
505        self.profileKeybindings.update(keybindings)
506
507        msg = 'SETTINGS MANAGER: Keybindings settings for profile set.'
508        debug.println(debug.LEVEL_INFO, msg, True)
509
510    def _saveAppSettings(self, appName, general, pronunciations, keybindings):
511        appGeneral = {}
512        profileGeneral = self.getGeneralSettings(self.profile)
513        for key, value in general.items():
514            if value != profileGeneral.get(key):
515                appGeneral[key] = value
516
517        appPronunciations = {}
518        profilePronunciations = self.getPronunciations(self.profile)
519        for key, value in pronunciations.items():
520            if value != profilePronunciations.get(key):
521                appPronunciations[key] = value
522
523        appKeybindings = {}
524        profileKeybindings = self.getKeybindings(self.profile)
525        for key, value in keybindings.items():
526            if value != profileKeybindings.get(key):
527                appKeybindings[key] = value
528
529        self._backend.saveAppSettings(appName,
530                                      self.profile,
531                                      appGeneral,
532                                      appPronunciations,
533                                      appKeybindings)
534
535    def saveSettings(self, script, general, pronunciations, keybindings):
536        """Save the settings provided for the script provided."""
537
538        msg = 'SETTINGS MANAGER: Saving settings for %s (app: %s)' % (script, script.app)
539        debug.println(debug.LEVEL_INFO, msg, True)
540
541        app = script.app
542        if app:
543            self._saveAppSettings(app.name, general, pronunciations, keybindings)
544            return
545
546        # Assign current profile
547        _profile = general.get('profile', settings.profile)
548        currentProfile = _profile[1]
549
550        self.profile = currentProfile
551
552        # Elements that need to stay updated in main configuration.
553        self.defaultGeneral['startingProfile'] = general.get('startingProfile',
554                                                              _profile)
555
556        self._setProfileGeneral(general)
557        self._setProfilePronunciations(pronunciations)
558        self._setProfileKeybindings(keybindings)
559
560        msg = 'SETTINGS MANAGER: Saving for backend %s' % self._backend
561        debug.println(debug.LEVEL_INFO, msg, True)
562
563        self._backend.saveProfileSettings(self.profile,
564                                          self.profileGeneral,
565                                          self.profilePronunciations,
566                                          self.profileKeybindings)
567
568        msg = 'SETTINGS MANAGER: Settings for %s (app: %s) saved' % (script, script.app)
569        debug.println(debug.LEVEL_INFO, msg, True)
570
571        return self._enableAccessibility()
572
573    def _adjustBindingTupleValues(self, bindingTuple):
574        """Converts the values of bindingTuple into KeyBinding-ready values."""
575
576        keysym, mask, mods, clicks = bindingTuple
577        if not keysym:
578            bindingTuple = ('', 0, 0, 0)
579        else:
580            bindingTuple = (keysym, int(mask), int(mods), int(clicks))
581
582        return bindingTuple
583
584    def overrideKeyBindings(self, script, scriptKeyBindings):
585        keybindingsSettings = self.profileKeybindings
586        for handlerString, bindingTuples in keybindingsSettings.items():
587            handler = script.inputEventHandlers.get(handlerString)
588            if not handler:
589                continue
590
591            scriptKeyBindings.removeByHandler(handler)
592            for bindingTuple in bindingTuples:
593                bindingTuple = self._adjustBindingTupleValues(bindingTuple)
594                keysym, mask, mods, clicks = bindingTuple
595                newBinding = KeyBinding(keysym, mask, mods, handler, clicks)
596                scriptKeyBindings.add(newBinding)
597
598        return scriptKeyBindings
599
600    def isFirstStart(self):
601        """Check if the firstStart key is True or false"""
602        return self._backend.isFirstStart()
603
604    def setFirstStart(self, value=False):
605        """Set firstStart. This user-configurable setting is primarily
606        intended to serve as an indication as to whether or not initial
607        configuration is needed."""
608        self._backend.setFirstStart(value)
609
610    def availableProfiles(self):
611        """Get available profiles from active backend"""
612
613        return self._backend.availableProfiles()
614
615    def getAppSetting(self, app, settingName, fallbackOnDefault=True):
616        if not app:
617            return None
618
619        appPrefs = self._backend.getAppSettings(app.name)
620        profiles = appPrefs.get('profiles', {})
621        profilePrefs = profiles.get(self.profile, {})
622        general = profilePrefs.get('general', {})
623        appSetting = general.get(settingName)
624        if appSetting is None and fallbackOnDefault:
625            general = self._backend.getGeneral(self.profile)
626            appSetting = general.get(settingName)
627
628        return appSetting
629
630    def loadAppSettings(self, script):
631        """Load the users application specific settings for an app.
632
633        Arguments:
634        - script: the current active script.
635        """
636
637        if not (script and script.app):
638            return
639
640        for key in self._appPronunciations.keys():
641            self.pronunciations.pop(key)
642
643        prefs = self._backend.getAppSettings(script.app.name)
644        profiles = prefs.get('profiles', {})
645        profilePrefs = profiles.get(self.profile, {})
646
647        self._appGeneral = profilePrefs.get('general', {})
648        self._appKeybindings = profilePrefs.get('keybindings', {})
649        self._appPronunciations = profilePrefs.get('pronunciations', {})
650        self._activeApp = script.app.name
651
652        self._loadProfileSettings()
653        self._mergeSettings()
654        self._setSettingsRuntime(self.general)
655        self._setPronunciationsRuntime(self.pronunciations)
656        script.keyBindings = self.overrideKeyBindings(script, script.getKeyBindings())
657
658_manager = SettingsManager()
659
660def getManager():
661    return _manager
662