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", {}) self.add_key("snippets", {}) self.add_key("show-status-icon", True) self.add_key("start-minimized", False) self.add_key("xembed-onboard", False, "onboard_xembed_enabled") self.add_key("show-tooltips", True) self.add_key("key-label-font", "") # default font for all themes self.add_key("key-label-overrides", {}) # default labels for all themes self.add_key("current-settings-page", 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.gss = ConfigGSS(self) self.gdi = ConfigGDI(self) self.scanner = ConfigScanner(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] try: self.mousetweaks = Mousetweaks() self.children.append(self.mousetweaks) except (SchemaError, ImportError) as e: _logger.warning(unicode_str(e)) self.mousetweaks = None self.clickmapper = ClickMapper()
class Config(ConfigObject): """ Singleton Class to encapsulate the gsettings stuff and check values. """ # String representation of the module containing the Keyboard mixin # used to draw keyboard _kbd_render_mixin_mod = GTK_KBD_MIXIN_MOD # String representation of the keyboard mixin used to draw keyboard. _kbd_render_mixin_cls = GTK_KBD_MIXIN_CLS # 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 = 5.0 # radius of the rounded window corners CORNER_RADIUS = 10 # y displacement of the key face of dish keys DISH_KEY_Y_OFFSET = 1.0 # raised border size of dish keys DISH_KEY_BORDER = (2.5, 2.5) # 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 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 to runs before the single instance check. Only do the bare minimum here. """ # parse command line parser = OptionParser() parser.add_option("-l", "--layout", dest="layout", help="Specify layout file ({}) or name" \ .format(self.LAYOUT_FILE_EXTENSION)) parser.add_option("-t", "--theme", dest="theme", help="Specify theme file (.theme) or name") parser.add_option("-x", type="int", dest="x", help="x coord of window") parser.add_option("-y", type="int", dest="y", help="y coord of window") parser.add_option("-s", "--size", dest="size", help="size widthxheight") parser.add_option("-e", "--xid", action="store_true", dest="xid_mode", help="XEmbed mode for gnome-screensaver") parser.add_option("-a", "--keep-aspect", action="store_true", dest="keep_aspect_ratio", help="Keep aspect ratio when resizing the window") parser.add_option("-d", "--debug", type="str", dest="debug", help="DEBUG={notset|debug|info|warning|error|critical}") parser.add_option("-m", "--allow-multiple-instances", action="store_true", dest="allow_multiple_instances", help="Allow multiple Onboard instances") options = parser.parse_args()[0] self.options = options self.xid_mode = options.xid_mode # setup logging log_params = { "format" : '%(asctime)s:%(levelname)s:%(name)s: %(message)s' } if options.debug: log_params["level"] = getattr(logging, options.debug.upper()) if False: # log to file log_params["level"] = "DEBUG" log_params["filename"] = "/tmp/onboard.log" log_params["filemode"] = "w" logging.basicConfig(**log_params) 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() # init paths self.install_dir = self._get_install_dir() self.user_dir = self._get_user_dir() # migrate old user dir ".sok" to ".onboard" old_user_dir = os.path.join(os.path.expanduser("~"), ".sok") user_dir = self.user_dir if not os.path.exists(user_dir) and os.path.exists(old_user_dir): _logger.info(_("Migrating user directory '{}' to '{}'.") \ .format(old_user_dir, user_dir)) try: copytree(old_user_dir, user_dir) except OSError as ex: # python >2.5 _logger.error(_("Failed to migrate user directory. ") + \ unicode_str(ex)) # Load system defaults (if there are any, not required). # Used for distribution specific settings, aka branding. paths = [os.path.join(self.install_dir, SYSTEM_DEFAULTS_FILENAME), os.path.join("/etc/onboard", SYSTEM_DEFAULTS_FILENAME)] self.load_system_defaults(paths) # initialize all property values self.init_properties(self.options) # Make sure there is a 'Default' entry when tracking the system theme. # 'Default' is the theme used when encountering an so far unknown # gtk-theme. 'Default' is added on first start and therefore a # corresponding system default is respected. theme_assocs = self.system_theme_associations if not "Default" in theme_assocs: theme_assocs["Default"] = self.theme self.system_theme_associations = theme_assocs # remember command line theme for system theme tracking if self.options.theme: self.remember_theme(self.theme) # load theme global Theme from Onboard.Appearance import Theme self.apply_theme() # give gtk theme a chance to take over self.update_theme_from_system_theme() # misc initializations self._last_snippets = dict(self.snippets) # store a copy # remember state of mousetweaks click-type window if self.mousetweaks: self.mousetweaks.old_click_type_window_visible = \ self.mousetweaks.click_type_window_visible if self.mousetweaks.is_active() and \ self.universal_access.hide_click_type_window: self.mousetweaks.click_type_window_visible = False # 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() _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() self.clickmapper.cleanup() if self.mousetweaks: self.mousetweaks.cleanup() def final_cleanup(self): if self.mousetweaks: if self.xid_mode: self.mousetweaks.click_type_window_visible = \ self.mousetweaks.old_click_type_window_visible else: if self.universal_access.enable_click_type_window_on_exit: self.mousetweaks.click_type_window_visible = True else: self.mousetweaks.click_type_window_visible = \ self.mousetweaks.old_click_type_window_visible 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", {}) self.add_key("snippets", {}) self.add_key("show-status-icon", True) self.add_key("start-minimized", False) self.add_key("xembed-onboard", False, "onboard_xembed_enabled") self.add_key("show-tooltips", True) self.add_key("key-label-font", "") # default font for all themes self.add_key("key-label-overrides", {}) # default labels for all themes self.add_key("current-settings-page", 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.gss = ConfigGSS(self) self.gdi = ConfigGDI(self) self.scanner = ConfigScanner(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] try: self.mousetweaks = Mousetweaks() self.children.append(self.mousetweaks) except (SchemaError, ImportError) as e: _logger.warning(unicode_str(e)) self.mousetweaks = None self.clickmapper = ClickMapper() def init_from_gsettings(self): """ Overloaded to migrate old dconf data to a new gsettings schema """ ConfigObject.init_from_gsettings(self) import osk util = osk.Util() def migrate_dconf_value(dconf_key, config_object, gskey): try: value = util.read_dconf_key(dconf_key) except (ValueError, TypeError) as e: value = None _logger.warning("migrate_dconf_value: {}".format(e)) if not value is None: setattr(config_object, gskey.prop, value) _logger.debug("migrate_dconf_value: {key} -> {path} {gskey}, value={value}" \ .format(key=dconf_key, path=co.schema, gskey=gskey.key, value=value)) def migrate_dconf_key(dconf_key, config_object, key): gskey = config_object.find_key(key) if gskey.is_default(): migrate_dconf_value(dconf_key, config_object, gskey) # --- onboard 0.96 -> 0.97 --------------------------------------------- format = Version.from_string(self.schema_version) if format < SCHEMA_VERSION_0_97: # window rect moves from apps.onboard to # apps.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.settings.delay() migrate_dconf_value("/apps/onboard/x", co, co.gskeys["x"]) migrate_dconf_value("/apps/onboard/y", co, co.gskeys["y"]) migrate_dconf_value("/apps/onboard/width", co, co.gskeys["width"]) migrate_dconf_value("/apps/onboard/height", co, co.gskeys["height"]) co.settings.apply() # icon-palette rect moves from apps.onboard.icon-palette to # apps.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.settings.delay() migrate_dconf_value("/apps/onboard/icon-palette/x", co, co.gskeys["x"]) migrate_dconf_value("/apps/onboard/icon-palette/y", co, co.gskeys["y"]) migrate_dconf_value("/apps/onboard/icon-palette/width", co, co.gskeys["width"]) migrate_dconf_value("/apps/onboard/icon-palette/height", co, co.gskeys["height"]) co.settings.apply() # move keys from root to window co = self.window migrate_dconf_key("/apps/onboard/window-decoration", co, "window-decoration") migrate_dconf_key("/apps/onboard/force-to-top", co, "force-to-top") migrate_dconf_key("/apps/onboard/transparent-background", co, "transparent-background") migrate_dconf_key("/apps/onboard/transparency", co, "transparency") migrate_dconf_key("/apps/onboard/background-transparency", co, "background-transparency") migrate_dconf_key("/apps/onboard/enable-inactive-transparency", co, "enable-inactive-transparency") migrate_dconf_key("/apps/onboard/inactive-transparency", co, "inactive-transparency") migrate_dconf_key("/apps/onboard/inactive-transparency-delay", co, "inactive-transparency-delay") # accessibility keys move from root to universal-access co = self.universal_access migrate_dconf_key("/apps/onboard/hide-click-type-window", co, "hide-click-type-window") migrate_dconf_key("/apps/onboard/enable-click-type-window-on-exit", co, "enable-click-type-window-on-exit") # move keys from root to keyboard co = self.keyboard migrate_dconf_key("/apps/onboard/show-click-buttons", co, "show-click-buttons") self.schema_version = SCHEMA_VERSION.to_string() ##### 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 _gsettings_get_key_label_overrides(self, gskey): return self.get_unpacked_string_list(gskey, "a{s[ss]}") def _gsettings_set_key_label_overrides(self, gskey, value): self.set_packed_string_list(gskey, value) def _gsettings_get_snippets(self, gskey): return self.get_unpacked_string_list(gskey, "a{i[ss]}") def _gsettings_set_snippets(self, gskey, value): self.set_packed_string_list(gskey, value) # 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): return self._get_user_sys_filename_gs( gskey = self.layout_key, user_filename_func = lambda x: \ os.path.join(self.user_dir, "layouts", x) + \ self.LAYOUT_FILE_EXTENSION, system_filename_func = lambda x: \ os.path.join(self.install_dir, "layouts", x) + \ self.LAYOUT_FILE_EXTENSION, final_fallback = 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(_("layout '{filename}' does not exist") \ .format(filename=filename)) layout_filename = property(get_layout_filename, set_layout_filename) # 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): return self._get_user_sys_filename_gs( gskey = self.theme_key, user_filename_func = Theme.build_user_filename, system_filename_func = Theme.build_system_filename, final_fallback = os.path.join(self.install_dir, "themes", DEFAULT_THEME + "." + Theme.extension())) def set_theme_filename(self, filename, save = True): if filename and os.path.exists(filename): self.set_theme(filename, save) # remember currently active gtk theme if self.system_theme_tracking_enabled: self.remember_theme(filename) else: _logger.warning(_("theme '{filename}' does not exist") \ .format(filename=filename)) theme_filename = property(get_theme_filename, set_theme_filename) def remember_theme(self, theme_filename): if self.gdi: # be defensive gtk_theme = self.get_gtk_theme() theme_assocs = self.system_theme_associations theme_assocs[gtk_theme] = theme_filename self.system_theme_associations = theme_assocs def _gsettings_get_system_theme_associations(self, gskey): return gskey.settings.get_value(gskey.key).unpack() def _gsettings_set_system_theme_associations(self, gskey, value): gskey.settings.set_value(gskey.key, GLib.Variant('a{ss}', value)) def apply_theme(self): theme_filename = self.theme_filename _logger.info(_("Loading theme from '{}'").format(theme_filename)) theme = Theme.load(theme_filename) if not theme: _logger.error(_("Unable to read theme '{}'").format(theme_filename)) else: # Save to gsettings # Make sure gsettings is in sync with onboard (LP: 877601) self.theme = 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.settings.apply() def update_theme_from_system_theme(self): """ Switches themes for system theme tracking """ if self.system_theme_tracking_enabled: gtk_theme = self.get_gtk_theme() theme_assocs = self.system_theme_associations new_theme = theme_assocs.get(gtk_theme, None) if not new_theme: new_theme = theme_assocs.get("Default", None) if not new_theme: new_theme = DEFAULT_THEME self.theme = new_theme self.apply_theme() 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.' """ return self._get_user_sys_filename( filename = image_filename, description = "image", user_filename_func = lambda x: \ os.path.join(self.user_dir, "layouts", "images", x), system_filename_func = lambda x: \ os.path.join(self.install_dir, "layouts", "images", x)) def allow_system_click_type_window(self, allow): """ called from hover click button """ if not self.mousetweaks: return # This assumes that mousetweaks.click_type_window_visible never # changes between activation and deactivation of mousetweaks. if allow: self.mousetweaks.click_type_window_visible = \ self.mousetweaks.old_click_type_window_visible else: # hide the mousetweaks window when onboard's settings say so if self.universal_access.hide_click_type_window: self.mousetweaks.old_click_type_window_visible = \ self.mousetweaks.click_type_window_visible self.mousetweaks.click_type_window_visible = False def enable_hover_click(self, enable): if enable: self.allow_system_click_type_window(False) self.mousetweaks.set_active(True) else: self.mousetweaks.set_active(False) self.allow_system_click_type_window(True) def is_visible_on_start(self): return not self.xid_mode and \ 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 get_frame_width(self): """ width of the frame around the keyboard """ if self.xid_mode: return 1.0 elif self.has_window_decoration(): return 0.0 elif self.window.transparent_background: return 1.0 else: return self.UNDECORATED_FRAME_WIDTH 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) 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.window.force_to_top def get_sticky_state(self): return not self.xid_mode and \ (self.window.window_state_sticky or self.window.force_to_top) def is_inactive_transparency_enabled(self): return self.window.enable_inactive_transparency and \ not self.scanner.enabled def is_keep_aspect_ratio_enabled(self): return self.window.keep_aspect_ratio or self.options.keep_aspect_ratio ####### resize handles ####### def resize_handles_notify_add(self, callback): self.window.resize_handles_notify_add(callback) self.icp.resize_handles_notify_add(callback) def get_num_resize_handles(self): """ Translate array of handles to simplified NumResizeHandles enum """ handles = self.window.resize_handles if len(handles) == 0: return NumResizeHandles.NONE if len(handles) == 8: return NumResizeHandles.ALL return NumResizeHandles.SOME def set_num_resize_handles(self, num): if num == NumResizeHandles.ALL: window_handles = list(Handle.RESIZERS) icp_handles = list(Handle.RESIZERS) elif num == NumResizeHandles.NONE: window_handles = [] icp_handles = [] else: window_handles = list(Handle.CORNERS) icp_handles = [Handle.SOUTH_EAST] self.window.resize_handles = window_handles self.icp.resize_handles = icp_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_install_dir(self): result = None # when run from source src_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) src_data_path = os.path.join(src_path, "data") if os.path.isfile(os.path.join(src_data_path, "onboard.gschema.xml")): # Add the data directory to the icon search path icon_theme = Gtk.IconTheme.get_default() 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 os.path.join(os.path.expanduser("~"), USER_DIR)