# -*- coding: utf-8 -*- # Copyright © 2008-2010 Chris Jones # Copyright © 2008-2011 Francesco Fumanti # Copyright © 2012 Gerd Kohlberger # Copyright © 2009, 2011-2017 marmuta # # This file is part of Onboard. # # Onboard is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # Onboard 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ File containing Config singleton. """ from __future__ import division, print_function, unicode_literals import os import sys import locale from shutil import copytree from optparse import OptionParser, OptionGroup from Onboard.Version import require_gi_versions require_gi_versions() from gi.repository import Gtk, Gio, GLib from Onboard.WindowUtils import show_confirmation_dialog from Onboard.utils import Version, \ unicode_str, XDGDirs, chmodtree, \ Process, hexcolor_to_rgba, TermColors from Onboard.definitions import DesktopEnvironmentEnum, \ StatusIconProviderEnum, \ InputEventSourceEnum, \ TouchInputEnum, \ LearningBehavior, \ RepositionMethodEnum, \ Handle, DockingEdge, DockingMonitor from Onboard.ConfigUtils import ConfigObject from Onboard.ClickSimulator import CSMousetweaks0, CSMousetweaks1 from Onboard.Exceptions import SchemaError ### Logging ### import logging _logger = logging.getLogger("Config") ############### # gsettings schemas SCHEMA_ONBOARD = "org.onboard" SCHEMA_KEYBOARD = "org.onboard.keyboard" SCHEMA_WINDOW = "org.onboard.window" SCHEMA_WINDOW_LANDSCAPE = "org.onboard.window.landscape" SCHEMA_WINDOW_PORTRAIT = "org.onboard.window.portrait" SCHEMA_ICP = "org.onboard.icon-palette" SCHEMA_ICP_LANDSCAPE = "org.onboard.icon-palette.landscape" SCHEMA_ICP_PORTRAIT = "org.onboard.icon-palette.portrait" SCHEMA_AUTO_SHOW = "org.onboard.auto-show" SCHEMA_UNIVERSAL_ACCESS = "org.onboard.universal-access" SCHEMA_THEME = "org.onboard.theme-settings" SCHEMA_LOCKDOWN = "org.onboard.lockdown" SCHEMA_SCANNER = "org.onboard.scanner" SCHEMA_TYPING_ASSISTANCE = "org.onboard.typing-assistance" SCHEMA_WORD_SUGGESTIONS = "org.onboard.typing-assistance.word-suggestions" SCHEMA_GSS = "org.gnome.desktop.screensaver" SCHEMA_GDI = "org.gnome.desktop.interface" SCHEMA_GDA = "org.gnome.desktop.a11y.applications" SCHEMA_UNITY_GREETER = "com.canonical.unity-greeter" MODELESS_GKSU_KEY = "/apps/gksu/disable-grab" # old gconf key, unused # hard coded defaults DEFAULT_X = 100 # Make sure these match the schema defaults, DEFAULT_Y = 50 # else dconf data migration won't happen. DEFAULT_WIDTH = 700 DEFAULT_HEIGHT = 205 # Default rect on Nexus 7 # landscape x=65, y=500, w=1215 h=300 # portrait x=55, y=343, w=736 h=295 DEFAULT_ICP_X = 100 # Make sure these match the schema defaults, DEFAULT_ICP_Y = 50 # else dconf data migration won't happen. DEFAULT_ICP_HEIGHT = 64 DEFAULT_ICP_WIDTH = 64 DEFAULT_LAYOUT = "Compact" DEFAULT_THEME = "Classic Onboard" DEFAULT_COLOR_SCHEME = "Classic Onboard" START_ONBOARD_XEMBED_COMMAND = "onboard --xid" INSTALL_DIR = "/usr/share/onboard" LOCAL_INSTALL_DIR = "/usr/local/share/onboard" USER_DIR = "onboard" SYSTEM_DEFAULTS_FILENAME = "onboard-defaults.conf" DEFAULT_WINDOW_HANDLES = list(Handle.RESIZE_MOVE) DEFAULT_FREQUENCY_TIME_RATIO = 75 # 0=100% frequency, 100=100% time (last use) SCHEMA_VERSION_0_97 = Version(1, 0) # Onboard 0.97 SCHEMA_VERSION_0_98 = Version(2, 0) # Onboard 0.97.1 SCHEMA_VERSION_0_99 = Version(2, 1) # Onboard 0.99.0 SCHEMA_VERSION_1_1 = Version(2, 2) SCHEMA_VERSION_1_2 = Version(2, 3) SCHEMA_VERSION = SCHEMA_VERSION_1_2 # enum for simplified number of window_handles class NumResizeHandles: NONE = 0 NORESIZE = 1 SOME = 2 ALL = 3 class Config(ConfigObject): """ Singleton Class to encapsulate the gsettings stuff and check values. """ # launched by ... (LAUNCHER_NONE, LAUNCHER_GSS, LAUNCHER_UNITY_GREETER) = range(3) # extension of layout files LAYOUT_FILE_EXTENSION = ".onboard" # A copy of snippets so that when the list changes in gsettings we can # tell which items have changed. _last_snippets = None # Margin to leave around labels LABEL_MARGIN = (1, 1) # Horizontal label alignment DEFAULT_LABEL_X_ALIGN = 0.5 # Vertical label alignment DEFAULT_LABEL_Y_ALIGN = 0.5 # layout group for independently sized superkey labels SUPERKEY_SIZE_GROUP = "super" # width of frame around onboard when window decoration is disabled UNDECORATED_FRAME_WIDTH = 6.0 # width of frame around popup windows POPUP_FRAME_WIDTH = 5.0 # radius of the rounded window corners CORNER_RADIUS = 10 # y displacement of the key face of dish keys DISH_KEY_Y_OFFSET = 0.8 # raised border size of dish keys DISH_KEY_BORDER = (2.5, 2.5) # minimum time keys are drawn in pressed state UNPRESS_DELAY = 0.15 # Margin to leave around wordlist labels; smaller margins leave # more room for prediction choices WORDLIST_LABEL_MARGIN = (2, 2) # Gap between wordlist buttons WORDLIST_BUTTON_SPACING = (0.5, 0.5) # Gap between wordlist predictions and correctios WORDLIST_ENTRY_SPACING = (1.0, 1.0) # index of currently active pane, not stored in gsettings active_layer_index = 0 # threshold protect window move/resize drag_protection = True # Allow to iconify onboard when neither icon-palette nor # status-icon are enabled, else hide and show the window. # Iconifying is shaky in unity and gnome-shell. After unhiding # from launcher once, the WM won't allow to unminimize onboard # itself anymore for auto-show. (Precise) allow_iconifying = False # Gdk window scaling factor window_scaling_factor = 1.0 _xembed_background_rgba = None _desktop_environment = None _source_path = None def __new__(cls, *args, **kwargs): """ Singleton magic. """ if not hasattr(cls, "self"): cls.self = object.__new__(cls, *args, **kwargs) cls.self.construct() return cls.self def __init__(self): """ This constructor is still called multiple times. Do nothing here and use the singleton constructor construct() instead. Don't call base class constructors. """ pass def construct(self): """ Singleton constructor, runs only once. First intialization stage that runs before the single instance check. Only do the bare minimum here. """ # parse command line parser = OptionParser() group = OptionGroup(parser, "General Options") group.add_option("-l", "--layout", dest="layout", help=_format("Layout file ({}) or name", self.LAYOUT_FILE_EXTENSION)) group.add_option("-t", "--theme", dest="theme", help=_("Theme file (.theme) or name")) group.add_option("-s", "--size", dest="size", help=_("Window size, widthxheight")) group.add_option("-x", type="int", dest="x", help=_("Window x position")) group.add_option("-y", type="int", dest="y", help=_("Window y position")) parser.add_option_group(group) group = OptionGroup(parser, "Advanced Options") group.add_option("-e", "--xid", action="store_true", dest="xid_mode", help=_("Start in XEmbed mode, e.g. for gnome-screensaver")) group.add_option("-m", "--allow-multiple-instances", action="store_true", dest="allow_multiple_instances", help=_("Allow multiple Onboard instances")) group.add_option("--not-show-in", dest="not_show_in", metavar="DESKTOPS", help=_("Silently fail to start in the given desktop " "environments. DESKTOPS is a comma-separated list of " "XDG desktop names, e.g. GNOME for GNOME Shell." )) group.add_option("-D", "--startup-delay", type="float", dest="startup_delay", help=_("Delay showing the initial keyboard window " "by STARTUP_DELAY seconds (default 0.0).")) group.add_option("-a", "--keep-aspect", action="store_true", dest="keep_aspect_ratio", help=_("Keep aspect ratio when resizing the window")) group.add_option("-q", "--quirks", dest="quirks_name", help=_("Override auto-detection and manually select quirks\n" "QUIRKS={metacity|compiz|mutter}")) parser.add_option_group(group) group = OptionGroup(parser, "Debug Options") group.add_option("-d", "--debug", type="str", dest="debug", metavar="ARG", help="Set logging level or range of levels \n" "ARG=MIN_LEVEL[-MAX_LEVEL]\n" "LEVEL={all|event|atspi|debug|info|warning|error|\n" "critical}\n") group.add_option("", "--launched-by", type="str", dest="launched_by", metavar="ARG", help="Simulate being launched by certain XEmbed sockets. \n" "Use this together with option --xid. \n" "ARG={unity-greeter|gnome-screen-saver}\n") group.add_option("-g", "--log-learning", action="store_true", dest="log_learn", default=False, help="log all learned text; off by default") parser.add_option_group(group) options = parser.parse_args()[0] self.options = options self.xid_mode = options.xid_mode self.log_learn = options.log_learn self.quirks_name = options.quirks_name self.quirks = None # WMQuirks instance, provided by KbdWindow for now self.startup_delay = options.startup_delay # optionally log to file; everything, including stack traces if 0: options.debug = "debug" logfile = open("/tmp/onboard.log", "w") sys.stdout = logfile sys.stderr = logfile # setup logging min_level_name = None max_level_name = None if options.debug: level_range = options.debug.split("-") if len(level_range) >= 2: min_level_name, max_level_name = level_range[:2] else: min_level_name = level_range[0] self._init_logging(min_level_name, max_level_name) # Add basic config children for usage before the single instance check. # All the others are added in self._init_keys(). self.children = [] self.gnome_a11y = self.add_optional_child(ConfigGDA) if self.gnome_a11y: # schema may not exist on Gentoo (LP: #1402558) self.gnome_a11y.init_from_gsettings() # detect who launched us self.launched_by = self.LAUNCHER_NONE if self.xid_mode: if options.launched_by: if options.launched_by == "gnome-screensaver": self.launched_by = self.LAUNCHER_GSS elif options.launched_by == "unity-greeter": self.launched_by = self.LAUNCHER_UNITY_GREETER else: if Process.was_launched_by("gnome-screensaver"): self.launched_by = self.LAUNCHER_GSS elif "UNITY_GREETER_DBUS_NAME" in os.environ: self.launched_by = self.LAUNCHER_UNITY_GREETER self.is_running_from_source = self._is_running_from_source() if self.is_running_from_source: _logger.warning("Starting in project directory, " "importing local packages and extensions.") def init(self): """ Second initialization stage. Call this after single instance checking on application start. """ # call base class constructor once logging is available try: ConfigObject.__init__(self) except SchemaError as e: _logger.error(unicode_str(e)) sys.exit() # log desktop environment _logger.debug("Desktop environment: {}" .format(self.get_desktop_environment().name)) # log how we were launched if _logger.isEnabledFor(logging.DEBUG): _logger.debug("command line: " + str(sys.argv)) #_logger.debug("environment: " + str(os.environ)) cmdline = Process.get_launch_process_cmdline() _logger.debug("lauched by, process: '{}'".format(cmdline)) _logger.debug("lauched by, detected: {}".format(self.launched_by)) # init paths self.install_dir = self._get_install_dir() self.user_dir = self._get_user_dir() self._image_search_paths = [] # migrate old user dir ".sok" or ".onboard" to XDG data home if not os.path.exists(self.user_dir): old_user_dir = os.path.join(os.path.expanduser("~"), ".onboard") if not os.path.exists(old_user_dir): old_user_dir = os.path.join(os.path.expanduser("~"), ".sok") if os.path.exists(old_user_dir): _logger.info(_format("Migrating user directory '{}' to '{}'.", \ old_user_dir, self.user_dir)) # Copy the data directory try: copytree(old_user_dir, self.user_dir) chmodtree(self.user_dir, 0o700, True) # honor XDG spec except OSError as ex: # python >2.5 _logger.error(_format("failed to migrate user directory. ") + \ unicode_str(ex)) # Return paths in gsettings to basenames. Custom path # information will be lost. try: filter = lambda x: os.path.splitext(os.path.basename(x))[0] s = Gio.Settings.new(SCHEMA_ONBOARD) s["layout"] = filter(s["layout"]) s["theme"] = filter(s["theme"]) s["system-theme-associations"] = \ {k : filter(v) \ for k, v in s["system-theme-associations"].items()} except Exception as ex: _logger.warning("migrating gsettings paths failed: " + \ unicode_str(ex)) # Migrate old user language model to language specific # model for Onboard 1.0. from Onboard.WPEngine import ModelCache old_fn = ModelCache.get_filename("lm:user:user") if os.path.exists(old_fn): lang_id = self.get_system_default_lang_id() new_fn = ModelCache.get_filename("lm:user:" + lang_id) old_bak = ModelCache.get_backup_filename(old_fn) new_bak = ModelCache.get_backup_filename(new_fn) _logger.info("Starting migration, user.lm has been deprecated.") for old, new in [[old_fn, new_fn], [old_bak, new_bak]]: if os.path.exists(old): if os.path.exists(new): _logger.info("Migration target already exists, " "skipping renaming " "'{}' to '{}'." \ .format(old, new)) break # skip backup _logger.info("Migrating user language model " "'{}' to '{}'." \ .format(old, new)) try: os.rename(old, new) except OSError as ex: _logger.error("Failed to migrate " "user language model. " + \ unicode_str(ex)) break # Load system defaults (if there are any, not required). # Used for distribution specific settings, aka branding. paths = XDGDirs.get_all_config_dirs(USER_DIR) + \ [self.install_dir, "/etc/onboard"] paths = [os.path.join(p, SYSTEM_DEFAULTS_FILENAME) for p in paths] self.load_system_defaults(paths) # initialize all property values used_system_defaults = self.init_properties(self.options) self._update_xembed_background_rgba() # Make sure there is a 'Default' entry when tracking the system theme. # 'Default' is the theme used when encountering a so far unknown # gtk-theme. theme_assocs = self.system_theme_associations.copy() if not "Default" in theme_assocs: theme_assocs["Default"] = "" self.system_theme_associations = theme_assocs # Remember command line theme for system theme tracking. if self.options.theme: self.theme_key.modified = True # force writing on next occasion self.remember_theme(self.theme) # load theme global Theme from Onboard.Appearance import Theme _logger.info("Theme candidates {}" \ .format(self._get_theme_candidates())) self.load_theme() # misc initializations self._last_snippets = dict(self.snippets) # store a copy # init state of mousetweaks' click-type window if self.mousetweaks: self.mousetweaks.init_click_type_window_visible( self.universal_access.hide_click_type_window) # remember if we are running under GDM self.running_under_gdm = 'RUNNING_UNDER_GDM' in os.environ # tell config objects that their properties are valid now self.on_properties_initialized() # Work around changes in preferences having no effect in Saucy. # If there is an unbalanced self.delay() somewhere I haven't found it. self.apply() _logger.debug("Leaving init") def cleanup(self): # This used to stop dangling main windows from responding # when restarting. Restarts don't happen anymore, keep # this for now anyway. self.disconnect_notifications() if self.mousetweaks: self.mousetweaks.cleanup() def final_cleanup(self): if self.mousetweaks: self.mousetweaks.restore_click_type_window_visible( self.universal_access.enable_click_type_window_on_exit and \ not self.xid_mode) def _init_logging(self, min_level_name, max_level_name): LEVEL_ATSPI = 6 LEVEL_EVENT = 5 levels = { # builtin levels "CRITICAL" : (50, TermColors.RED), "ERROR" : (40, TermColors.RED), "WARNING" : (30, TermColors.YELLOW), "INFO" : (20, TermColors.GREEN), "DEBUG" : (10, TermColors.BLUE), # custom levels "ATSPI" : (LEVEL_ATSPI, TermColors.CYAN), "EVENT" : (LEVEL_EVENT, TermColors.MAGENTA), "ALL" : ( 1, None), "NOTSET" : ( 0, None), } for name, (level, color) in levels.items(): if logging.getLevelName(level) != name: logging.addLevelName(level, name) class CustomLogger(logging.Logger): def __init__(self, *args): self.LEVEL_ATSPI = LEVEL_ATSPI self.LEVEL_EVENT = LEVEL_EVENT logging.Logger.__init__(self, *args) def atspi(self, msg, *args, **kwargs): if self.isEnabledFor(LEVEL_ATSPI): self._log(LEVEL_ATSPI, msg, args, **kwargs) def event(self, msg, *args, **kwargs): if self.isEnabledFor(LEVEL_EVENT): self._log(LEVEL_EVENT, msg, args, **kwargs) class CustomFilter(object): def __init__(self, max_level): self._max_level = max_level def filter(self, logRecord): return logRecord.levelno <= self._max_level class ColorFormatter(logging.Formatter): def __init__(self, msgfmt, log_levels, use_colors): self._log_levels = log_levels self._use_colors = use_colors self._tcs = TermColors() if use_colors else None msg = self._substitute_variables(msgfmt, use_colors) logging.Formatter.__init__(self, msg, datefmt="%H:%M:%S", style="{") def _substitute_variables(self, msg, use_colors): if use_colors: msg = msg.replace("{RESET}", self._tcs.get(TermColors.RESET)) msg = msg.replace("{BOLD}", self._tcs.get(TermColors.BOLD)) else: msg = msg.replace("{RESET}", "") msg = msg.replace("{BOLD}", "") return msg def formatMessage(self, record): level_name = record.levelname if self._use_colors and \ level_name in self._log_levels: level, color = self._log_levels[level_name] color_seq = self._tcs.get(color) else: color_seq = "" record.__dict__["levelcolor"] = color_seq debug = bool(min_level_name) record.__dict__["name_width"] = 21 if debug else 1 record.__dict__["level_width"] = 7 if debug else 1 record.__dict__["name_"] = record.name + \ ("" if debug else ":") return logging.Formatter.formatMessage(self, record) msgfmt = ("{asctime}.{msecs:03.0f} " "{levelcolor}{levelname:{level_width}}{RESET} " "{BOLD}{name_:{name_width}}{RESET} " "{message}") handler = logging.StreamHandler() try: is_tty = handler.stream.isatty() # color only in interact. terminal except: is_tty = False handler.setFormatter(ColorFormatter(msgfmt, levels, is_tty)) root = logging.getLogger() root.addHandler(handler) logging.setLoggerClass(CustomLogger) if min_level_name: root.setLevel(min_level_name.upper()) if max_level_name: max_level_name = max_level_name.upper() if max_level_name in levels: max_level, color = levels[max_level_name] handler.addFilter(CustomFilter(max_level)) def _init_keys(self): """ Create key descriptions """ self.schema = SCHEMA_ONBOARD self.sysdef_section = "main" self.add_key("schema-version", "") # is assigned SCHEMA_VERSION on first start self.add_key("use-system-defaults", False) self.layout_key = \ self.add_key("layout", DEFAULT_LAYOUT) self.theme_key = \ self.add_key("theme", DEFAULT_THEME) self.add_key("system-theme-tracking-enabled", True) self.add_key("system-theme-associations", {}, 'a{ss}') self.add_key("snippets", {}, "as") self.add_key("show-status-icon", True) self.add_key("status-icon-provider", StatusIconProviderEnum.AppIndicator, enum={"auto" : 0, "GtkStatusIcon" : 1, "AppIndicator" : 2, }) self.add_key("start-minimized", False) self.add_key("show-tooltips", True) self.add_key("key-label-font", "") # default font for all themes self.add_key("key-label-overrides", {}, "as") # default labels for all themes self.add_key("current-settings-page", 0) self.add_key("xembed-onboard", False, prop="onboard_xembed_enabled") self.add_key("xembed-aspect-change-range", [0, 1.6]) self.add_key("xembed-background-color", "#0000007F") self.add_key("xembed-background-image-enabled", True) self.add_key("xembed-unity-greeter-offset-x", 85.0) self.keyboard = ConfigKeyboard() self.window = ConfigWindow() self.icp = ConfigICP(self) self.auto_show = ConfigAutoShow() self.universal_access = ConfigUniversalAccess(self) self.theme_settings = ConfigTheme(self) self.lockdown = ConfigLockdown(self) self.scanner = ConfigScanner(self) self.typing_assistance = ConfigTypingAssistance(self) self.gss = ConfigGSS(self) self.gdi = ConfigGDI(self) self.children += [self.keyboard, self.window, self.icp, self.auto_show, self.universal_access, self.theme_settings, self.lockdown, self.gss, self.gdi, self.scanner, self.typing_assistance] # mousetweaks (optional) for _class in [CSMousetweaks1, CSMousetweaks0]: try: self.mousetweaks = _class() self.children.append(self.mousetweaks) break except (SchemaError, ImportError, RuntimeError) as e: _logger.info(unicode_str(e)) self.mousetweaks = None if self.mousetweaks is None: _logger.warning("mousetweaks GSettings schema not found, " "mousetweaks integration disabled.") # unity greeter (very optional) self.unity_greeter = None if self.launched_by == self.LAUNCHER_UNITY_GREETER: try: self.unity_greeter = ConfigUnityGreeter(self) self.children.append(self.unity_greeter) except (SchemaError, ImportError) as e: _logger.warning(unicode_str(e)) def init_from_gsettings(self): """ Overloaded to migrate old dconf data to new gsettings schemas """ ConfigObject.init_from_gsettings(self) # --- onboard 0.97 -> 0.98 -------------------------------------------- format = Version.from_string(self.schema_version) if format < SCHEMA_VERSION_0_98: _logger.info("Migrating dconf values from before v0.98: " "/apps/onboard -> /org/onboard") self.migrate_dconf_tree("apps.", "org.") # --- onboard 0.96 -> 0.97 ---------------------------------------- format = Version.from_string(self.schema_version) if format < SCHEMA_VERSION_0_97: _logger.info("Migrating dconfs values from before v0.97") self._migrate_to_0_97() # --- onboard 1.0.0 -> 1.1.0------------------------------------------- format = Version.from_string(self.schema_version) if format < SCHEMA_VERSION_1_1: _logger.info("Migrating dconf values from before v1.1.0 ") self._migrate_to_1_1() # --- onboard 1.1.0 -> 1.2.0------------------------------------------- format = Version.from_string(self.schema_version) if format < SCHEMA_VERSION_1_2: _logger.info("Migrating dconf values from before v1.2.0 ") self._migrate_to_1_2() self.schema_version = SCHEMA_VERSION.to_string() def _migrate_to_1_2(self): """ reposition-method split into reposition-method-floating and reposition-method-docked """ co = self.auto_show co.delay() co.migrate_dconf_key("/org/onboard/auto-show/reposition-method", "reposition-method-floating") co.migrate_dconf_key("/org/onboard/auto-show/reposition-method", "reposition-method-docked") co.apply() def _migrate_to_1_1(self): """ resize_handles renamed to window_handles and movement added """ def migrate_resize_handles(co, dconf_path, key): gskey = co.find_key(key) co.delay() co.migrate_dconf_value(dconf_path, gskey) # add new "MOVE" entry handles = getattr(co, gskey.prop)[:] if not Handle.MOVE in handles: handles += (Handle.MOVE,) setattr(co, gskey.prop, handles) co.apply() migrate_resize_handles(self.window, "/org/onboard/window/resize-handles", "window-handles") migrate_resize_handles(self.icp, "/org/onboard/icon-palette/resize-handles", "window-handles") def _migrate_to_0_97(self): # window rect moves from org.onboard to # org.onboard.window.landscape/portrait co = self.window.landscape if co.gskeys["x"].is_default() and \ co.gskeys["y"].is_default() and \ co.gskeys["width"].is_default() and \ co.gskeys["height"].is_default(): co.delay() co.migrate_dconf_value("/apps/onboard/x", co.gskeys["x"]) co.migrate_dconf_value("/apps/onboard/y", co.gskeys["y"]) co.migrate_dconf_value("/apps/onboard/width", co.gskeys["width"]) co.migrate_dconf_value("/apps/onboard/height", co.gskeys["height"]) co.apply() # icon-palette rect moves from org.onboard.icon-palette to # org.onboard.icon-palette.landscape/portrait co = self.icp.landscape if co.gskeys["x"].is_default() and \ co.gskeys["y"].is_default() and \ co.gskeys["width"].is_default() and \ co.gskeys["height"].is_default(): co.delay() co.migrate_dconf_value("/apps/onboard/icon-palette/x", co.gskeys["x"]) co.migrate_dconf_value("/apps/onboard/icon-palette/y", co.gskeys["y"]) co.migrate_dconf_value("/apps/onboard/icon-palette/width", co.gskeys["width"]) co.migrate_dconf_value("/apps/onboard/icon-palette/height", co.gskeys["height"]) co.apply() # move keys from root to window co = self.window co.migrate_dconf_key("/apps/onboard/window-decoration", "window-decoration") co.migrate_dconf_key("/apps/onboard/force-to-top", "force-to-top") co.migrate_dconf_key("/apps/onboard/transparent-background", "transparent-background") co.migrate_dconf_key("/apps/onboard/transparency", "transparency") co.migrate_dconf_key("/apps/onboard/background-transparency", "background-transparency") co.migrate_dconf_key("/apps/onboard/enable-inactive-transparency", "enable-inactive-transparency") co.migrate_dconf_key("/apps/onboard/inactive-transparency", "inactive-transparency") co.migrate_dconf_key("/apps/onboard/inactive-transparency-delay", "inactive-transparency-delay") # accessibility keys move from root to universal-access co = self.universal_access co.migrate_dconf_key("/apps/onboard/hide-click-type-window", "hide-click-type-window") co.migrate_dconf_key("/apps/onboard/enable-click-type-window-on-exit", "enable-click-type-window-on-exit") # move keys from root to keyboard co = self.keyboard co.migrate_dconf_key("/apps/onboard/show-click-buttons", "show-click-buttons") ##### handle special keys only valid in system defaults ##### def _read_sysdef_section(self, parser): super(self.__class__, self)._read_sysdef_section(parser) # Convert the simplified superkey_label setting into # the more general key_label_overrides setting. sds = self.system_defaults if "superkey_label" in sds: overrides = sds.get( "key_label_overrides", {}) group = self.SUPERKEY_SIZE_GROUP \ if sds.get("superkey_label_independent_size") else "" for key_id in ["LWIN", "RWIN"]: overrides[key_id] = (sds["superkey_label"], group) sds["key_label_overrides"] = overrides def _convert_sysdef_key(self, gskey, sysdef, value): # key exclusive to system defaults? if sysdef in ["superkey-label", "superkey-label-independent-size"]: return value else: return super(self.__class__, self). \ _convert_sysdef_key(gskey, sysdef, value) ##### property helpers ##### def _unpack_key_label_overrides(self, value): return self.unpack_string_list(value, "a{s[ss]}") def _pack_key_label_overrides(self, value): return self.pack_string_list(value) def _unpack_snippets(self, value): return self.unpack_string_list(value, "a{i[ss]}") def _pack_snippets(self, value): return self.pack_string_list(value) def _post_notify_layout(self): self._invalidate_layout_resource_search_paths() # Property layout_filename, linked to gsettings key "layout". # layout_filename may only get/set a valid filename, # whereas layout also allows to get/set only the basename of a layout. def layout_filename_notify_add(self, callback): self.layout_notify_add(callback) def get_layout_filename(self): gskey = self.layout_key return self.find_layout_filename(gskey.value, gskey.key, self.LAYOUT_FILE_EXTENSION, os.path.join(self.install_dir, "layouts", DEFAULT_LAYOUT + self.LAYOUT_FILE_EXTENSION)) def set_layout_filename(self, filename): if filename and os.path.exists(filename): self.layout = filename else: _logger.warning(_format("layout '{filename}' does not exist", \ filename=filename)) layout_filename = property(get_layout_filename, set_layout_filename) def get_fallback_layout_filename(self): """ Layout file to fallback to when the initial layout won't load """ return self.find_layout_filename(DEFAULT_LAYOUT, "layout", self.LAYOUT_FILE_EXTENSION) def find_layout_filename(self, filename, description, extension = "", final_fallback = ""): """ Find layout file, either the root layout or an include file. """ return self._get_user_sys_filename( filename = filename, description = description, user_filename_func = lambda x: \ os.path.join(self.user_dir, "layouts", x) + extension, system_filename_func = lambda x: \ os.path.join(self.install_dir, "layouts", x) + extension, final_fallback = final_fallback) # Property theme_filename, linked to gsettings key "theme". # theme_filename may only get/set a valid filename, # whereas theme also allows to get/set only the basename of a theme. def theme_filename_notify_add(self, callback): self.theme_notify_add(callback) def get_theme_filename(self): candidates = self._get_theme_candidates() for theme in candidates: if theme: filename = self._expand_theme_filename(theme) if filename: return filename return "" def set_theme_filename(self, filename): if filename and os.path.exists(filename): self.remember_theme(filename) else: _logger.warning(_format("theme '{filename}' does not exist", \ filename=filename)) theme_filename = property(get_theme_filename, set_theme_filename) def _expand_theme_filename(self, theme): """ expand generic theme name """ return self._expand_user_sys_filename(theme, user_filename_func = Theme.build_user_filename, system_filename_func = Theme.build_system_filename) def remember_theme(self, theme_filename): if self.system_theme_tracking_enabled and \ self.gdi: # be defensive gtk_theme = self.get_gtk_theme() theme_assocs = self.system_theme_associations.copy() theme_assocs[gtk_theme] = theme_filename self.system_theme_associations = theme_assocs self.set_theme(theme_filename) def apply_theme(self, filename = None): if not filename: filename = self.get_theme_filename() _logger.info(_format("Loading theme from '{}'", filename)) theme = Theme.load(filename) if not theme: _logger.error(_format("Unable to read theme '{}'", filename)) else: # Save to gsettings # Make sure gsettings is in sync with onboard (LP: 877601) #self.theme = filename theme.apply() # Fix theme not saved to gesettings when switching # system contrast themes. # Possible gsettings bug in Precise (wasn't in Oneiric). #self.apply() return bool(theme) def load_theme(self): """ Figure out which theme to load and load it. """ self.apply_theme() def _get_theme_candidates(self): """ Return a list of themes to consider for loading. Highest priority first. """ candidates = [] if self.system_theme_tracking_enabled: theme_assocs = self.system_theme_associations gtk_theme = self.get_gtk_theme() candidates += [theme_assocs.get(gtk_theme, ""), theme_assocs.get("Default", ""), self.theme] else: candidates += ["", "", self.theme] if self.system_defaults: theme = self.system_defaults.get("theme", "") else: theme = "" candidates.append(theme) candidates.append(DEFAULT_THEME) return candidates def get_gtk_theme(self): gtk_settings = Gtk.Settings.get_default() if gtk_settings: # be defensive, don't know if this can fail gtk_theme = gtk_settings.get_property('gtk-theme-name') return gtk_theme return None def get_image_filename(self, image_filename): """ Returns an absolute path for a label image. This function isn't linked to any gsettings key. """ if not self._image_search_paths: self._image_search_paths = \ self._get_layout_resource_search_paths("images") return self._get_resource_filename(image_filename, "image", self._image_search_paths) def _invalidate_layout_resource_search_paths(self): """ Force re-generation of search paths at next opportunity. """ self._image_search_paths = [] def _get_layout_resource_search_paths(self, subdir): """ Look for files in directory of current layout, user- and system directory, in that order. """ user_dir = os.path.join(self.user_dir, "layouts") sys_dir = os.path.join(self.install_dir, "layouts") if subdir: user_dir = os.path.join(user_dir, subdir) sys_dir = os.path.join(sys_dir, subdir) paths = [user_dir, sys_dir] # In case a full path was given for the layout file on command line, # look relative to there first. if self._is_valid_filename(self.layout): dir_ = os.path.dirname(self.layout) if subdir: dir_ = os.path.join(dir_, "images") try: # make sure it isn't one of those we already know same = os.path.isdir(user_dir) and \ os.path.samefile(dir_, sys_dir) or \ os.path.isdir(user_dir) and \ os.path.samefile(dir_, user_dir) except Exception as ex: _logger.error("samefile failed with '{}': {}" \ .format(dir_, unicode_str(ex))) same = True if not same: paths = [dir_] + paths return paths def get_user_model_dir(self): return os.path.join(self.user_dir, "models") def get_system_model_dir(self): return os.path.join(self.install_dir, "models") def enable_hover_click(self, enable): hide = self.universal_access.hide_click_type_window if enable: self.mousetweaks.allow_system_click_type_window(False, hide) self.mousetweaks.set_active(True) else: self.mousetweaks.set_active(False) self.mousetweaks.allow_system_click_type_window(True, hide) def is_hover_click_active(self): return bool(self.mousetweaks) and self.mousetweaks.is_active() def is_visible_on_start(self): return self.xid_mode or \ not self.start_minimized and \ not self.auto_show.enabled def is_auto_show_enabled(self): return not self.xid_mode and \ self.auto_show.enabled def is_auto_hide_enabled(self): return self.is_auto_hide_on_keypress_enabled() or \ self.is_tablet_mode_detection_enabled() or \ self.is_keyboard_device_detection_enabled() def is_auto_hide_on_keypress_enabled(self): return self.can_set_auto_hide() and \ self.auto_show.hide_on_key_press def is_tablet_mode_detection_enabled(self): return self.can_set_auto_hide() and \ self.auto_show.tablet_mode_detection_enabled def is_keyboard_device_detection_enabled(self): return self.can_set_auto_hide() and \ self.auto_show.keyboard_device_detection_enabled def can_auto_show_reposition(self): return self.is_auto_show_enabled() and \ self.get_auto_show_reposition_method() != RepositionMethodEnum.NONE def get_auto_show_reposition_method(self): if self.is_docking_enabled(): return self.auto_show.reposition_method_docked else: return self.auto_show.reposition_method_floating def can_set_auto_hide(self): """ Allowed to change auto hide? """ return self.is_auto_show_enabled() and \ self.is_event_source_xinput() def is_force_to_top(self): return self.window.force_to_top or self.is_docking_enabled() def is_override_redirect(self): return self.is_force_to_top() and \ self.quirks.can_set_override_redirect(self) def is_docking_enabled(self): return self.window.docking_enabled def is_dock_expanded(self, orientation_co): return self.window.docking_enabled and orientation_co.dock_expand def are_word_suggestions_enabled(self): return self.word_suggestions.enabled and not self.xid_mode def are_spelling_suggestions_enabled(self): return self.are_word_suggestions_enabled() and \ self.word_suggestions.spelling_suggestions_enabled def is_spell_checker_enabled(self): return self.are_spelling_suggestions_enabled() or \ self.typing_assistance.auto_capitalization or \ self.typing_assistance.auto_correction def is_auto_capitalization_enabled(self): return self.typing_assistance.auto_capitalization and not self.xid_mode def is_typing_assistance_enabled(self): return self.are_word_suggestions_enabled() or \ self.is_auto_capitalization_enabled() def is_event_source_gtk(self): return self.keyboard.input_event_source == InputEventSourceEnum.GTK def is_event_source_xinput(self): return self.keyboard.input_event_source == InputEventSourceEnum.XINPUT def check_gnome_accessibility(self, parent = None): if not self.xid_mode and \ not self.gdi.toolkit_accessibility: question = _("Enabling auto-show requires Gnome Accessibility.\n\n" "Onboard can turn on accessiblity now, however it is " "recommended that you log out and back in " "for it to reach its full potential.\n\n" "Enable accessibility now?") reply = show_confirmation_dialog(question, parent, self.is_force_to_top()) if not reply == True: return False self.gdi.toolkit_accessibility = True return True def get_drag_threshold(self): threshold = self.universal_access.gskeys["drag_threshold"].value if threshold == -1: # get the systems DND threshold threshold = Gtk.Settings.get_default(). \ get_property("gtk-dnd-drag-threshold") return threshold def is_icon_palette_in_use(self): """ Show icon palette when there is no other means to unhide onboard. Unhiding by unity launcher isn't available in force-to-top mode. """ return self.icp.in_use or self.is_icon_palette_last_unhide_option() def is_icon_palette_last_unhide_option(self): """ Is the icon palette the last remaining way to unhide onboard? Consider single instance check a way to always unhide onboard. """ return False def has_unhide_option(self): """ No native ui visible to unhide onboard? There might still be the launcher to unminimize it. """ return self.is_icon_palette_in_use() or self.show_status_icon def has_window_decoration(self): """ Force-to-top mode doesn't support window decoration """ return self.window.window_decoration and not self.is_force_to_top() def get_sticky_state(self): return not self.xid_mode and \ (self.window.window_state_sticky or self.is_force_to_top()) def is_inactive_transparency_enabled(self): return self.window.enable_inactive_transparency and \ not self.scanner.enabled def is_keep_window_aspect_ratio_enabled(self, orientation_co): """ Keep aspect ratio of the whole keyboard window? Not recommended, no effect in MATE and elsewhere the keyboard tends to shrink with each interaction. """ return ((self.window.keep_aspect_ratio or self.options.keep_aspect_ratio) and not self.xid_mode and not self.is_docking_enabled()) def is_keep_frame_aspect_ratio_enabled(self, orientation_co): """ Keep aspect ratio of only the frame (keyboard area) inside the keyboard window? """ return \ self.is_keep_xembed_frame_aspect_ratio_enabled() or \ self.is_keep_docking_frame_aspect_ratio_enabled(orientation_co) def is_keep_xembed_frame_aspect_ratio_enabled(self): return self.xid_mode and self.launched_by != self.LAUNCHER_NONE def is_keep_docking_frame_aspect_ratio_enabled(self, orientation_co): return (not self.xid_mode and self.is_docking_enabled() and self.is_dock_expanded(orientation_co)) def is_mousetweaks_active(self): return self.mousetweaks and self.mousetweaks.is_active() ####### window handles (resize & move handles) ####### def window_handles_notify_add(self, callback): self.window.window_handles_notify_add(callback) self.icp.window_handles_notify_add(callback) def window_handles_to_num_handles(self, handles): """ Translate array of handles to simplified NumResizeHandles enum """ if len(handles) == 0: return NumResizeHandles.NONE if len(handles) == 1 and handles[0] == Handle.MOVE: return NumResizeHandles.NORESIZE if len(handles) == 8 + 1: return NumResizeHandles.ALL return NumResizeHandles.SOME def num_handles_to_window_handles(self, num): if num == NumResizeHandles.ALL: handles = list(Handle.RESIZE_MOVE) elif num == NumResizeHandles.NORESIZE: handles = [Handle.MOVE] elif num == NumResizeHandles.NONE: handles = [] else: # NumResizeHandles.SOME handles = list(Handle.CORNERS + (Handle.MOVE, )) return handles def num_handles_to_icon_palette_handles(self, num): handles = self.num_handles_to_window_handles(num) if num == NumResizeHandles.SOME: handles = [Handle.SOUTH_EAST, Handle.MOVE] return handles @staticmethod def _string_to_handles(string): """ String of handle ids to array of Handle enums """ ids = string.split() handles = [] for id in ids: handle = Handle.RIDS.get(id) if not handle is None: handles.append(handle) return handles @staticmethod def _handles_to_string(handles): """ Array of handle enums to string of handle ids """ ids = [] for handle in handles: ids.append(Handle.IDS[handle]) return " ".join(ids) ####### Snippets editing ####### def set_snippet(self, index, value): """ Set a snippet in the snippet list. Enlarge the list if not big enough. @type index: int @param index: index of the snippet to set. @type value: str @param value: Contents of the new snippet. """ if value == None: raise TypeError("Snippet text must be str") label, text = value snippets = dict(self.snippets) # copy to enable callbacks _logger.info("Setting snippet %d to '%s', '%s'" % (index, label, text)) snippets[index] = (label, text) self.snippets = snippets def del_snippet(self, index): """ Delete a snippet. @type index: int @param index: index of the snippet to delete. """ _logger.info("Deleting snippet %d" % index) snippets = dict(self.snippets) # copy to enable callbacks del snippets[index] self.snippets = snippets ###### gnome-screensaver, xembedding ##### def enable_gss_embedding(self, enable): if enable: self.onboard_xembed_enabled = True self.gss.embedded_keyboard_enabled = True self.set_xembed_command_string_to_onboard() else: self.onboard_xembed_enabled = False self.gss.embedded_keyboard_enabled = False def is_onboard_in_xembed_command_string(self): """ Checks whether the gsettings key for the embeded application command contains the entry defined by onboard. Returns True if it is set to onboard and False otherwise. """ if self.gss.embedded_keyboard_command.startswith(START_ONBOARD_XEMBED_COMMAND): return True else: return False def set_xembed_command_string_to_onboard(self): """ Write command to start the embedded onboard into the corresponding gsettings key. """ self.gss.embedded_keyboard_command = START_ONBOARD_XEMBED_COMMAND def _get_kbd_render_mixin(self): __import__(self._kbd_render_mixin_mod) return getattr(sys.modules[self._kbd_render_mixin_mod], self._kbd_render_mixin_cls) kbd_render_mixin = property(_get_kbd_render_mixin) # modeless gksu - disabled until gksu moves to gsettings def modeless_gksu_notify_add(self, callback): pass modeless_gksu = property(lambda self: False) def get_desktop_background_filename(self): fn = "" # Starting with Vivid's unity greeter try to get the filename # from the greeter's schema. if self.launched_by == self.LAUNCHER_UNITY_GREETER: fn = self._get_desktop_background_filename_from_schema( "com.canonical.unity-greeter", "background") # Elsewhere (old Ubuntu releases, gnome-screen-saver) get it # from the gnome key. if not fn: fn = self._get_desktop_background_filename_from_schema( "org.gnome.desktop.background", "picture-uri") return fn def _get_desktop_background_filename_from_schema(self, schema, key): try: s = Gio.Settings.new(schema) fn = s.get_string(key) except Exception as ex: # private exception gi._glib.GError fn = "" _logger.error("failed to read desktop background from {} {}: {}" \ .format(schema, key, unicode_str(ex))) if fn: try: # Valid file URI? # Prevents error 'not an absolute URI using the "file" scheme' # when using the unity-greeter schema. if GLib.uri_parse_scheme(fn) == "file": try: fn, error = GLib.filename_from_uri(fn) except TypeError: # broken introspection on Precise fn = GLib.filename_from_uri(fn, "") error = "" if error: fn = "" except Exception as ex: # private exception gi._glib.GError _logger.error("failed to unescape URI for desktop background " "'{}': {}" \ .format(fn, unicode_str(ex))) return fn def get_xembed_unity_greeter_offset_x(self): value = self.gskeys["xembed_unity_greeter_offset_x"].value if value < 0: value = None return value def get_xembed_background_rgba(self): return self._xembed_background_rgba def _update_xembed_background_rgba(self): value = self.xembed_background_color self._xembed_background_rgba = hexcolor_to_rgba(value) def _is_running_from_source(self): return bool(self._get_source_path()) def _get_source_path(self): if self._source_path is None: candidates = [ os.path.abspath(os.path.curdir), os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ] self._source_path = "" for path in candidates: data_path = os.path.join(path, "data") fn = os.path.join(data_path, "org.onboard.gschema.xml") if os.path.isfile(fn): self._source_path = path break return self._source_path def _get_install_dir(self): result = None # when run from source if self._is_running_from_source(): # Add the data directory to the icon search path icon_theme = Gtk.IconTheme.get_default() src_path = self._get_source_path() src_icon_path = os.path.join(src_path, "icons") icon_theme.append_search_path(src_icon_path) result = src_path # when installed to /usr/local elif os.path.isdir(LOCAL_INSTALL_DIR): result = LOCAL_INSTALL_DIR # when installed to /usr elif os.path.isdir(INSTALL_DIR): result = INSTALL_DIR assert(result) # warn early when the installation dir wasn't found return result def _get_user_dir(self): return XDGDirs.get_data_home(USER_DIR) def get_user_layout_dir(self): return os.path.join(self.user_dir, "layouts/") def get_system_default_lang_id(self): lang_id = locale.getdefaultlocale()[0] if not lang_id: # None e.g. with LANG=C lang_id = "en_US" return lang_id def get_desktop_environment(self): """ Return current desktop environment. """ if not self._desktop_environment: self._desktop_environment = self._detect_desktop_environment() return self._desktop_environment @staticmethod def _detect_desktop_environment(): """ Detect current desktop environment. Extend as needed. """ xdg_desktop = os.environ.get("XDG_CURRENT_DESKTOP", "") desktop = os.environ.get("DESKTOP_SESSION", "") def istrcmp(s1, s2): if sys.version_info < (3,3): return s1.lower() == s2.lower() return s1.casefold() == s2.casefold() def isdesktop(id): return istrcmp(xdg_desktop, id) or istrcmp(desktop, id) if isdesktop("X-Cinnamon") or isdesktop("cinnamon"): return DesktopEnvironmentEnum.Cinnamon if isdesktop("GNOME"): return DesktopEnvironmentEnum.GNOME_Shell if isdesktop("GNOME-Classic:GNOME"): return DesktopEnvironmentEnum.GNOME_Classic if isdesktop("KDE"): return DesktopEnvironmentEnum.KDE if isdesktop("lxde"): return DesktopEnvironmentEnum.LXDE if isdesktop("LXQt"): return DesktopEnvironmentEnum.LXQT if isdesktop("mate"): return DesktopEnvironmentEnum.MATE if isdesktop("Unity"): return DesktopEnvironmentEnum.Unity if isdesktop("xfce"): return DesktopEnvironmentEnum.XFCE return DesktopEnvironmentEnum.Unknown def get_preferred_statusicon_provider(self): """ Auto-detect if we should fall back to GtkStatusIcon. """ result = StatusIconProviderEnum.AppIndicator de = self.get_desktop_environment() # Gnome-shell annoys with sliding in their legacy icon panel. # We have our indicator extension now, so turn the status icon off. if de in (DesktopEnvironmentEnum.GNOME_Shell, ): result = None elif de in ( # AppIndicator is supported in XUbuntu 16.04, but w/o left click # activation. GtkStatusIcon works well and allows left click. DesktopEnvironmentEnum.Cinnamon, # AppIndicator does nothing in 16.04. DesktopEnvironmentEnum.MATE, # AppIndicator does nothing in 16.04. DesktopEnvironmentEnum.XFCE, # AppIndicator is supported in Lubuntu 16.04, but w/o left # click activation. GtkStatusIcon works well, but is placed # into a legacy system tray applet with tiny 16x16 icon. # DesktopEnvironmentEnum.LXDE, # AppIndicator is supported in LXQt 16.04, including left # click activation. GtkStatusIcon works well too. # DesktopEnvironmentEnum.LXQT, ): result = StatusIconProviderEnum.GtkStatusIcon return result class ConfigKeyboard(ConfigObject): """Window configuration """ DEFAULT_KEY_ACTION = 1 # Release-only, supports long press DEFAULT_KEY_SYNTH = 0 # Auto DEFAULT_TOUCH_INPUT = TouchInputEnum.MULTI DEFAULT_INPUT_EVENT_SOURCE = InputEventSourceEnum.XINPUT def _init_keys(self): self.schema = SCHEMA_KEYBOARD self.sysdef_section = "keyboard" self.add_key("show-click-buttons", False) self.add_key("sticky-key-release-delay", 0.0) self.add_key("sticky-key-release-on-hide-delay", 5.0) self.add_key("sticky-key-behavior", {"all" : "cycle"}, 'a{ss}') self.add_key("long-press-delay", 0.5) self.add_key("default-key-action", self.DEFAULT_KEY_ACTION, enum={"single-stroke" : 0, "delayed-stroke" : 1, }) self.add_key("key-synth", self.DEFAULT_KEY_SYNTH, enum={"auto" : 0, "XTest" : 1, "uinput" : 2, "AT-SPI" : 3, }) self.add_key("touch-input", self.DEFAULT_TOUCH_INPUT, enum={"none" : 0, "single" : 1, "multi" : 2, }) self.add_key("input-event-source", self.DEFAULT_INPUT_EVENT_SOURCE, enum={"GTK" : 0, "XInput" : 1, }) self.add_key("touch-feedback-enabled", False) self.add_key("touch-feedback-size", 0) self.add_key("audio-feedback-enabled", False) self.add_key("audio-feedback-place-in-space", False) self.add_key("show-secondary-labels", False) self.add_key("inter-key-stroke-delay", 0.0) self.add_key("modifier-update-delay", 1.0) self.add_key("key-press-modifiers", {"button3" : "SHIFT"}, 'a{ss}') def can_upper_case_on_button(self, button): kpms = self.key_press_modifiers if not kpms: # speed up the empty case return False key = "button" + str(button) return kpms.get(key) == "SHIFT" def set_upper_case_on_button(self, button, on): key = "button" + str(button) values = dict(self.key_press_modifiers) # must be a copy if on: values[key] = "SHIFT" else: if key in values: del values[key] self.key_press_modifiers = values class ConfigWindow(ConfigObject): """Window configuration """ DEFAULT_DOCKING_EDGE = DockingEdge.BOTTOM DEFAULT_DOCKING_MONITOR = DockingMonitor.ACTIVE def _init_keys(self): self.schema = SCHEMA_WINDOW self.sysdef_section = "window" self.add_key("window-state-sticky", True) self.add_key("window-decoration", False) self.add_key("force-to-top", False) self.add_key("keep-aspect-ratio", False) self.add_key("transparent-background", False) self.add_key("transparency", 0.0) self.add_key("background-transparency", 10.0) self.add_key("enable-inactive-transparency", False) self.add_key("inactive-transparency", 50.0) self.add_key("inactive-transparency-delay", 1.0) self.add_key("window-handles", DEFAULT_WINDOW_HANDLES) self.add_key("docking-enabled", False) self.add_key("docking-edge", self.DEFAULT_DOCKING_EDGE, enum={"top" : DockingEdge.TOP, "bottom" : DockingEdge.BOTTOM, }) self.add_key("docking-monitor", self.DEFAULT_DOCKING_MONITOR, enum={"active" : DockingMonitor.ACTIVE, "primary" : DockingMonitor.PRIMARY, "monitor0" : DockingMonitor.MONITOR0, "monitor1" : DockingMonitor.MONITOR1, "monitor2" : DockingMonitor.MONITOR2, "monitor3" : DockingMonitor.MONITOR3, "monitor4" : DockingMonitor.MONITOR4, "monitor5" : DockingMonitor.MONITOR5, "monitor6" : DockingMonitor.MONITOR6, "monitor7" : DockingMonitor.MONITOR7, "monitor8" : DockingMonitor.MONITOR8, }) self.add_key("docking-shrink-workarea", True) self.add_key("docking-aspect-change-range", [0, 1.6], "ad") self.landscape = ConfigWindow.Landscape(self) self.portrait = ConfigWindow.Portrait(self) self.children = [self.landscape, self.portrait] ##### property helpers ##### def _convert_sysdef_key(self, gskey, sysdef, value): if sysdef == "resize-handles" or \ sysdef == "window-handles": return Config._string_to_handles(value) else: return ConfigObject._convert_sysdef_key(self, gskey, sysdef, value) def _unpack_window_handles(self, value): return Config._string_to_handles(value) def _pack_window_handles(self, value): return Config._handles_to_string(value) def position_notify_add(self, callback): self.landscape.x_notify_add(callback) self.landscape.y_notify_add(callback) self.portrait.x_notify_add(callback) self.portrait.y_notify_add(callback) def size_notify_add(self, callback): self.landscape.width_notify_add(callback) self.landscape.height_notify_add(callback) self.portrait.width_notify_add(callback) self.portrait.height_notify_add(callback) def dock_size_notify_add(self, callback): self.landscape.dock_width_notify_add(callback) self.landscape.dock_height_notify_add(callback) self.portrait.dock_width_notify_add(callback) self.portrait.dock_height_notify_add(callback) def docking_notify_add(self, callback): self.docking_enabled_notify_add(callback) self.docking_edge_notify_add(callback) self.docking_monitor_notify_add(callback) self.docking_shrink_workarea_notify_add(callback) self.landscape.dock_expand_notify_add(callback) self.portrait.dock_expand_notify_add(callback) def get_active_opacity(self): return 1.0 - self.transparency / 100.0 def get_inactive_opacity(self): return 1.0 - self.inactive_transparency / 100.0 def get_minimal_opacity(self): # Return the lowest opacity the window can have when visible. return min(self.get_active_opacity(), self.get_inactive_opacity()) def get_background_opacity(self): return 1.0 - self.background_transparency / 100.0 class Landscape(ConfigObject): def _init_keys(self): self.schema = SCHEMA_WINDOW_LANDSCAPE self.sysdef_section = "window.landscape" self.add_key("x", DEFAULT_X) self.add_key("y", DEFAULT_Y) self.add_key("width", DEFAULT_WIDTH) self.add_key("height", DEFAULT_HEIGHT) self.add_key("dock-width", DEFAULT_WIDTH) self.add_key("dock-height", DEFAULT_HEIGHT) self.add_key("dock-expand", True) class Portrait(ConfigObject): def _init_keys(self): self.schema = SCHEMA_WINDOW_PORTRAIT self.sysdef_section = "window.portrait" self.add_key("x", DEFAULT_X) self.add_key("y", DEFAULT_Y) self.add_key("width", DEFAULT_WIDTH) self.add_key("height", DEFAULT_HEIGHT) self.add_key("dock-width", DEFAULT_WIDTH) self.add_key("dock-height", DEFAULT_HEIGHT) self.add_key("dock-expand", True) class ConfigICP(ConfigObject): """ Icon palette configuration """ def _init_keys(self): self.schema = SCHEMA_ICP self.sysdef_section = "icon-palette" self.add_key("in-use", False) self.add_key("window-handles", DEFAULT_WINDOW_HANDLES) self.landscape = ConfigICP.Landscape(self) self.portrait = ConfigICP.Portrait(self) self.children = [self.landscape, self.portrait] ##### property helpers ##### def _convert_sysdef_key(self, gskey, sysdef, value): if sysdef == "resize-handles" or \ sysdef == "window-handles": return Config._string_to_handles(value) else: return ConfigObject._convert_sysdef_key(self, gskey, sysdef, value) def _unpack_window_handles(self, value): return Config._string_to_handles(value) def _pack_window_handles(self, value): return Config._handles_to_string(value) def position_notify_add(self, callback): self.landscape.x_notify_add(callback) self.landscape.y_notify_add(callback) self.portrait.x_notify_add(callback) self.portrait.y_notify_add(callback) def size_notify_add(self, callback): self.landscape.width_notify_add(callback) self.landscape.height_notify_add(callback) self.portrait.width_notify_add(callback) self.portrait.height_notify_add(callback) class Landscape(ConfigObject): def _init_keys(self): self.schema = SCHEMA_ICP_LANDSCAPE self.sysdef_section = "icon-palette.landscape" self.add_key("x", DEFAULT_ICP_X) self.add_key("y", DEFAULT_ICP_Y) self.add_key("width", DEFAULT_ICP_WIDTH) self.add_key("height", DEFAULT_ICP_HEIGHT) class Portrait(ConfigObject): def _init_keys(self): self.schema = SCHEMA_ICP_PORTRAIT self.sysdef_section = "icon-palette.portrait" self.add_key("x", DEFAULT_ICP_X) self.add_key("y", DEFAULT_ICP_Y) self.add_key("width", DEFAULT_ICP_WIDTH) self.add_key("height", DEFAULT_ICP_HEIGHT) class ConfigAutoShow(ConfigObject): """ auto_show configuration """ DEFAULT_REPOSITION_METHOD = RepositionMethodEnum.PREVENT_OCCLUSION def _init_keys(self): self.schema = SCHEMA_AUTO_SHOW self.sysdef_section = "auto-show" self.add_key("enabled", False) enum = { "none" : 0, "prevent-occlusion" : RepositionMethodEnum.PREVENT_OCCLUSION, "reduce-travel" : RepositionMethodEnum.REDUCE_POINTER_TRAVEL, } self.add_key("reposition-method-floating", self.DEFAULT_REPOSITION_METHOD, enum=enum) self.add_key("reposition-method-docked", self.DEFAULT_REPOSITION_METHOD, enum=enum) self.add_key("widget-clearance", (25.0, 55.0, 25.0, 40.0), '(dddd)') self.add_key("hide-on-key-press", True) self.add_key("hide-on-key-press-pause", 1800.0) self.add_key("tablet-mode-detection-enabled", True) self.add_key("tablet-mode-enter-key", 0) self.add_key("tablet-mode-leave-key", 0) self.add_key("tablet-mode-state-file", "") self.add_key("tablet-mode-state-file-pattern", "1") self.add_key("keyboard-device-detection-enabled", False) self.add_key("keyboard-device-detection-exceptions", []) def tablet_mode_detection_notify_add(self, callback): self.tablet_mode_detection_enabled_notify_add(callback) self.tablet_mode_enter_key_notify_add(callback) self.tablet_mode_leave_key_notify_add(callback) class ConfigUniversalAccess(ConfigObject): """ universal_access configuration """ def _init_keys(self): self.schema = SCHEMA_UNIVERSAL_ACCESS self.sysdef_section = "universal-access" self.add_key("drag-threshold", -1) self.add_key("hide-click-type-window", True) self.add_key("enable-click-type-window-on-exit", True) def _post_notify_hide_click_type_window(self): """ called when changed in gsettings (preferences window) """ mousetweaks = self.parent.mousetweaks if mousetweaks: mousetweaks.on_hide_click_type_window_changed( self.hide_click_type_window) class ConfigTheme(ConfigObject): """ Theme configuration """ def _init_keys(self): self.schema = SCHEMA_THEME self.sysdef_section = "theme-settings" self.add_key("color-scheme", DEFAULT_COLOR_SCHEME, prop="color_scheme_filename") self.add_key("background-gradient", 0.0) self.add_key("key-style", "flat") self.add_key("roundrect-radius", 0.0) self.add_key("key-size", 100.0) self.add_key("key-stroke-width", 100.0) self.add_key("key-fill-gradient", 0.0) self.add_key("key-stroke-gradient", 0.0) self.add_key("key-gradient-direction", 0.0) self.key_label_font_key = \ self.add_key("key-label-font", "") # font for current theme self.key_label_overrides_key = \ self.add_key("key-label-overrides", {}, "as") # labels for current theme self.add_key("key-shadow-strength", 20.0) self.add_key("key-shadow-size", 5.0) ##### property helpers ##### def theme_attributes_notify_add(self, callback): self.background_gradient_notify_add(callback) self.key_style_notify_add(callback) self.roundrect_radius_notify_add(callback) self.key_size_notify_add(callback) self.key_stroke_width_notify_add(callback) self.key_fill_gradient_notify_add(callback) self.key_stroke_gradient_notify_add(callback) self.key_gradient_direction_notify_add(callback) self.key_label_font_notify_add(callback) self.key_label_overrides_notify_add(callback) self.key_style_notify_add(callback) self.key_shadow_strength_notify_add(callback) self.key_shadow_size_notify_add(callback) def _can_set_color_scheme_filename(self, filename): if not os.path.exists(filename): _logger.warning(_format("color scheme '{filename}' does not exist", \ filename=filename)) return False return True def _unpack_key_label_overrides(self, value): return self.unpack_string_list(value, "a{s[ss]}") def _pack_key_label_overrides(self, value): return self.pack_string_list(value) def get_key_label_overrides(self): gskey = self.key_label_overrides_key # merge with default value from onboard base config value = dict(self.parent.key_label_overrides) value.update(gskey.value) return value _font_attributes = ("bold", "italic", "condensed") _cached_key_label_font = None def _post_notify_key_label_font(self): self._cached_key_label_font = None def get_key_label_font(self): if self._cached_key_label_font is None: gskey = self.key_label_font_key value = gskey.value if value: items = value.split() if items: # If no font family was provided merge in system font family if items[0] in self._font_attributes: system_items = self.parent.key_label_font.split() if system_items and \ system_items[0] not in self._font_attributes: items.insert(0, system_items[0]) value = " ".join(items) else: # get default value from onboard base config instead value = self.parent.key_label_font self._cached_key_label_font = value return self._cached_key_label_font class ConfigLockdown(ConfigObject): """ Lockdown/Kiosk mode configuration """ def _init_keys(self): self.schema = SCHEMA_LOCKDOWN self.sysdef_section = "lockdown" self.add_key("disable-click-buttons", False) self.add_key("disable-hover-click", False) self.add_key("disable-dwell-activation", False) self.add_key("disable-preferences", False) self.add_key("disable-quit", False) self.add_key("disable-touch-handles", False) self.add_key("disable-keys", [["CTRL", "LALT", "F[0-9]+"]], 'aas') def lockdown_notify_add(self, callback): self.disable_click_buttons_notify_add(callback) self.disable_hover_click_notify_add(callback) self.disable_preferences_notify_add(callback) self.disable_quit_notify_add(callback) class ConfigGSS(ConfigObject): """ gnome-screen-saver configuration keys""" def _init_keys(self): self.schema = SCHEMA_GSS self.sysdef_section = "gnome-screen-saver" self.add_key("embedded-keyboard-enabled", True) self.add_key("embedded-keyboard-command", "") class ConfigGDI(ConfigObject): """ Key to enable Gnome Accessibility""" def _init_keys(self): self.schema = SCHEMA_GDI self.sysdef_section = "gnome-desktop-interface" self.add_key("toolkit-accessibility", False) self.add_key("gtk-theme", "", writable=False) # read-only for safety class ConfigGDA(ConfigObject): """ Key to check if a11y keyboard is enabled """ def _init_keys(self): self.schema = SCHEMA_GDA self.sysdef_section = "gnome-desktop-a11y-applications" # read-only for safety self.add_key("screen-keyboard-enabled", False, writable=False) class ConfigUnityGreeter(ConfigObject): """ Key to hide onboard when embedded into unity-greeter """ def _init_keys(self): self.schema = SCHEMA_UNITY_GREETER self.sysdef_section = "unity-greeter" self.add_key("onscreen-keyboard", False) class ConfigScanner(ConfigObject): """ Scanner configuration """ DEFAULT_INTERVAL = 1.20 DEFAULT_INTERVAL_FAST = 0.05 DEFAULT_MODE = 0 # AutoScan DEFAULT_CYCLES = 2 DEFAULT_BACKTRACK = 5 DEFAULT_ALTERNATE = False DEFAULT_USER_SCAN = False DEFAULT_DEVICE_NAME = "Default" DEFAULT_DEVICE_KEY_MAP = {} DEFAULT_DEVICE_BUTTON_MAP = { 1: 0, 3: 5 } # Button 1: Step, Button 3: Activate DEFAULT_FEEDBACK_FLASH = True def _init_keys(self): self.schema = SCHEMA_SCANNER self.sysdef_section = "scanner" self.add_key("enabled", False) self.add_key("mode", self.DEFAULT_MODE, enum={"Autoscan" : 0, "Overscan" : 1, "Stepscan" : 2, "Directed" : 3}) self.add_key("interval", self.DEFAULT_INTERVAL) self.add_key("interval-fast", self.DEFAULT_INTERVAL_FAST) self.add_key("cycles", self.DEFAULT_CYCLES) self.add_key("backtrack", self.DEFAULT_BACKTRACK) self.add_key("alternate", self.DEFAULT_ALTERNATE) self.add_key("user-scan", self.DEFAULT_USER_SCAN) self.add_key("device-name", self.DEFAULT_DEVICE_NAME) self.add_key("device-detach", False) self.add_key("device-key-map", self.DEFAULT_DEVICE_KEY_MAP, 'a{ii}') self.add_key("device-button-map", self.DEFAULT_DEVICE_BUTTON_MAP, 'a{ii}') self.add_key("feedback-flash", self.DEFAULT_FEEDBACK_FLASH) class ConfigTypingAssistance(ConfigObject): """ typing-assistance configuration keys""" DEFAULT_BACKEND = 0 def _init_keys(self): self.schema = SCHEMA_TYPING_ASSISTANCE self.sysdef_section = "typing-assistance" self.add_key("active-language", "") self.add_key("recent-languages", [], 'as') self.add_key("max-recent-languages", 5) self.add_key("spell-check-backend", self.DEFAULT_BACKEND, enum={"hunspell" : 0, "aspell" : 1}) self.add_key("auto-capitalization", False) self.add_key("auto-correction", False) self.word_suggestions = ConfigWordSuggestions(self) self.children = [self.word_suggestions] # shortcuts in the root for convenient access self.get_root().wp = self.word_suggestions self.get_root().word_suggestions = self.word_suggestions class ConfigWordSuggestions(ConfigObject): """ word-suggestions configuration keys""" # wordlist_buttons KEY_ID_PREVIOUS_PREDICTIONS = "previous-predictions" KEY_ID_NEXT_PREDICTIONS = "next-predictions" KEY_ID_LANGUAGE = "language" KEY_ID_PAUSE_LEARNING = "pause-learning" KEY_ID_HIDE = "hide" KEY_ORDER = [KEY_ID_PREVIOUS_PREDICTIONS, KEY_ID_NEXT_PREDICTIONS, KEY_ID_PAUSE_LEARNING, KEY_ID_LANGUAGE, KEY_ID_HIDE] def _init_keys(self): self.schema = SCHEMA_WORD_SUGGESTIONS self.sysdef_section = "word-suggestions" self.add_key("enabled", False) self.add_key("auto-learn", True) self.add_key("punctuation-assistance", True) self.add_key("delayed-word-separators-enabled", False) self.add_key("accent-insensitive", True) self.add_key("max-word-choices", 5) self.add_key("spelling-suggestions-enabled", True) self.add_key("wordlist-buttons", [self.KEY_ID_PREVIOUS_PREDICTIONS, self.KEY_ID_NEXT_PREDICTIONS, self.KEY_ID_LANGUAGE, self.KEY_ID_HIDE]) self.add_key("pause-learning-locked", False) self.add_key("learning-behavior-paused", LearningBehavior.NOTHING, enum={"nothing" : LearningBehavior.NOTHING, "known-only" : LearningBehavior.KNOWN_ONLY}) # 0=off, 1=latched, 2=locked; not in gsettings self._pause_learning = 0 # deprecated self.add_key("stealth-mode", False) self.add_key("show-context-line", False) def word_prediction_notify_add(self, callback): self.auto_learn_notify_add(callback) self.punctuation_assistance_notify_add(callback) self.stealth_mode_notify_add(callback) def can_auto_learn(self): return self.enabled and \ self.auto_learn and \ (not self.is_learning_paused() or \ self.learning_behavior_paused != \ LearningBehavior.NOTHING) and \ not self.stealth_mode def is_learning_paused(self): return self.get_pause_learning() > 0 def get_pause_learning(self): if self.pause_learning_locked: return 2 else: return self._pause_learning def set_pause_learning(self, value): self._pause_learning = value self.pause_learning_locked = value == 2 def _post_notify_pause_learning_locked(self): self._pause_learning = 2 if self.pause_learning_locked else 0 def get_shown_wordlist_button_ids(self): result = [] for button_id in self.wordlist_buttons: if button_id != self.KEY_ID_PAUSE_LEARNING or \ self.can_show_pause_learning_button(): result.append(button_id) return result def show_wordlist_button(self, key_id, show): """ Doctests: >>> co = ConfigWordSuggestions() >>> ConfigWordSuggestions.wordlist_buttons = [] >>> co.wordlist_buttons = [] >>> co.show_wordlist_button(co.KEY_ID_HIDE, True) >>> print(co.wordlist_buttons) ['hide'] >>> co.wordlist_buttons = [] >>> co.show_wordlist_button(co.KEY_ID_LANGUAGE, True) >>> print(co.wordlist_buttons) ['language'] >>> co.wordlist_buttons = [] >>> co.show_wordlist_button("notabutton", True) >>> print(co.wordlist_buttons) [] >>> co.wordlist_buttons = [co.KEY_ID_PAUSE_LEARNING, co.KEY_ID_HIDE] >>> co.show_wordlist_button(co.KEY_ID_LANGUAGE, True) >>> print(co.wordlist_buttons) ['pause-learning', 'language', 'hide'] >>> co.wordlist_buttons = [co.KEY_ID_LANGUAGE, co.KEY_ID_HIDE] >>> co.show_wordlist_button(co.KEY_ID_PAUSE_LEARNING, True) >>> print(co.wordlist_buttons) ['pause-learning', 'language', 'hide'] """ buttons = self.wordlist_buttons[:] if show: if key_id not in buttons: priority = self._get_button_priority(key_id) if priority >= 0: for i, button in enumerate(buttons): p = self._get_button_priority(button) if priority < p: buttons.insert(i, key_id) break else: buttons.append(key_id) self.wordlist_buttons = buttons else: if key_id in buttons: buttons.remove(key_id) self.wordlist_buttons = buttons @staticmethod def _get_button_priority(key_id): try: return ConfigWordSuggestions.KEY_ORDER.index(key_id) except ValueError: return -1 def can_show_language_button(self): return self.KEY_ID_LANGUAGE in self.wordlist_buttons def can_show_pause_learning_button(self): return self.auto_learn and \ self.KEY_ID_PAUSE_LEARNING in self.wordlist_buttons def can_show_more_predictions_button(self): return self.KEY_ID_NEXT_PREDICTIONS in self.wordlist_buttons def can_learn_new_words(self): return not self.is_learning_paused()