Exemplo n.º 1
0
    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()
Exemplo n.º 2
0
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)