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