Exemple #1
0
    def __init__(self):
        self._opacity = 1.0

        WindowRectTracker.__init__(self)

        args = {
            "skip_taskbar_hint": True,
            "skip_pager_hint": True,
            "urgency_hint": False,
            "decorated": False,
            "accept_focus": False,
            "opacity": 1.0,
            "app_paintable": True,
        }
        if gtk_has_resize_grip_support():
            args["has_resize_grip"] = False

        Gtk.Window.__init__(self, **args)

        self.set_keep_above(True)

        # In Precise, directly drawing on the top level window has no effect.
        # The Cairo target surface is correctly rendered, but somehow it
        # doesn't become visible. Compositing or not doesn't matter.
        # It's most likely an old issue with Gtk/Gdk. Later releases like
        # Trusty, Vivid are unaffected.
        # -> Create a widget we can successfully draw on anywhere.
        self.drawing_area = Gtk.DrawingArea()
        self.add(self.drawing_area)
        self.drawing_area.connect("draw", self.on_draw)

        # use transparency if available
        screen = Gdk.Screen.get_default()
        visual = screen.get_rgba_visual()
        self.supports_alpha = False
        if visual:
            self.set_visual(visual)
            self.drawing_area.set_visual(visual)

            # Somehow Gtk 3.4 still needs these now deprecated calls
            # for LayoutPopups, even though IconPalette and LabelPopups
            # don't. Otherwise there will be a white background.
            gtk_version = Version(Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION)
            if gtk_version < Version(3, 18):  # Xenial doesn't need them
                self.override_background_color(Gtk.StateFlags.NORMAL,
                                               Gdk.RGBA(0, 0, 0, 0))
                self.drawing_area.override_background_color(
                    Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 0))
            self.supports_alpha = True
Exemple #2
0
    def _load_layout(self, layout_filename, color_scheme_filename):
        self.layout_dir = os.path.dirname(layout_filename)
        self.svg_cache = {}
        layout = None

        if color_scheme_filename:
            self.color_scheme = ColorScheme.load(color_scheme_filename)

        f = open(layout_filename)
        try:
            dom = minidom.parse(f).documentElement

            # check layout format
            format = self.LAYOUT_FORMAT_LEGACY
            if dom.hasAttribute("format"):
               format = Version.from_string(dom.attributes["format"].value)

            if format >= self.LAYOUT_FORMAT_LAYOUT_TREE:
                items = self._parse_dom_node(dom)
            else:
                _logger.warning(_("Loading legacy layout format '{}'. "
                            "Please consider upgrading to current format '{}'"
                            ).format(format, self.LAYOUT_FORMAT))
                items = self._parse_legacy_layout(dom)

            if items:
                layout = items[0]
        finally:
            f.close()

        self.svg_cache = {} # Free the memory
        return layout
    def load(filename, is_system=False):
        """ Load a color scheme and return it as a new instance. """

        color_scheme = None

        f = open_utf8(filename)
        try:
            dom = minidom.parse(f).documentElement
            name = dom.attributes["name"].value

            # check layout format
            format = ColorScheme.COLOR_SCHEME_FORMAT_LEGACY
            if dom.hasAttribute("format"):
                format = Version.from_string(dom.attributes["format"].value)

            if format >= ColorScheme.COLOR_SCHEME_FORMAT_TREE:  # tree format?
                items = ColorScheme._parse_dom_node(dom, None, {})
            else:
                _logger.warning(_format( \
                    "Loading legacy color scheme format '{old_format}', "
                    "please consider upgrading to current format "
                    "'{new_format}': '{filename}'",
                    old_format = format,
                    new_format = ColorScheme.COLOR_SCHEME_FORMAT,
                    filename = filename))

                items = ColorScheme._parse_legacy_color_scheme(dom)

            if not items is None:
                root = Root()
                root.set_items(items)

                color_scheme = ColorScheme()
                color_scheme.name = name
                color_scheme._filename = filename
                color_scheme.is_system = is_system
                color_scheme._root = root
                #print(root.dumps())
        except xml.parsers.expat.ExpatError as ex:
            _logger.error(
                _format(
                    "Error loading color scheme '{filename}'. "
                    "{exception}: {cause}",
                    filename=filename,
                    exception=type(ex).__name__,
                    cause=unicode_str(ex)))
        finally:
            f.close()

        return color_scheme
Exemple #4
0
    def load(filename, is_system=False):
        """ Load a color scheme and return it as a new instance. """

        color_scheme = None

        f = open_utf8(filename)
        try:
            dom = minidom.parse(f).documentElement
            name = dom.attributes["name"].value

            # check layout format
            format = ColorScheme.COLOR_SCHEME_FORMAT_LEGACY
            if dom.hasAttribute("format"):
               format = Version.from_string(dom.attributes["format"].value)

            if format >= ColorScheme.COLOR_SCHEME_FORMAT_TREE:   # tree format?
                items = ColorScheme._parse_dom_node(dom, None, {})
            else:
                _logger.warning(_format( \
                    "Loading legacy color scheme format '{old_format}', "
                    "please consider upgrading to current format "
                    "'{new_format}': '{filename}'",
                    old_format = format,
                    new_format = ColorScheme.COLOR_SCHEME_FORMAT,
                    filename = filename))

                items = ColorScheme._parse_legacy_color_scheme(dom)

            if  not items is None:
                root = Root()
                root.set_items(items)

                color_scheme = ColorScheme()
                color_scheme.name = name
                color_scheme._filename = filename
                color_scheme.is_system = is_system
                color_scheme._root = root
                #print(root.dumps())
        except xml.parsers.expat.ExpatError as ex:
            _logger.error(_format("Error loading color scheme '{filename}'. "
                                  "{exception}: {cause}",
                                  filename = filename,
                                  exception = type(ex).__name__,
                                  cause = unicode_str(ex)))
        finally:
            f.close()

        return color_scheme
    def _load_layout(self, layout_filename, parent_item=None):
        self._svg_cache = {}
        layout = None

        try:
            f = open_utf8(layout_filename)
        except FileNotFoundError as ex:
            _logger.warning("Failed to open '{}': {}".format(
                layout_filename, unicode_str(ex)))
            return None

        # make sure unlink is called
        with minidom.parse(f).documentElement as dom:

            # check layout format, no format version means legacy layout
            format = self.LAYOUT_FORMAT_LEGACY
            if dom.hasAttribute("format"):
                format = Version.from_string(dom.attributes["format"].value)
            self._format = format

            root = LayoutPanel()  # root, representing the 'keyboard' tag
            root.set_id("__root__")  # id for debug prints

            # Init included root with the parent item's svg filename.
            # -> Allows to skip specifying svg filenames in includes.
            if parent_item:
                root.filename = parent_item.filename

            if format >= self.LAYOUT_FORMAT_LAYOUT_TREE:
                self._parse_dom_node(dom, root)
                layout = root
            else:
                _logger.warning(
                    _format(
                        "Loading legacy layout, format '{}'. "
                        "Please consider upgrading to current format '{}'",
                        format, self.LAYOUT_FORMAT))
                items = self._parse_legacy_layout(dom)
                if items:
                    root.set_items(items)
                    layout = root

        f.close()

        self._svg_cache = {}  # Free the memory
        return layout
Exemple #6
0
    def load(filename, is_system=False):
        """ Load a color scheme and return it as a new object. """

        color_scheme = None

        f = open(filename)
        try:
            dom = minidom.parse(f).documentElement
            name = dom.attributes["name"].value

            # check layout format
            format = ColorScheme.COLOR_SCHEME_FORMAT_LEGACY
            if dom.hasAttribute("format"):
               format = Version.from_string(dom.attributes["format"].value)

            if format >= ColorScheme.COLOR_SCHEME_FORMAT_TREE:   # tree format?
                items = ColorScheme._parse_dom_node(dom, None, {})
            else:
                _logger.warning( \
                  _("Loading legacy color scheme format '{old_format}', "
                    "please consider upgrading to current format " \
                    "'{new_format}': '{filename}'") \
                .format(old_format = format, 
                        new_format = ColorScheme.COLOR_SCHEME_FORMAT,
                        filename = filename))

                items = ColorScheme._parse_legacy_color_scheme(dom)

            if  not items is None:
                root = Root()
                root.set_items(items)

                color_scheme = ColorScheme()
                color_scheme.name = name
                color_scheme.filename = filename
                color_scheme.is_system = is_system
                color_scheme.root = root
                #print(root.dumps())
        finally:
            f.close()

        return color_scheme
Exemple #7
0
    def _load_layout(self, layout_filename, parent_item = None):
        self._svg_cache = {}
        layout = None

        f = open_utf8(layout_filename)

        # make sure unlink is called
        with minidom.parse(f).documentElement as dom:

            # check layout format, no format version means legacy layout
            format = self.LAYOUT_FORMAT_LEGACY
            if dom.hasAttribute("format"):
               format = Version.from_string(dom.attributes["format"].value)
            self._format = format

            root = LayoutPanel() # root, representing the 'keyboard' tag
            root.set_id("__root__") # id for debug prints

            # Init included root with the parent item's svg filename.
            # -> Allows to skip specifying svg filenames in includes.
            if parent_item:
                root.filename = parent_item.filename

            if format >= self.LAYOUT_FORMAT_LAYOUT_TREE:
                self._parse_dom_node(dom, root)
                layout = root
            else:
                _logger.warning(_format("Loading legacy layout, format '{}'. "
                            "Please consider upgrading to current format '{}'",
                            format, self.LAYOUT_FORMAT))
                items = self._parse_legacy_layout(dom)
                if items:
                    root.set_items(items)
                    layout = root

        f.close()

        self._svg_cache = {} # Free the memory
        return layout
Exemple #8
0
    def copy_layout(src_filename, dst_filename):
        src_dir = os.path.dirname(src_filename)
        dst_dir, name_ext = os.path.split(dst_filename)
        dst_basename, ext = os.path.splitext(name_ext)
        _logger.info(_("copying layout '{}' to '{}'") \
                     .format(src_filename, dst_filename))

        domdoc = None
        svg_filenames = {}
        fallback_layers = {}

        with open(src_filename) as f:
            domdoc = minidom.parse(f)
            keyboard_node = domdoc.documentElement

            # check layout format
            format = KeyboardSVG.LAYOUT_FORMAT_LEGACY
            if keyboard_node.hasAttribute("format"):
               format = Version.from_string(keyboard_node.attributes["format"].value)
            keyboard_node.attributes["id"] = dst_basename

            if format < KeyboardSVG.LAYOUT_FORMAT_LAYOUT_TREE:
                raise Exceptions.LayoutFileError( \
                    _("copy_layouts failed, unsupported layout format '{}'.") \
                    .format(format))
            else:
                # replace the basename of all svg filenames
                for node in KeyboardSVG._iter_dom_nodes(keyboard_node):
                    if KeyboardSVG.is_layout_node(node):
                        if node.hasAttribute("filename"):
                            filename = node.attributes["filename"].value

                            # Create a replacement layer name for the unlikely
                            # case  that the svg-filename doesn't contain a
                            # layer section (as in path/basename-layer.ext).
                            fallback_layer_name = fallback_layers.get(filename,
                                         "Layer" + str(len(fallback_layers)))
                            fallback_layers[filename] = fallback_layer_name

                            # replace the basename of this filename
                            new_filename = KeyboardSVG._replace_basename( \
                                 filename, dst_basename, fallback_layer_name)

                            node.attributes["filename"].value = new_filename
                            svg_filenames[filename] = new_filename

        if domdoc:
            # write the new layout file
            with open(dst_filename, "w") as f:
                xml = toprettyxml(domdoc)
                f.write(xml.encode("UTF-8"))

                # copy the svg files
                for src, dst in list(svg_filenames.items()):

                    dir, name = os.path.split(src)
                    if not dir:
                        src = os.path.join(src_dir, name)
                    dir, name = os.path.split(dst)
                    if not dir:
                        dst = os.path.join(dst_dir, name)

                    _logger.info(_("copying svg file '{}' to '{}'") \
                                 .format(src, dst))
                    shutil.copyfile(src, dst)
Exemple #9
0
    def copy_layout(src_filename, dst_filename):
        src_dir = os.path.dirname(src_filename)
        dst_dir, name_ext = os.path.split(dst_filename)
        dst_basename, ext = os.path.splitext(name_ext)
        _logger.info(
            _format("copying layout '{}' to '{}'", src_filename, dst_filename))

        domdoc = None
        svg_filenames = {}
        fallback_layers = {}

        try:
            with open_utf8(src_filename) as f:
                domdoc = minidom.parse(f)
                keyboard_node = domdoc.documentElement

                # check layout format
                format = LayoutLoaderSVG.LAYOUT_FORMAT_LEGACY
                if keyboard_node.hasAttribute("format"):
                    format = Version.from_string(
                        keyboard_node.attributes["format"].value)
                keyboard_node.attributes["id"] = dst_basename

                if format < LayoutLoaderSVG.LAYOUT_FORMAT_LAYOUT_TREE:
                    raise Exceptions.LayoutFileError( \
                        _format("copy_layouts failed, unsupported layout format '{}'.",
                                format))
                else:
                    # replace the basename of all svg filenames
                    for node in LayoutLoaderSVG._iter_dom_nodes(keyboard_node):
                        if LayoutLoaderSVG.is_layout_node(node):
                            if node.hasAttribute("filename"):
                                filename = node.attributes["filename"].value

                                # Create a replacement layer name for the unlikely
                                # case  that the svg-filename doesn't contain a
                                # layer section (as in path/basename-layer.ext).
                                fallback_layer_name = fallback_layers.get(
                                    filename,
                                    "Layer" + str(len(fallback_layers)))
                                fallback_layers[filename] = fallback_layer_name

                                # replace the basename of this filename
                                new_filename = LayoutLoaderSVG._replace_basename( \
                                     filename, dst_basename, fallback_layer_name)

                                node.attributes[
                                    "filename"].value = new_filename
                                svg_filenames[filename] = new_filename

            if domdoc:
                XDGDirs.assure_user_dir_exists(config.get_user_layout_dir())

                # write the new layout file
                with open_utf8(dst_filename, "w") as f:
                    xml = toprettyxml(domdoc)
                    if sys.version_info.major == 2:  # python 2?
                        xml = xml.encode("UTF-8")
                    f.write(xml)

                    # copy the svg files
                    for src, dst in list(svg_filenames.items()):

                        dir, name = os.path.split(src)
                        if not dir:
                            src = os.path.join(src_dir, name)
                        dir, name = os.path.split(dst)
                        if not dir:
                            dst = os.path.join(dst_dir, name)

                        _logger.info(_format("copying svg file '{}' to '{}'", \
                                     src, dst))
                        shutil.copyfile(src, dst)
        except OSError as ex:
            _logger.error("copy_layout failed: " + \
                          unicode_str(ex))
        except Exceptions.LayoutFileError as ex:
            _logger.error(unicode_str(ex))
class Theme:
    """
    Theme controls the visual appearance of Onboards keyboard window.
    """
    # onboard 0.95
    THEME_FORMAT_INITIAL = Version(1, 0)

    # onboard 0.97, added key_size, switch most int values to float,
    # changed range of key_gradient_direction
    THEME_FORMAT_1_1 = Version(1, 1)

    # onboard 0.98, added shadow keys
    THEME_FORMAT_1_2 = Version(1, 2)

    # onboard 0.99, added key_stroke_width
    THEME_FORMAT_1_3 = Version(1, 3)

    THEME_FORMAT = THEME_FORMAT_1_3

    # core theme members
    # name, type, default
    attributes = [
        ["color_scheme_basename", "s", ""],
        ["background_gradient", "d", 0.0],
        ["key_style", "s", "flat"],
        ["roundrect_radius", "d", 0.0],
        ["key_size", "d", 100.0],
        ["key_stroke_width", "d", 100.0],
        ["key_fill_gradient", "d", 0.0],
        ["key_stroke_gradient", "d", 0.0],
        ["key_gradient_direction", "d", 0.0],
        ["key_label_font", "s", ""],
        ["key_label_overrides", "a{s[ss]}", {}],  # dict {name:(key:group)}
        ["key_shadow_strength", "d", 0.0],
        ["key_shadow_size", "d", 0.0],
    ]

    def __init__(self):
        self._modified = False

        self._filename = ""
        self._is_system = False  # True if this a system theme
        self._system_exists = False  # True if there exists a system
        #  theme with the same basename
        self._name = ""

        # create attributes
        for name, _type, default in self.attributes:
            setattr(self, name, default)

    @property
    def basename(self):
        """ Returns the file base name of the theme. """
        return os.path.splitext(os.path.basename(self._filename))[0]

    @property
    def filename(self):
        """ Returns the filename of the theme. """
        return self._filename

    def __eq__(self, other):
        if not other:
            return False
        for name, _type, _default in self.attributes:
            if getattr(self, name) != getattr(other, name):
                return False
        return True

    def __str__(self):
        return "name=%s, colors=%s, font=%s, radius=%d" % (
            self._name, self.color_scheme_basename, self.key_label_font,
            self.roundrect_radius)

    def apply(self, save=True):
        """ Applies the theme to config properties/gsettings. """
        filename = self.get_color_scheme_filename()
        if not filename:
            _logger.error(_format("Color scheme for theme '{filename}' not found", \
                                  filename=self._filename))
            return False

        config.theme_settings.set_color_scheme_filename(filename, save)
        for name, _type, _default in self.attributes:
            if name != "color_scheme_basename":
                getattr(config.theme_settings, "set_" + name) \
                                 (getattr(self, name), save)

        return True

    def get_color_scheme_filename(self):
        """ Returns the filename of the themes color scheme."""
        filename = os.path.join(Theme.user_path(),
                                self.color_scheme_basename) + \
                                "." + ColorScheme.extension()
        if not os.path.isfile(filename):
            filename = os.path.join(Theme.system_path(),
                                    self.color_scheme_basename) + \
                                    "." + ColorScheme.extension()
        if not os.path.isfile(filename):
            return None
        return filename

    def set_color_scheme_filename(self, filename):
        """ Set the filename of the color_scheme. """
        self.color_scheme_basename = \
                             os.path.splitext(os.path.basename(filename ))[0]

    def get_superkey_label(self):
        """ Returns the (potentially overridden) label of the super keys. """
        override = self.key_label_overrides.get("LWIN")
        if override:
            return override[0]  # assumes RWIN=LWIN
        return None

    def get_superkey_size_group(self):
        """
        Returns the (potentially overridden) size group of the super keys.
        """
        override = self.key_label_overrides.get("LWIN")
        if override:
            return override[1]  # assumes RWIN=LWIN
        return None

    def set_superkey_label(self, label, size_group):
        """ Sets or clears the override for left and right super key labels. """
        tuples = self.key_label_overrides
        if label is None:
            if "LWIN" in tuples:
                del tuples["LWIN"]
            if "RWIN" in tuples:
                del tuples["RWIN"]
        else:
            tuples["LWIN"] = (label, size_group)
            tuples["RWIN"] = (label, size_group)
        self.key_label_overrides = tuples

    @staticmethod
    def system_to_user_filename(filename):
        """ Returns the user filename for the given system filename. """
        basename = os.path.splitext(os.path.basename(filename))[0]
        return os.path.join(Theme.user_path(),
                            basename) + "." + Theme.extension()

    @staticmethod
    def build_user_filename(basename):
        """
        Returns a fully qualified filename pointing into the user directory
        """
        return os.path.join(Theme.user_path(),
                            basename) + "." + Theme.extension()

    @staticmethod
    def build_system_filename(basename):
        """
        Returns a fully qualified filename pointing into the system directory
        """
        return os.path.join(Theme.system_path(),
                            basename) + "." + Theme.extension()

    @staticmethod
    def user_path():
        """ Returns the path of the user directory for themes. """
        return os.path.join(config.user_dir, "themes")

    @staticmethod
    def system_path():
        """ Returns the path of the system directory for themes. """
        return os.path.join(config.install_dir, "themes")

    @staticmethod
    def extension():
        """ Returns the file extension of theme files """
        return "theme"

    @staticmethod
    def load_merged_themes():
        """
        Merge system and user themes.
        User themes take precedence and hide system themes.
        """
        system_themes = Theme.load_themes(True)
        user_themes = Theme.load_themes(False)
        themes = dict((t.basename, (t, None)) for t in system_themes)
        for theme in user_themes:
            # system theme hidden behind user theme?
            if theme.basename in themes:
                # keep the system theme behind the user theme
                themes[theme.basename] = (theme, themes[theme.basename][0])
            else:
                themes[theme.basename] = (theme, None)
        return themes

    @staticmethod
    def load_themes(is_system=False):
        """ Load all themes from either the user or the system directory. """
        themes = []

        if is_system:
            path = Theme.system_path()
        else:
            path = Theme.user_path()

        filenames = Theme.find_themes(path)
        for filename in filenames:
            theme = Theme.load(filename, is_system)
            if theme:
                themes.append(theme)
        return themes

    @staticmethod
    def find_themes(path):
        """
        Returns the full path names of all themes found in the given path.
        """
        themes = []

        try:
            files = os.listdir(path)
        except OSError:
            files = []

        for filename in files:
            if filename.endswith(Theme.extension()):
                themes.append(os.path.join(path, filename))
        return themes

    @staticmethod
    def load(filename, is_system=False):
        """ Load a theme and return a new theme object. """

        result = None

        _file = open_utf8(filename)
        try:
            domdoc = minidom.parse(_file).documentElement
            try:
                theme = Theme()

                node = domdoc.attributes.get("format")
                format = Version.from_string(node.value) \
                         if node else Theme.THEME_FORMAT_INITIAL

                theme.name = domdoc.attributes["name"].value

                # "color_scheme" is the base file name of the color scheme
                text = utils.xml_get_text(domdoc, "color_scheme")
                if not text is None:
                    theme.color_scheme_basename = text

                # get key label overrides
                nodes = domdoc.getElementsByTagName("key_label_overrides")
                if nodes:
                    overrides = nodes[0]
                    tuples = {}
                    for override in overrides.getElementsByTagName("key"):
                        key_id = override.attributes["id"].value
                        node = override.attributes.get("label")
                        label = node.value if node else ""
                        node = override.attributes.get("group")
                        group = node.value if node else ""
                        tuples[key_id] = (label, group)
                    theme.key_label_overrides = tuples

                # read all other members
                for name, _type, _default in Theme.attributes:
                    if not name in [
                            "color_scheme_basename", "key_label_overrides"
                    ]:
                        value = utils.xml_get_text(domdoc, name)
                        if not value is None:

                            if _type == "i":
                                value = int(value)
                            if _type == "d":
                                value = float(value)
                            if _type == "ad":
                                value = [float(s) for s in value.split(",")]

                            # upgrade to current file format
                            if format < Theme.THEME_FORMAT_1_1:
                                # direction was    0..360, ccw
                                #        is now -180..180, cw
                                if name == "key_gradient_direction":
                                    value = -(value % 360)
                                    if value <= -180:
                                        value += 360

                            setattr(theme, name, value)

                theme._filename = filename
                theme.is_system = is_system
                theme.system_exists = is_system
                result = theme
            finally:
                domdoc.unlink()

        except (Exceptions.ThemeFileError, xml.parsers.expat.ExpatError) as ex:
            _logger.error(
                _format(
                    "Error loading theme '{filename}'. "
                    "{exception}: {cause}",
                    filename=filename,
                    exception=type(ex).__name__,
                    cause=unicode_str(ex)))
            result = None
        finally:
            _file.close()

        return result

    def save_as(self, basename, name):
        """ Save this theme under a new name. """
        self._filename = self.build_user_filename(basename)
        self._name = name
        self.save()

    def save(self):
        """ Save this theme. """

        domdoc = minidom.Document()
        try:
            theme_element = domdoc.createElement("theme")
            theme_element.setAttribute("name", self._name)
            theme_element.setAttribute("format", str(self.THEME_FORMAT))
            domdoc.appendChild(theme_element)

            for name, _type, _default in self.attributes:
                if name == "color_scheme_basename":
                    element = domdoc.createElement("color_scheme")
                    text = domdoc.createTextNode(self.color_scheme_basename)
                    element.appendChild(text)
                    theme_element.appendChild(element)
                elif name == "key_label_overrides":
                    overrides_element = \
                            domdoc.createElement("key_label_overrides")
                    theme_element.appendChild(overrides_element)
                    tuples = self.key_label_overrides
                    for key_id, values in list(tuples.items()):
                        element = domdoc.createElement("key")
                        element.setAttribute("id", key_id)
                        element.setAttribute("label", values[0])
                        element.setAttribute("group", values[1])
                        overrides_element.appendChild(element)
                else:
                    value = getattr(self, name)
                    if _type == "s":
                        pass
                    elif _type == "i":
                        value = str(value)
                    elif _type == "d":
                        value = str(round(float(value), 2))
                    elif _type == "ad":
                        value = ", ".join(str(d) for d in value)
                    else:
                        assert (False)  # attribute of unknown type

                    element = domdoc.createElement(name)
                    text = domdoc.createTextNode(value)
                    element.appendChild(text)
                    theme_element.appendChild(element)

            pretty_xml = toprettyxml(domdoc)

            XDGDirs.assure_user_dir_exists(self.user_path())

            with open_utf8(self._filename, "w") as _file:
                if sys.version_info.major >= 3:
                    _file.write(pretty_xml)
                else:
                    _file.write(pretty_xml.encode("UTF-8"))

        except Exception as xxx_todo_changeme2:
            (ex) = xxx_todo_changeme2
            raise Exceptions.ThemeFileError(_("Error saving ") +
                                            self._filename,
                                            chained_exception=ex)
        finally:
            domdoc.unlink()
Exemple #11
0
    def load(filename, is_system=False):
        """ Load a theme and return a new theme object. """

        result = None

        _file = open_utf8(filename)
        try:
            domdoc = minidom.parse(_file).documentElement
            try:
                theme = Theme()

                node = domdoc.attributes.get("format")
                format = Version.from_string(node.value) \
                         if node else Theme.THEME_FORMAT_INITIAL

                theme.name = domdoc.attributes["name"].value

                # "color_scheme" is the base file name of the color scheme
                text = utils.xml_get_text(domdoc, "color_scheme")
                if not text is None:
                    theme.color_scheme_basename = text

                # get key label overrides
                nodes = domdoc.getElementsByTagName("key_label_overrides")
                if nodes:
                    overrides = nodes[0]
                    tuples = {}
                    for override in overrides.getElementsByTagName("key"):
                        key_id = override.attributes["id"].value
                        node = override.attributes.get("label")
                        label = node.value if node else ""
                        node = override.attributes.get("group")
                        group = node.value if node else ""
                        tuples[key_id] = (label, group)
                    theme.key_label_overrides = tuples

                # read all other members
                for name, _type, _default in Theme.attributes:
                    if not name in ["color_scheme_basename",
                                    "key_label_overrides"]:
                        value = utils.xml_get_text(domdoc, name)
                        if not value is None:

                            if _type == "i":
                                value = int(value)
                            if _type == "d":
                                value = float(value)
                            if _type == "ad":
                                value = [float(s) for s in value.split(",")]

                            # upgrade to current file format
                            if format < Theme.THEME_FORMAT_1_1:
                                # direction was    0..360, ccw
                                #        is now -180..180, cw
                                if name == "key_gradient_direction":
                                    value = -(value % 360)
                                    if value <= -180:
                                        value += 360

                            setattr(theme, name, value)

                theme._filename = filename
                theme.is_system = is_system
                theme.system_exists = is_system
                result = theme
            finally:
                domdoc.unlink()

        except (Exceptions.ThemeFileError,
                xml.parsers.expat.ExpatError) as ex:
            _logger.error(_format("Error loading theme '{filename}'. "
                                  "{exception}: {cause}",
                                  filename = filename,
                                  exception = type(ex).__name__,
                                  cause = unicode_str(ex)))
            result = None
        finally:
            _file.close()

        return result
    def load(filename, is_system=False):
        """ Load a theme and return a new theme object. """

        result = None

        _file = open_utf8(filename)
        try:
            domdoc = minidom.parse(_file).documentElement
            try:
                theme = Theme()

                node = domdoc.attributes.get("format")
                format = Version.from_string(node.value) \
                         if node else Theme.THEME_FORMAT_INITIAL

                theme.name = domdoc.attributes["name"].value

                # "color_scheme" is the base file name of the color scheme
                text = utils.xml_get_text(domdoc, "color_scheme")
                if not text is None:
                    theme.color_scheme_basename = text

                # get key label overrides
                nodes = domdoc.getElementsByTagName("key_label_overrides")
                if nodes:
                    overrides = nodes[0]
                    tuples = {}
                    for override in overrides.getElementsByTagName("key"):
                        key_id = override.attributes["id"].value
                        node = override.attributes.get("label")
                        label = node.value if node else ""
                        node = override.attributes.get("group")
                        group = node.value if node else ""
                        tuples[key_id] = (label, group)
                    theme.key_label_overrides = tuples

                # read all other members
                for name, _type, _default in Theme.attributes:
                    if not name in [
                            "color_scheme_basename", "key_label_overrides"
                    ]:
                        value = utils.xml_get_text(domdoc, name)
                        if not value is None:

                            if _type == "i":
                                value = int(value)
                            if _type == "d":
                                value = float(value)
                            if _type == "ad":
                                value = [float(s) for s in value.split(",")]

                            # upgrade to current file format
                            if format < Theme.THEME_FORMAT_1_1:
                                # direction was    0..360, ccw
                                #        is now -180..180, cw
                                if name == "key_gradient_direction":
                                    value = -(value % 360)
                                    if value <= -180:
                                        value += 360

                            setattr(theme, name, value)

                theme._filename = filename
                theme.is_system = is_system
                theme.system_exists = is_system
                result = theme
            finally:
                domdoc.unlink()

        except (Exceptions.ThemeFileError, xml.parsers.expat.ExpatError) as ex:
            _logger.error(
                _format(
                    "Error loading theme '{filename}'. "
                    "{exception}: {cause}",
                    filename=filename,
                    exception=type(ex).__name__,
                    cause=unicode_str(ex)))
            result = None
        finally:
            _file.close()

        return result
class ColorScheme(object):
    """
    ColorScheme defines the colors of onboards keyboard.
    Each key or groups of keys may have their own individual colors.
    Any color definition may be omitted. Undefined colors fall back
    to color scheme defaults first, then to hard coded default colors.
    """

    # onboard 0.95
    COLOR_SCHEME_FORMAT_LEGACY = Version(1, 0)

    # onboard 0.97, tree format, rule-based color matching
    COLOR_SCHEME_FORMAT_TREE = Version(2, 0)

    # onboard 0.99, added window colors
    COLOR_SCHEME_WINDOW_COLORS = Version(2, 1)

    COLOR_SCHEME_FORMAT = COLOR_SCHEME_WINDOW_COLORS

    def __init__(self):
        self._filename = ""
        self._is_system = False
        self._root = None  # tree root

    @property
    def basename(self):
        """ Returns the file base name of the color scheme. """
        return os.path.splitext(os.path.basename(self._filename))[0]

    @property
    def filename(self):
        """ Returns the filename of the color scheme. """
        return self._filename

    def is_key_in_scheme(self, key):
        for id in [key.theme_id, key.id]:
            if self._root.find_key_id(id):
                return True
        return False

    def get_key_rgba(self, key, element, state=None):
        """
        Get the color for the given key element and optionally key state.
        If <state> is None the key state is retrieved from <key>.
        """

        if state is None:
            state = key.get_state()
            state["insensitive"] = not key.sensitive
            del state["sensitive"]

        rgb = None
        opacity = None
        root_rgb = None
        root_opacity = None
        key_group = None

        # First try to find the theme_id then fall back to the generic id
        ids = [key.theme_id, key.id]

        # Let numbered keys fall back to their base id, e.g. instead
        # of prediction0, prediction1,... have only "prediction" in
        # the color scheme.
        if key.id == "correctionsbg":
            ids.append("wordlist")
        elif key.id == "predictionsbg":
            ids.append("wordlist")
        elif key.is_prediction_key():
            ids.append("prediction")
        elif key.is_correction_key():
            ids.append("correction")
        elif key.is_layer_button():
            ids.append(key.get_similar_theme_id("layer"))
            ids.append("layer")

        # look for a matching key_group and color in the color scheme
        for id in ids:
            key_group = self._root.find_key_id(id)
            if key_group:
                rgb, opacity = key_group.find_element_color(element, state)
                break

        # Get root colors as fallback for the case when key id
        # wasn't mentioned anywhere in the color scheme.
        root_key_group = self._root.get_default_key_group()
        if root_key_group:
            root_rgb, root_opacity = \
                    root_key_group.find_element_color(element, state)

        # Special case for layer buttons:
        # don't take fill color from the root group,
        # we want the layer fill color instead (via get_key_default_rgba()).
        if element == "fill" and key.is_layer_button() or \
           element == "label" and key.is_correction_key():
            # Don't pick layer fill opacity when there is
            # an rgb color defined in the color scheme.
            if not rgb is None and \
               opacity is None:
                opacity = root_opacity
                if opacity is None:
                    opacity = 1.0
        elif key_group is None:
            # All other colors fall back to the root group's colors
            rgb = root_rgb
            opacity = root_opacity

        if rgb is None:
            rgb = self.get_key_default_rgba(key, element, state)[:3]

        if opacity is None:
            opacity = self.get_key_default_rgba(key, element, state)[3]

        rgba = rgb + [opacity]
        return rgba

    def get_key_default_rgba(self, key, element, state):
        colors = {
            "fill": [0.9, 0.85, 0.7, 1.0],
            "prelight": [0.0, 0.0, 0.0, 1.0],
            "pressed": [0.6, 0.6, 0.6, 1.0],
            "active": [0.5, 0.5, 0.5, 1.0],
            "locked": [1.0, 0.0, 0.0, 1.0],
            "scanned": [0.45, 0.45, 0.7, 1.0],
            "stroke": [0.0, 0.0, 0.0, 1.0],
            "label": [0.0, 0.0, 0.0, 1.0],
            "secondary-label": [0.5, 0.5, 0.5, 1.0],
            "dwell-progress": [0.82, 0.19, 0.25, 1.0],
            "correction-label": [1.0, 0.5, 0.5, 1.0],
        }

        rgba = [0.0, 0.0, 0.0, 1.0]

        if element == "fill":
            if key.is_layer_button() and \
               not any(state.values()):
                # Special case for base fill color of layer buttons:
                # default color is layer fill color (as in onboard <=0.95).
                layer_index = key.get_layer_index()
                rgba = self.get_layer_fill_rgba(layer_index)

            elif state.get("pressed"):
                new_state = dict(list(state.items()))
                new_state["pressed"] = False
                rgba = self.get_key_rgba(key, element, new_state)

                # Make the default pressed color a slightly darker
                # or brighter variation of the unpressed color.
                h, l, s = colorsys.rgb_to_hls(*rgba[:3])

                # boost lightness changes for very dark and very bright colors
                # Ad-hoc formula, purly for aesthetics
                amount = -(log((l + .001) * (1 - (l - .001)))) * 0.05 + 0.08

                if l < .5:  # dark color?
                    rgba = brighten(+amount, *rgba)  # brigther
                else:
                    rgba = brighten(-amount, *rgba)  # darker

            elif state.get("scanned"):
                rgba = colors["scanned"]
                # Make scanned active modifier keys stick out by blending
                # scanned color with non-scanned color.
                if state.get("active"):  # includes locked
                    # inactive scanned color
                    new_state = dict(list(state.items()))
                    new_state["active"] = False
                    new_state["locked"] = False
                    scanned = self.get_key_rgba(key, element, new_state)

                    # unscanned fill color
                    new_state = dict(list(state.items()))
                    new_state["scanned"] = False
                    fill = self.get_key_rgba(key, element, new_state)

                    # blend inactive scanned color with unscanned fill color
                    for i in range(4):
                        rgba[i] = (scanned[i] + fill[i]) / 2.0

            elif state.get("prelight"):
                rgba = colors["prelight"]
            elif state.get("locked"):
                rgba = colors["locked"]
            elif state.get("active"):
                rgba = colors["active"]
            else:
                rgba = colors["fill"]

        elif element == "stroke":
            rgba == colors["stroke"]

        elif element == "label":

            if key.is_correction_key():
                rgba = colors["correction-label"]
            else:
                rgba = colors["label"]

            # dim label color for insensitive keys
            if state.get("insensitive"):
                rgba = self._get_insensitive_color(key, state, element)

        elif element == "secondary-label":

            rgba = colors["secondary-label"]

            # dim label color for insensitive keys
            if state.get("insensitive"):
                rgba = self._get_insensitive_color(key, state, element)

        elif element == "dwell-progress":
            rgba = colors["dwell-progress"]

        else:
            assert (False)  # unknown element

        return rgba

    def _get_insensitive_color(self, key, state, element):
        new_state = state.copy()
        new_state["insensitive"] = False
        fill = self.get_key_rgba(key, "fill", new_state)
        rgba = self.get_key_rgba(key, element, new_state)

        h, lf, s = colorsys.rgb_to_hls(*fill[:3])
        h, ll, s = colorsys.rgb_to_hls(*rgba[:3])

        # Leave only one third of the lightness difference
        # between label and fill color.
        amount = (ll - lf) * 2.0 / 3.0
        return brighten(-amount, *rgba)

    def get_window_rgba(self, window_type, element):
        """
        Returns window colors.
        window_type may be "keyboard" or "key-popup".
        element may be "border"
        """
        rgb = None
        opacity = None
        windows = self._root.get_windows()

        window = None
        for item in windows:
            if item.type == window_type:
                window = item
                break

        if window:
            for item in window.items:
                if item.is_color() and \
                   item.element == element:
                    rgb = item.rgb
                    opacity = item.opacity
                    break

        if rgb is None:
            rgb = [1.0, 1.0, 1.0]
        if opacity is None:
            opacity = 1.0
        rgba = rgb + [opacity]

        return rgba

    def get_layer_fill_rgba(self, layer_index):
        """
        Returns the background fill color of the layer with the given index.
        """

        rgb = None
        opacity = None
        layers = self._root.get_layers()

        # If there is no layer definition for this index,
        # repeat the last defined layer color.
        layer_index = min(layer_index, len(layers) - 1)

        if layer_index >= 0 and layer_index < len(layers):
            for item in layers[layer_index].items:
                if item.is_color() and \
                   item.element == "background":
                    rgb = item.rgb
                    opacity = item.opacity
                    break

        if rgb is None:
            rgb = [0.5, 0.5, 0.5]
        if opacity is None:
            opacity = 1.0
        rgba = rgb + [opacity]

        return rgba

    def get_icon_rgba(self, element):
        """
        Returns the color for the given element of the icon.
        """
        rgb = None
        opacity = None
        icons = self._root.get_icons()
        for icon in icons:
            for item in icon.items:
                if item.is_color() and \
                   item.element == element:
                    rgb = item.rgb
                    opacity = item.opacity
                    break

        # default icon background is layer0 background
        if element == "background":
            # hard-coded default is the most common color
            rgba_default = [0.88, 0.88, 0.88, 1.0]
        else:
            assert (False)

        if rgb is None:
            rgb = rgba_default[:3]
        if opacity is None:
            opacity = rgba_default[3]

        if rgb is None:
            rgb = [0.5, 0.5, 0.5]
        if opacity is None:
            opacity = 1.0

        rgba = rgb + [opacity]

        return rgba

    @staticmethod
    def user_path():
        """ Returns the path of the user directory for color schemes. """
        return os.path.join(config.user_dir, "themes")

    @staticmethod
    def system_path():
        """ Returns the path of the system directory for color schemes. """
        return os.path.join(config.install_dir, "themes")

    @staticmethod
    def extension():
        """ Returns the file extension of color scheme files """
        return "colors"

    @staticmethod
    def get_merged_color_schemes():
        """
        Merge system and user color schemes.
        User color schemes take precedence and hide system color schemes.
        """
        system_color_schemes = ColorScheme.load_color_schemes(True)
        user_color_schemes = ColorScheme.load_color_schemes(False)
        color_schemes = dict((t.basename, t) for t in system_color_schemes)
        for scheme in user_color_schemes:
            color_schemes[scheme.basename] = scheme
        return color_schemes

    @staticmethod
    def load_color_schemes(is_system=False):
        """
        Load all color schemes from either the user or the system directory.
        """
        color_schemes = []

        if is_system:
            path = ColorScheme.system_path()
        else:
            path = ColorScheme.user_path()

        filenames = ColorScheme.find_color_schemes(path)
        for filename in filenames:
            color_scheme = ColorScheme.load(filename, is_system)
            if color_scheme:
                color_schemes.append(color_scheme)
        return color_schemes

    @staticmethod
    def find_color_schemes(path):
        """
        Returns the full path names of all color schemes found in the given path.
        """
        color_schemes = []

        try:
            files = os.listdir(path)
        except OSError:
            files = []

        for filename in files:
            if filename.endswith(ColorScheme.extension()):
                color_schemes.append(os.path.join(path, filename))
        return color_schemes

    @staticmethod
    def load(filename, is_system=False):
        """ Load a color scheme and return it as a new instance. """

        color_scheme = None

        f = open_utf8(filename)
        try:
            dom = minidom.parse(f).documentElement
            name = dom.attributes["name"].value

            # check layout format
            format = ColorScheme.COLOR_SCHEME_FORMAT_LEGACY
            if dom.hasAttribute("format"):
                format = Version.from_string(dom.attributes["format"].value)

            if format >= ColorScheme.COLOR_SCHEME_FORMAT_TREE:  # tree format?
                items = ColorScheme._parse_dom_node(dom, None, {})
            else:
                _logger.warning(_format( \
                    "Loading legacy color scheme format '{old_format}', "
                    "please consider upgrading to current format "
                    "'{new_format}': '{filename}'",
                    old_format = format,
                    new_format = ColorScheme.COLOR_SCHEME_FORMAT,
                    filename = filename))

                items = ColorScheme._parse_legacy_color_scheme(dom)

            if not items is None:
                root = Root()
                root.set_items(items)

                color_scheme = ColorScheme()
                color_scheme.name = name
                color_scheme._filename = filename
                color_scheme.is_system = is_system
                color_scheme._root = root
                #print(root.dumps())
        except xml.parsers.expat.ExpatError as ex:
            _logger.error(
                _format(
                    "Error loading color scheme '{filename}'. "
                    "{exception}: {cause}",
                    filename=filename,
                    exception=type(ex).__name__,
                    cause=unicode_str(ex)))
        finally:
            f.close()

        return color_scheme

    @staticmethod
    def _parse_dom_node(dom_node, parent_item, used_keys):
        """ Recursive function to parse all dom nodes of the layout tree """
        items = []
        for child in dom_node.childNodes:
            if child.nodeType == minidom.Node.ELEMENT_NODE:
                if child.tagName == "window":
                    item = ColorScheme._parse_window(child)
                elif child.tagName == "layer":
                    item = ColorScheme._parse_layer(child)
                elif child.tagName == "icon":
                    item = ColorScheme._parse_icon(child)
                elif child.tagName == "key_group":
                    item = ColorScheme._parse_key_group(child, used_keys)
                elif child.tagName == "color":
                    item = ColorScheme._parse_color(child)
                else:
                    item = None

                if item:
                    item.parent = parent_item
                    item.items = ColorScheme._parse_dom_node(
                        child, item, used_keys)
                    items.append(item)

        return items

    @staticmethod
    def _parse_dom_node_item(node, item):
        """ Parses common properties of all items """
        if node.hasAttribute("id"):
            item.id = node.attributes["id"].value

    @staticmethod
    def _parse_window(node):
        item = Window()
        if node.hasAttribute("type"):
            item.type = node.attributes["type"].value
        ColorScheme._parse_dom_node_item(node, item)
        return item

    @staticmethod
    def _parse_layer(node):
        item = Layer()
        ColorScheme._parse_dom_node_item(node, item)
        return item

    @staticmethod
    def _parse_icon(node):
        item = Icon()
        ColorScheme._parse_dom_node_item(node, item)
        return item

    _key_ids_pattern = re.compile('[\w-]+(?:[.][\w-]+)?', re.UNICODE)

    @staticmethod
    def _parse_key_group(node, used_keys):
        item = KeyGroup()
        ColorScheme._parse_dom_node_item(node, item)

        # read key ids
        text = "".join([n.data for n in node.childNodes \
                        if n.nodeType == n.TEXT_NODE])
        ids = [id for id in ColorScheme._key_ids_pattern.findall(text) if id]

        # check for duplicate key definitions
        for key_id in ids:
            if key_id in used_keys:
                raise ValueError(
                    _format(
                        "Duplicate key_id '{}' found "
                        "in color scheme file. "
                        "Key_ids must occur only once.", key_id))

        used_keys.update(list(zip(ids, ids)))

        item.key_ids = ids

        return item

    @staticmethod
    def _parse_color(node):
        item = KeyColor()
        ColorScheme._parse_dom_node_item(node, item)

        if node.hasAttribute("element"):
            item.element = node.attributes["element"].value
        if node.hasAttribute("rgb"):
            value = node.attributes["rgb"].value
            item.rgb = [
                hexstring_to_float(value[1:3]) / 255,
                hexstring_to_float(value[3:5]) / 255,
                hexstring_to_float(value[5:7]) / 255
            ]
        if node.hasAttribute("opacity"):
            item.opacity = float(node.attributes["opacity"].value)

        state = {}
        ColorScheme._parse_state_attibute(node, "prelight", state)
        ColorScheme._parse_state_attibute(node, "pressed", state)
        ColorScheme._parse_state_attibute(node, "active", state)
        ColorScheme._parse_state_attibute(node, "locked", state)
        ColorScheme._parse_state_attibute(node, "insensitive", state)
        ColorScheme._parse_state_attibute(node, "scanned", state)
        item.state = state

        return item

    @staticmethod
    def _parse_state_attibute(node, name, state):
        if node.hasAttribute(name):
            value = node.attributes[name].value == "true"
            state[name] = value

            if name == "locked" and value:
                state["active"] = True  # locked implies active

    ###########################################################################
    @staticmethod
    def _parse_legacy_color_scheme(dom_node):
        """ Load a color scheme and return it as a new object. """

        color_defaults = {
            "fill": [0.0, 0.0, 0.0, 1.0],
            "hovered": [0.0, 0.0, 0.0, 1.0],
            "pressed": [0.6, 0.6, 0.6, 1.0],
            "pressed-latched": [0.6, 0.6, 0.6, 1.0],
            "pressed-locked": [0.6, 0.6, 0.6, 1.0],
            "latched": [0.5, 0.5, 0.5, 1.0],
            "locked": [1.0, 0.0, 0.0, 1.0],
            "scanned": [0.45, 0.45, 0.7, 1.0],
            "stroke": [0.0, 0.0, 0.0, 1.0],
            "stroke-hovered": [0.0, 0.0, 0.0, 1.0],
            "stroke-pressed": [0.0, 0.0, 0.0, 1.0],
            "stroke-pressed-latched": [0.0, 0.0, 0.0, 1.0],
            "stroke-pressed-locked": [0.0, 0.0, 0.0, 1.0],
            "stroke-latched": [0.0, 0.0, 0.0, 1.0],
            "stroke-locked": [0.0, 0.0, 0.0, 1.0],
            "stroke-scanned": [0.0, 0.0, 0.0, 1.0],
            "label": [0.0, 0.0, 0.0, 1.0],
            "label-hovered": [0.0, 0.0, 0.0, 1.0],
            "label-pressed": [0.0, 0.0, 0.0, 1.0],
            "label-pressed-latched": [0.0, 0.0, 0.0, 1.0],
            "label-pressed-locked": [0.0, 0.0, 0.0, 1.0],
            "label-latched": [0.0, 0.0, 0.0, 1.0],
            "label-locked": [0.0, 0.0, 0.0, 1.0],
            "label-scanned": [0.0, 0.0, 0.0, 1.0],
            "dwell-progress": [0.82, 0.19, 0.25, 1.0],
        }

        items = []

        # layer colors
        layers = dom_node.getElementsByTagName("layer")
        if not layers:
            # Still accept "pane" for backwards compatibility
            layers = dom_node.getElementsByTagName("pane")
        for i, layer in enumerate(layers):
            attrib = "fill"
            rgb = None
            opacity = None

            color = KeyColor()
            if layer.hasAttribute(attrib):
                value = layer.attributes[attrib].value
                color.rgb = [
                    hexstring_to_float(value[1:3]) / 255,
                    hexstring_to_float(value[3:5]) / 255,
                    hexstring_to_float(value[5:7]) / 255
                ]

            oattrib = attrib + "-opacity"
            if layer.hasAttribute(oattrib):
                color.opacity = float(layer.attributes[oattrib].value)

            color.element = "background"
            layer = Layer()
            layer.set_items([color])
            items.append(layer)

        # key groups
        used_keys = {}
        root_key_group = None
        key_groups = []
        for group in dom_node.getElementsByTagName("key_group"):

            # Check for default flag.
            # Default colors are applied to all keys
            # not found in the color scheme.
            default_group = False
            if group.hasAttribute("default"):
                default_group = bool(group.attributes["default"].value)

            # read key ids
            text = "".join([n.data for n in group.childNodes])
            key_ids = [x for x in re.findall('\w+(?:[.][\w-]+)?', text) if x]

            # check for duplicate key definitions
            for key_id in key_ids:
                if key_id in used_keys:
                    raise ValueError(
                        _format(
                            "Duplicate key_id '{}' found "
                            "in color scheme file. "
                            "Key_ids must occur only once.", key_id))
            used_keys.update(list(zip(key_ids, key_ids)))

            colors = []

            for attrib in list(color_defaults.keys()):

                rgb = None
                opacity = None

                # read color attribute
                if group.hasAttribute(attrib):
                    value = group.attributes[attrib].value
                    rgb = [
                        hexstring_to_float(value[1:3]) / 255,
                        hexstring_to_float(value[3:5]) / 255,
                        hexstring_to_float(value[5:7]) / 255
                    ]

                # read opacity attribute
                oattrib = attrib + "-opacity"
                if group.hasAttribute(oattrib):
                    opacity = float(group.attributes[oattrib].value)

                if not rgb is None or not opacity is None:
                    elements = ["fill", "stroke", "label", "dwell-progress"]
                    for element in elements:
                        if attrib.startswith(element):
                            break
                    else:
                        element = "fill"

                    if attrib.startswith(element):
                        state_attrib = attrib[len(element):]
                        if state_attrib.startswith("-"):
                            state_attrib = state_attrib[1:]
                    else:
                        state_attrib = attrib

                    color = KeyColor()
                    color.rgb = rgb
                    color.opacity = opacity
                    color.element = element
                    if state_attrib:
                        color.state = {state_attrib: True}
                    else:
                        color.state = {}

                    colors.append(color)

            key_group = KeyGroup()
            key_group.set_items(colors)
            key_group.key_ids = key_ids
            if default_group:
                root_key_group = key_group
            else:
                key_groups.append(key_group)

        if root_key_group:
            root_key_group.append_items(key_groups)
            items.append(root_key_group)

        return items
Exemple #14
0
def gtk_has_resize_grip_support():
    """ Gtk from 3.14 removes resize grips. """
    gtk_version = Version(Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION)
    return gtk_version < Version(3, 14)
Exemple #15
0
    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()
Exemple #16
0
    def copy_layout(src_filename, dst_filename):
        src_dir = os.path.dirname(src_filename)
        dst_dir, name_ext = os.path.split(dst_filename)
        dst_basename, ext = os.path.splitext(name_ext)
        _logger.info(_format("copying layout '{}' to '{}'",
                             src_filename, dst_filename))

        domdoc = None
        svg_filenames = {}
        fallback_layers = {}

        try:
            with open_utf8(src_filename) as f:
                domdoc = minidom.parse(f)
                keyboard_node = domdoc.documentElement

                # check layout format
                format = LayoutLoaderSVG.LAYOUT_FORMAT_LEGACY
                if keyboard_node.hasAttribute("format"):
                   format = Version.from_string(keyboard_node.attributes["format"].value)
                keyboard_node.attributes["id"] = dst_basename

                if format < LayoutLoaderSVG.LAYOUT_FORMAT_LAYOUT_TREE:
                    raise Exceptions.LayoutFileError( \
                        _format("copy_layouts failed, unsupported layout format '{}'.",
                                format))
                else:
                    # replace the basename of all svg filenames
                    for node in LayoutLoaderSVG._iter_dom_nodes(keyboard_node):
                        if LayoutLoaderSVG.is_layout_node(node):
                            if node.hasAttribute("filename"):
                                filename = node.attributes["filename"].value

                                # Create a replacement layer name for the unlikely
                                # case  that the svg-filename doesn't contain a
                                # layer section (as in path/basename-layer.ext).
                                fallback_layer_name = fallback_layers.get(filename,
                                             "Layer" + str(len(fallback_layers)))
                                fallback_layers[filename] = fallback_layer_name

                                # replace the basename of this filename
                                new_filename = LayoutLoaderSVG._replace_basename( \
                                     filename, dst_basename, fallback_layer_name)

                                node.attributes["filename"].value = new_filename
                                svg_filenames[filename] = new_filename

            if domdoc:
                XDGDirs.assure_user_dir_exists(config.get_user_layout_dir())

                # write the new layout file
                with open_utf8(dst_filename, "w") as f:
                    xml = toprettyxml(domdoc)
                    if sys.version_info.major == 2:  # python 2?
                        xml = xml.encode("UTF-8")
                    f.write(xml)

                    # copy the svg files
                    for src, dst in list(svg_filenames.items()):

                        dir, name = os.path.split(src)
                        if not dir:
                            src = os.path.join(src_dir, name)
                        dir, name = os.path.split(dst)
                        if not dir:
                            dst = os.path.join(dst_dir, name)

                        _logger.info(_format("copying svg file '{}' to '{}'", \
                                     src, dst))
                        shutil.copyfile(src, dst)
        except OSError as ex:
            _logger.error("copy_layout failed: " + \
                          unicode_str(ex))
        except Exceptions.LayoutFileError as ex:
            _logger.error(unicode_str(ex))
Exemple #17
0
class LayoutLoaderSVG:
    """
    Keyboard layout loaded from an SVG file.
    """
    # onboard <= 0.95
    LAYOUT_FORMAT_LEGACY = Version(1, 0)

    # onboard 0.96, initial layout-tree
    LAYOUT_FORMAT_LAYOUT_TREE = Version(2, 0)

    # onboard 0.97, scanner overhaul, no more scan columns,
    # new attributes scannable, scan_priority
    LAYOUT_FORMAT_SCANNER = Version(2, 1)

    # onboard 0.99, prerelease on Nexus 7,
    # new attributes key.action, key.sticky_behavior.
    # allow (i.e. have by default) keycodes for modifiers.
    LAYOUT_FORMAT_2_2 = Version(2, 2)

    # onboard 0.99, key_templates in key_def.xml and include tags.
    LAYOUT_FORMAT_3_0 = Version(3, 0)

    # sub-layouts for popups, various new key attributes,
    # label_margin, theme_id, popup_id
    LAYOUT_FORMAT_3_1 = Version(3, 1)

    # new key attributes show_active
    LAYOUT_FORMAT_3_2 = Version(3, 2)

    # current format
    LAYOUT_FORMAT = LAYOUT_FORMAT_3_2

    # precalc mask permutations
    _label_modifier_masks = permute_mask(LABEL_MODIFIERS)

    def __init__(self):
        self._vk = None
        self._svg_cache = {}
        self._format = None  # format of the currently loading layout
        self._layout_filename = ""
        self._color_scheme = None
        self._root_layout_dir = ""  # path to svg files
        self._layout_regex = re.compile("([^\(]+) (?: \( ([^\)]*) \) )?",
                                        re.VERBOSE)

    def load(self, vk, layout_filename, color_scheme):
        """ Load layout root file. """
        self._system_layout, self._system_variant = \
                                      self._get_system_keyboard_layout(vk)
        _logger.info("current system keyboard layout(variant): '{}'" \
                     .format(self._get_system_layout_string()))

        layout = self._load(vk, layout_filename, color_scheme,
                            os.path.dirname(layout_filename))
        if layout:
            # purge attributes only used during loading
            for item in layout.iter_items():
                if not item.templates is None:
                    item.templates = None
                if not item.keysym_rules is None:
                    item.keysym_rules = None

            # enable caching
            layout = LayoutRoot(layout)

        return layout

    def _load(self,
              vk,
              layout_filename,
              color_scheme,
              root_layout_dir,
              parent_item=None):
        """ Load or include layout file at any depth level. """
        self._vk = vk
        self._layout_filename = layout_filename
        self._color_scheme = color_scheme
        self._root_layout_dir = root_layout_dir
        return self._load_layout(layout_filename, parent_item)

    def _load_layout(self, layout_filename, parent_item=None):
        self._svg_cache = {}
        layout = None

        f = open_utf8(layout_filename)

        # make sure unlink is called
        with minidom.parse(f).documentElement as dom:

            # check layout format, no format version means legacy layout
            format = self.LAYOUT_FORMAT_LEGACY
            if dom.hasAttribute("format"):
                format = Version.from_string(dom.attributes["format"].value)
            self._format = format

            root = LayoutPanel()  # root, representing the 'keyboard' tag
            root.set_id("__root__")  # id for debug prints

            # Init included root with the parent item's svg filename.
            # -> Allows to skip specifying svg filenames in includes.
            if parent_item:
                root.filename = parent_item.filename

            if format >= self.LAYOUT_FORMAT_LAYOUT_TREE:
                self._parse_dom_node(dom, root)
                layout = root
            else:
                _logger.warning(
                    _format(
                        "Loading legacy layout, format '{}'. "
                        "Please consider upgrading to current format '{}'",
                        format, self.LAYOUT_FORMAT))
                items = self._parse_legacy_layout(dom)
                if items:
                    root.set_items(items)
                    layout = root

        f.close()

        self._svg_cache = {}  # Free the memory
        return layout

    def _parse_dom_node(self, dom_node, parent_item):
        """ Recursively traverse the dom nodes of the layout tree. """
        loaded_ids = set()

        for child in dom_node.childNodes:
            if child.nodeType == minidom.Node.ELEMENT_NODE:

                # Skip over items with non-matching keyboard layout string.
                # Items with the same id are processed from top to bottom,
                # the first match wins. If no item matches we fall back to
                # the item without layout string.
                # This is used to select between alternative key definitions
                # depending on the current system layout.
                can_load = False
                if not child.hasAttribute("id"):
                    can_load = True
                else:
                    id = child.attributes["id"].value
                    if not id in loaded_ids:
                        if child.hasAttribute("layout"):
                            layout = child.attributes["layout"].value
                            can_load = self._has_matching_layout(layout)

                            # don't look at items with this id again
                            if can_load:
                                loaded_ids.add(id)
                        else:
                            can_load = True

                if can_load:
                    tag = child.tagName

                    # rule and control tags
                    if tag == "include":
                        self._parse_include(child, parent_item)
                    elif tag == "key_template":
                        self._parse_key_template(child, parent_item)
                    elif tag == "keysym_rule":
                        self._parse_keysym_rule(child, parent_item)
                    elif tag == "layout":
                        item = self._parse_sublayout(child, parent_item)
                        parent_item.append_sublayout(item)
                        self._parse_dom_node(child, item)
                    else:
                        # actual items that make up the layout tree
                        if tag == "box":
                            item = self._parse_box(child)
                        elif tag == "panel":
                            item = self._parse_panel(child)
                        elif tag == "key":
                            item = self._parse_key(child, parent_item)
                        else:
                            item = None

                        if item:
                            parent_item.append_item(item)
                            self._parse_dom_node(child, item)

    def _parse_include(self, node, parent):
        if node.hasAttribute("file"):
            filename = node.attributes["file"].value
            filepath = config.find_layout_filename(filename, "layout include")
            _logger.info("Including layout '{}'".format(filename))
            incl_root = LayoutLoaderSVG()._load(self._vk, filepath,
                                                self._color_scheme,
                                                self._root_layout_dir, parent)
            if incl_root:
                parent.append_items(incl_root.items)
                parent.update_keysym_rules(incl_root.keysym_rules)
                parent.update_templates(incl_root.templates)
                incl_root.items = None  # help garbage collector
                incl_root.keysym_rules = None
                incl_root.templates = None

    def _parse_key_template(self, node, parent):
        """
        Templates are partially define layout items. Later non-template
        items inherit attributes of templates with matching id.
        """
        attributes = dict(list(node.attributes.items()))
        id = attributes.get("id")
        if not id:
            raise Exceptions.LayoutFileError(
                "'id' attribute required for template '{} {}' "
                "in layout '{}'" \
                .format(tag,
                        str(list(attributes.values())),
                        self._layout_filename))

        parent.update_templates({(id, RectKey): attributes})

    def _parse_keysym_rule(self, node, parent):
        """
        Keysym rules link attributes like "label", "image"
        to certain keysyms.
        """
        attributes = dict(list(node.attributes.items()))
        keysym = attributes.get("keysym")
        if keysym:
            del attributes["keysym"]
            if keysym.startswith("0x"):
                keysym = int(keysym, 16)
            else:
                # translate symbolic keysym name
                keysym = 0

            if keysym:
                parent.update_keysym_rules({keysym: attributes})

    def _init_item(self, attributes, item_class):
        """ Parses attributes common to all LayoutItems """

        # allow to override the item's default class
        if "class" in attributes:
            class_name = attributes["class"]
            try:
                item_class = globals()[class_name]
            except KeyError:
                pass

        # create the item
        item = item_class()

        value = attributes.get("id")
        if not value is None:
            item.id = value

        value = attributes.get("group")
        if not value is None:
            item.group = value

        value = attributes.get("layer")
        if not value is None:
            item.layer_id = value

        value = attributes.get("filename")
        if not value is None:
            item.filename = value

        value = attributes.get("visible")
        if not value is None:
            item.visible = value == "true"

        value = attributes.get("sensitive")
        if not value is None:
            item.sensitive = value == "true"

        value = attributes.get("border")
        if not value is None:
            item.border = float(value)

        value = attributes.get("expand")
        if not value is None:
            item.expand = value == "true"

        value = attributes.get("unlatch_layer")
        if not value is None:
            item.unlatch_layer = value == "true"

        value = attributes.get("scannable")
        if value and value.lower() == 'false':
            item.scannable = False

        value = attributes.get("scan_priority")
        if not value is None:
            item.scan_priority = int(value)

        return item

    def _parse_sublayout(self, node, parent):
        attributes = dict(node.attributes.items())
        item = self._init_item(attributes, LayoutPanel)
        item.sublayout_parent = parent  # make templates accessible in the subl.
        return item

    def _parse_box(self, node):
        attributes = dict(node.attributes.items())
        item = self._init_item(attributes, LayoutBox)
        if node.hasAttribute("orientation"):
            item.horizontal = \
                node.attributes["orientation"].value.lower() == "horizontal"
        if node.hasAttribute("spacing"):
            item.spacing = float(node.attributes["spacing"].value)
        if node.hasAttribute("compact"):
            item.compact = node.attributes["compact"].value == "true"
        return item

    def _parse_panel(self, node):
        attributes = dict(node.attributes.items())
        item = self._init_item(attributes, LayoutPanel)
        if node.hasAttribute("compact"):
            item.compact = node.attributes["compact"].value == "true"
        return item

    def _parse_key(self, node, parent):
        result = None

        id = node.attributes["id"].value
        if id == "inputline":
            item_class = InputlineKey
        else:
            item_class = RectKey

        # find template attributes
        attributes = {}
        if node.hasAttribute("id"):
            theme_id, id = RectKey.parse_id(node.attributes["id"].value)
            attributes.update(self.find_template(parent, RectKey, [id]))

        # let current node override any preceding templates
        attributes.update(dict(node.attributes.items()))

        # handle common layout-item attributes
        key = self._init_item(attributes, item_class)
        key.parent = parent  # assign early to have get_filename() work

        # handle key-specific attributes
        self._init_key(key, attributes)

        # get key geometry from the closest svg file
        filename = key.get_filename()
        if not filename:
            if not attributes.get("group") == "wsbutton":
                _logger.warning(
                    _format("Ignoring key '{}'."
                            " No svg filename defined.", key.id))
        else:
            svg_nodes = self._get_svg_keys(filename)
            if svg_nodes:
                # try svg_id first, if there is one
                if key.svg_id != key.id:
                    svg_node = svg_nodes.get(key.svg_id)
                else:
                    # then the regular id
                    svg_node = svg_nodes.get(key.id)

                if svg_node:
                    r, geometry = svg_node.extract_key_params()
                    key.set_initial_border_rect(r.copy())
                    key.set_border_rect(r.copy())
                    key.geometry = geometry
                    result = key
                else:
                    _logger.info("Ignoring key '{}'."
                                 " No svg object found for '{}'." \
                                 .format(key.id, key.svg_id))

        return result  # ignore keys not found in an svg file

    def _init_key(self, key, attributes):
        # Re-parse the id to distinguish between the short key_id
        # and the optional longer theme_id.
        full_id = attributes["id"]
        theme_id = attributes.get("theme_id")
        svg_id = attributes.get("svg_id")
        key.set_id(full_id, theme_id, svg_id)

        if "_" in key.get_id():
            _logger.warning("underscore in key id '{}', please use dashes" \
                            .format(key.get_id()))

        value = attributes.get("modifier")
        if value:
            try:
                key.modifier = modifiers[value]
            except KeyError as ex:
                (strerror) = ex
                raise Exceptions.LayoutFileError("Unrecognized modifier %s in" \
                    "definition of %s" (strerror, full_id))

        value = attributes.get("action")
        if value:
            try:
                key.action = KeyCommon.actions[value]
            except KeyError as ex:
                (strerror) = ex
                raise Exceptions.LayoutFileError("Unrecognized key action {} in" \
                    "definition of {}".format(strerror, full_id))

        if "char" in attributes:
            key.code = attributes["char"]
            key.type = KeyCommon.CHAR_TYPE
        elif "keysym" in attributes:
            value = attributes["keysym"]
            key.type = KeyCommon.KEYSYM_TYPE
            if value[1] == "x":  #Deals for when keysym is hex
                key.code = int(value, 16)
            else:
                key.code = int(value, 10)
        elif "keypress_name" in attributes:
            key.code = attributes["keypress_name"]
            key.type = KeyCommon.KEYPRESS_NAME_TYPE
        elif "macro" in attributes:
            key.code = attributes["macro"]
            key.type = KeyCommon.MACRO_TYPE
        elif "script" in attributes:
            key.code = attributes["script"]
            key.type = KeyCommon.SCRIPT_TYPE
        elif "keycode" in attributes:
            key.code = int(attributes["keycode"])
            key.type = KeyCommon.KEYCODE_TYPE
        elif "button" in attributes:
            key.code = key.id[:]
            key.type = KeyCommon.BUTTON_TYPE
        elif key.modifier:
            key.code = None
            key.type = KeyCommon.LEGACY_MODIFIER_TYPE
        else:
            # key without action: just draw it, do nothing on click
            key.action = None
            key.action_type = None

        # get the size group of the key
        if "group" in attributes:
            group_name = attributes["group"]
        else:
            group_name = "_default"

        # get the optional image filename
        if "image" in attributes:
            if not key.image_filenames: key.image_filenames = {}
            key.image_filenames[ImageSlot.NORMAL] = attributes["image"].split(
                ";")[0]
        if "image_active" in attributes:
            if not key.image_filenames: key.image_filenames = {}
            key.image_filenames[ImageSlot.ACTIVE] = attributes["image_active"]

        # get labels
        labels = self._parse_key_labels(attributes, key)

        # Replace label and size group with overrides from
        # theme and/or system defaults.
        label_overrides = config.theme_settings.key_label_overrides
        override = label_overrides.get(key.id)
        if override:
            olabel, ogroup = override
            if olabel:
                labels = {0: olabel[:]}
                if ogroup:
                    group_name = ogroup[:]

        key.labels = labels
        key.group = group_name

        # optionally  override the theme's default key_style
        if "key_style" in attributes:
            key.style = attributes["key_style"]

        # select what gets drawn, different from "visible" flag as this
        # doesn't affect the layout.
        if "show" in attributes:
            if attributes["show"].lower() == 'false':
                key.show_face = False
                key.show_border = False
        if "show_face" in attributes:
            if attributes["show_face"].lower() == 'false':
                key.show_face = False
        if "show_border" in attributes:
            if attributes["show_border"].lower() == 'false':
                key.show_border = False

        # show_active: allow to display key in latched or locked state
        # legacy show_active behavior was hard-coded for layer0
        if self._format < LayoutLoaderSVG.LAYOUT_FORMAT_3_2:
            if key.id == "layer0":
                key.show_active = False
        if "show_active" in attributes:
            if attributes["show_active"].lower() == 'false':
                key.show_active = False

        if "label_x_align" in attributes:
            key.label_x_align = float(attributes["label_x_align"])
        if "label_y_align" in attributes:
            key.label_y_align = float(attributes["label_y_align"])

        if "label_margin" in attributes:
            values = attributes["label_margin"].replace(" ", "").split(",")
            margin = [float(x) if x else key.label_margin[i] \
                      for i, x in enumerate(values[:2])]
            margin += margin[:1] * (2 - len(margin))
            if margin:
                key.label_margin = margin

        if "sticky" in attributes:
            sticky = attributes["sticky"].lower()
            if sticky == "true":
                key.sticky = True
            elif sticky == "false":
                key.sticky = False
            else:
                raise Exceptions.LayoutFileError(
                    "Invalid value '{}' for 'sticky' attribute of key '{}'" \
                    .format(sticky, key.id))
        else:
            key.sticky = False

        # legacy sticky key behavior was hard-coded for CAPS
        if self._format < LayoutLoaderSVG.LAYOUT_FORMAT_2_2:
            if key.id == "CAPS":
                key.sticky_behavior = StickyBehavior.LOCK_ONLY

        value = attributes.get("sticky_behavior")
        if value:
            try:
                key.sticky_behavior = StickyBehavior.from_string(value)
            except KeyError as ex:
                (strerror) = ex
                raise Exceptions.LayoutFileError("Unrecognized sticky behavior {} in" \
                    "definition of {}".format(strerror, full_id))

        if "tooltip" in attributes:
            key.tooltip = attributes["tooltip"]

        if "popup_id" in attributes:
            key.popup_id = attributes["popup_id"]

        if "chamfer_size" in attributes:
            key.chamfer_size = float(attributes["chamfer_size"])

        key.color_scheme = self._color_scheme

    def _parse_key_labels(self, attributes, key):
        labels = {}  # {modifier_mask : label, ...}

        # Get labels from keyboard mapping first.
        if key.type == KeyCommon.KEYCODE_TYPE and \
           not key.id in ["BKSP"]:
            if self._vk:  # xkb keyboard found?
                vkmodmasks = self._label_modifier_masks
                if sys.version_info.major == 2:
                    vkmodmasks = [long(m) for m in vkmodmasks]
                vklabels = self._vk.labels_from_keycode(key.code, vkmodmasks)
                if sys.version_info.major == 2:
                    vklabels = [x.decode("UTF-8") for x in vklabels]
                labels = {m: l for m, l in zip(vkmodmasks, vklabels)}
            else:
                if key.id.upper() == "SPCE":
                    labels[0] = "No X keyboard found, retrying..."
                else:
                    labels[0] = "?"

        # If key is a macro (snippet) generate label from its number.
        elif key.type == KeyCommon.MACRO_TYPE:
            label, text = config.snippets.get(int(key.code), \
                                                       (None, None))
            tooltip = _format("Snippet {}", key.code)
            if not label:
                labels[0] = "     --     "
                # i18n: full string is "Snippet n, unassigned"
                tooltip += _(", unassigned")
            else:
                labels[0] = label.replace("\\n", "\n")
            key.tooltip = tooltip

        # get labels from the key/template definition in the layout
        layout_labels = self._parse_layout_labels(attributes)
        if layout_labels:
            labels = layout_labels

        # override with per-keysym labels
        keysym_rules = self._get_keysym_rules(key)
        if key.type == KeyCommon.KEYCODE_TYPE:
            if self._vk:  # xkb keyboard found?
                vkmodmasks = self._label_modifier_masks
                try:
                    if sys.version_info.major == 2:
                        vkmodmasks = [long(m) for m in vkmodmasks]
                    vkkeysyms = self._vk.keysyms_from_keycode(
                        key.code, vkmodmasks)
                except AttributeError:
                    # virtkey until 0.61.0 didn't have that method.
                    vkkeysyms = []

                # replace all labels whith keysyms matching a keysym rule
                for i, keysym in enumerate(vkkeysyms):
                    attributes = keysym_rules.get(keysym)
                    if attributes:
                        label = attributes.get("label")
                        if not label is None:
                            mask = vkmodmasks[i]
                            labels[mask] = label

        # Translate labels - Gettext behaves oddly when translating
        # empty strings
        return {mask: lab and _(lab) or None for mask, lab in labels.items()}

    def _parse_layout_labels(self, attributes):
        """ Deprecated label definitions up to v0.98.x """
        labels = {}
        # modifier masks were hard-coded in python-virtkey
        if "label" in attributes:
            labels[0] = attributes["label"]
            if "cap_label" in attributes:
                labels[1] = attributes["cap_label"]
            if "shift_label" in attributes:
                labels[2] = attributes["shift_label"]
            if "altgr_label" in attributes:
                labels[128] = attributes["altgr_label"]
            if "altgrNshift_label" in attributes:
                labels[129] = attributes["altgrNshift_label"]
            if "_label" in attributes:
                labels[129] = attributes["altgrNshift_label"]
        return labels

    def _get_svg_keys(self, filename):
        svg_nodes = self._svg_cache.get(filename)
        if svg_nodes is None:
            svg_nodes = self._load_svg_keys(filename)
            self._svg_cache[filename] = svg_nodes

        return svg_nodes

    def _load_svg_keys(self, filename):
        filename = os.path.join(self._root_layout_dir, filename)
        try:
            with open_utf8(filename) as svg_file:
                svg_dom = minidom.parse(svg_file).documentElement
                svg_nodes = self._parse_svg(svg_dom)
                svg_nodes = {node.id: node for node in svg_nodes}
        except Exceptions.LayoutFileError as ex:
            raise Exceptions.LayoutFileError(
                "error loading '{}'".format(filename), chained_exception=(ex))
        return svg_nodes

    def _parse_svg(self, node):
        svg_nodes = []
        for child in node.childNodes:
            if child.nodeType == minidom.Node.ELEMENT_NODE:
                tag = child.tagName
                if tag in ("rect", "path", "g"):
                    svg_node = SVGNode()
                    id = child.attributes["id"].value
                    svg_node.id = id

                    if tag == "rect":
                        svg_node.bounds = \
                                   Rect(float(child.attributes['x'].value),
                                        float(child.attributes['y'].value),
                                        float(child.attributes['width'].value),
                                        float(child.attributes['height'].value))

                    elif tag == "path":
                        data = child.attributes['d'].value

                        try:
                            svg_node.path = KeyPath.from_svg_path(data)
                        except ValueError as ex:
                            raise Exceptions.LayoutFileError(
                                "while reading geometry with id '{}'".format(
                                    id),
                                chained_exception=(ex))

                        svg_node.bounds = svg_node.path.get_bounds()

                    elif tag == "g":  # group
                        svg_node.children = self._parse_svg(child)

                    svg_nodes.append(svg_node)

                svg_nodes.extend(self._parse_svg(child))

        return svg_nodes

    def find_template(self, scope_item, classinfo, ids):
        """
        Look for a template definition upwards from item until the root.
        """
        for item in scope_item.iter_to_global_root():
            templates = item.templates
            if templates:
                for id in ids:
                    match = templates.get((id, classinfo))
                    if not match is None:
                        return match
        return {}

    def _get_keysym_rules(self, scope_item):
        """
        Collect and merge keysym_rule from the root to item.
        Rules in nested items overwrite their parents'.
        """
        keysym_rules = {}
        for item in reversed(list(scope_item.iter_to_root())):
            if not item.keysym_rules is None:
                keysym_rules.update(item.keysym_rules)

        return keysym_rules

    def _get_system_keyboard_layout(self, vk):
        """ get names of the currently active layout group and variant """

        if vk:  # xkb keyboard found?
            group = vk.get_current_group()
            names = vk.get_rules_names()
        else:
            group = 0
            names = ""

        if not names:
            names = ("base", "pc105", "us", "", "")
        layouts = names[2].split(",")
        variants = names[3].split(",")

        if group >= 0 and group < len(layouts):
            layout = layouts[group]
        else:
            layout = ""
        if group >= 0 and group < len(variants):
            variant = variants[group]
        else:
            variant = ""

        return layout, variant

    def _get_system_layout_string(self):
        s = self._system_layout
        if self._system_variant:
            s += "(" + self._system_variant + ")"
        return s

    def _has_matching_layout(self, layout_str):
        """
        Check if one ot the given layout strings matches
        system keyboard layout and variant.

        Doctests:
        >>> l = LayoutLoaderSVG()
        >>> l._system_layout = "ch"
        >>> l._system_variant = "fr"
        >>> l._has_matching_layout("ch(x), us, de")
        False
        >>> l._has_matching_layout("abc, ch(fr)")
        True
        >>> l._system_variant = ""
        >>> l._has_matching_layout("ch(x), us, de")
        False
        >>> l._has_matching_layout("ch, us, de")
        True
        """
        layouts = layout_str.split(",")  # comma separated layout specifiers
        sys_layout = self._system_layout
        sys_variant = self._system_variant
        for value in layouts:
            layout, variant = self._layout_regex.search(value.strip()).groups()
            if layout == sys_layout and \
               (not variant or sys_variant.startswith(variant)):
                return True
        return False

    # --------------------------------------------------------------------------
    # Legacy pane layout support
    # --------------------------------------------------------------------------
    def _parse_legacy_layout(self, dom_node):

        # parse panes
        panes = []
        is_scan = False
        for i, pane_node in enumerate(dom_node.getElementsByTagName("pane")):
            item = LayoutPanel()
            item.layer_id = "layer {}".format(i)

            item.id = pane_node.attributes["id"].value
            item.filename = pane_node.attributes["filename"].value

            # parse keys
            keys = []
            for node in pane_node.getElementsByTagName("key"):
                key = self._parse_key(node, item)
                if key:
                    # some keys have changed since Onboard 0.95
                    if key.id == "middleClick":
                        key.set_id("middleclick")
                        key.type = KeyCommon.BUTTON_TYPE
                    if key.id == "secondaryClick":
                        key.set_id("secondaryclick")
                        key.type = KeyCommon.BUTTON_TYPE

                    keys.append(key)

            item.set_items(keys)

            # check for scan columns
            if pane_node.getElementsByTagName("column"):
                is_scan = True

            panes.append(item)

        layer_area = LayoutPanel()
        layer_area.id = "layer_area"
        layer_area.set_items(panes)

        # find the most frequent key width
        histogram = {}
        for key in layer_area.iter_keys():
            w = key.get_border_rect().w
            histogram[w] = histogram.get(w, 0) + 1
        most_frequent_width = max(list(zip(list(histogram.values()), list(histogram.keys()))))[1] \
                              if histogram else 18

        # Legacy onboard had automatic tab-keys for pane switching.
        # Simulate this by generating layer buttons from scratch.
        keys = []
        group = "__layer_buttons__"
        widen = 1.4 if not is_scan else 1.0
        rect = Rect(0, 0, most_frequent_width * widen, 20)

        key = RectKey()
        attributes = {}
        attributes["id"] = "hide"
        attributes["group"] = group
        attributes["image"] = "close.svg"
        attributes["button"] = "true"
        attributes["scannable"] = "false"
        self._init_key(key, attributes)
        key.set_border_rect(rect.copy())
        keys.append(key)

        key = RectKey()
        attributes = {}
        attributes["id"] = "move"
        attributes["group"] = group
        attributes["image"] = "move.svg"
        attributes["button"] = "true"
        attributes["scannable"] = "false"
        self._init_key(key, attributes)
        key.set_border_rect(rect.copy())
        keys.append(key)

        if len(panes) > 1:
            for i, pane in enumerate(panes):
                key = RectKey()
                attributes = {}
                attributes["id"] = "layer{}".format(i)
                attributes["group"] = group
                attributes["label"] = pane.id
                attributes["button"] = "true"
                self._init_key(key, attributes)
                key.set_border_rect(rect.copy())
                keys.append(key)

        layer_switch_column = LayoutBox()
        layer_switch_column.horizontal = False
        layer_switch_column.set_items(keys)

        layout = LayoutBox()
        layout.border = 1
        layout.spacing = 2
        layout.set_items([layer_area, layer_switch_column])

        return [layout]

    @staticmethod
    def copy_layout(src_filename, dst_filename):
        src_dir = os.path.dirname(src_filename)
        dst_dir, name_ext = os.path.split(dst_filename)
        dst_basename, ext = os.path.splitext(name_ext)
        _logger.info(
            _format("copying layout '{}' to '{}'", src_filename, dst_filename))

        domdoc = None
        svg_filenames = {}
        fallback_layers = {}

        try:
            with open_utf8(src_filename) as f:
                domdoc = minidom.parse(f)
                keyboard_node = domdoc.documentElement

                # check layout format
                format = LayoutLoaderSVG.LAYOUT_FORMAT_LEGACY
                if keyboard_node.hasAttribute("format"):
                    format = Version.from_string(
                        keyboard_node.attributes["format"].value)
                keyboard_node.attributes["id"] = dst_basename

                if format < LayoutLoaderSVG.LAYOUT_FORMAT_LAYOUT_TREE:
                    raise Exceptions.LayoutFileError( \
                        _format("copy_layouts failed, unsupported layout format '{}'.",
                                format))
                else:
                    # replace the basename of all svg filenames
                    for node in LayoutLoaderSVG._iter_dom_nodes(keyboard_node):
                        if LayoutLoaderSVG.is_layout_node(node):
                            if node.hasAttribute("filename"):
                                filename = node.attributes["filename"].value

                                # Create a replacement layer name for the unlikely
                                # case  that the svg-filename doesn't contain a
                                # layer section (as in path/basename-layer.ext).
                                fallback_layer_name = fallback_layers.get(
                                    filename,
                                    "Layer" + str(len(fallback_layers)))
                                fallback_layers[filename] = fallback_layer_name

                                # replace the basename of this filename
                                new_filename = LayoutLoaderSVG._replace_basename( \
                                     filename, dst_basename, fallback_layer_name)

                                node.attributes[
                                    "filename"].value = new_filename
                                svg_filenames[filename] = new_filename

            if domdoc:
                XDGDirs.assure_user_dir_exists(config.get_user_layout_dir())

                # write the new layout file
                with open_utf8(dst_filename, "w") as f:
                    xml = toprettyxml(domdoc)
                    if sys.version_info.major == 2:  # python 2?
                        xml = xml.encode("UTF-8")
                    f.write(xml)

                    # copy the svg files
                    for src, dst in list(svg_filenames.items()):

                        dir, name = os.path.split(src)
                        if not dir:
                            src = os.path.join(src_dir, name)
                        dir, name = os.path.split(dst)
                        if not dir:
                            dst = os.path.join(dst_dir, name)

                        _logger.info(_format("copying svg file '{}' to '{}'", \
                                     src, dst))
                        shutil.copyfile(src, dst)
        except OSError as ex:
            _logger.error("copy_layout failed: " + \
                          unicode_str(ex))
        except Exceptions.LayoutFileError as ex:
            _logger.error(unicode_str(ex))

    @staticmethod
    def remove_layout(filename):
        for fn in LayoutLoaderSVG.get_layout_svg_filenames(filename):
            os.remove(fn)
        os.remove(filename)

    @staticmethod
    def get_layout_svg_filenames(filename):
        results = []
        domdoc = None
        with open_utf8(filename) as f:
            domdoc = minidom.parse(f).documentElement

        if domdoc:
            filenames = {}
            for node in LayoutLoaderSVG._iter_dom_nodes(domdoc):
                if LayoutLoaderSVG.is_layout_node(node):
                    if node.hasAttribute("filename"):
                        fn = node.attributes["filename"].value
                        filenames[fn] = fn

            layout_dir, name = os.path.split(filename)
            results = []
            for fn in list(filenames.keys()):
                dir, name = os.path.split(fn)
                results.append(os.path.join(layout_dir, name))

        return results

    @staticmethod
    def _replace_basename(filename, new_basename, fallback_layer_name):
        """
        Doctests:
        # Basename has to be replaced with new_basename.
        >>> test = LayoutLoaderSVG._replace_basename
        >>> test("/home/usr/.local/share/onboard/Base-Alpha.svg",
        ... "NewBase","Fallback")
        'NewBase-Alpha.svg'

        # Dashes in front are allowed, but the layer name must not have any.
        >>> test("/home/usr/.local/share/onboard/a-b-c-Alpha.svg",
        ... "d-e-f","g-h")
        'd-e-f-Alpha.svg'
        """
        dir, name_ext = os.path.split(filename)
        name, ext = os.path.splitext(name_ext)
        if name:
            index = name.rfind("-")
            if index >= 0:
                layer = name[index + 1:]
            else:
                layer = fallback_layer_name
            return "{}-{}{}".format(new_basename, layer, ext)
        return ""

    @staticmethod
    def is_layout_node(dom_node):
        return dom_node.tagName in [
            "include", "key_template", "keysym_rule", "box", "panel", "key",
            "layout"
        ]

    @staticmethod
    def _iter_dom_nodes(dom_node):
        """ Recursive generator function to traverse aa dom tree """
        yield dom_node

        for child in dom_node.childNodes:
            if child.nodeType == minidom.Node.ELEMENT_NODE:
                for node in LayoutLoaderSVG._iter_dom_nodes(child):
                    yield node
Exemple #18
0
    def load(filename, is_system=False):
        """ Load a theme and return a new theme object. """

        result = None

        _file = open(filename)
        try:
            domdoc = minidom.parse(_file).documentElement
            try:
                theme = Theme()

                node = domdoc.attributes.get("format")
                format = Version.from_string(node.value) \
                         if node else Theme.THEME_FORMAT_INITIAL

                theme.name = domdoc.attributes["name"].value

                # "color_scheme" is the base file name of the color scheme
                text = utils.xml_get_text(domdoc, "color_scheme")
                if not text is None:
                    theme.color_scheme_basename = text

                # get key label overrides
                nodes = domdoc.getElementsByTagName("key_label_overrides")
                if nodes:
                    overrides = nodes[0]
                    tuples = {}
                    for override in overrides.getElementsByTagName("key"):
                        key_id = override.attributes["id"].value
                        node = override.attributes.get("label")
                        label = node.value if node else ""
                        node = override.attributes.get("group")
                        group = node.value if node else ""
                        tuples[key_id] = (label, group)
                    theme.key_label_overrides = tuples

                # read all other members
                for name, _type, _default in Theme.attributes:
                    if not name in ["color_scheme_basename",
                                    "key_label_overrides"]:
                        value = utils.xml_get_text(domdoc, name)
                        if not value is None:

                            if _type == "i":
                                value = int(value)
                            if _type == "f":
                                value = float(value)

                            # upgrade to current file format
                            if format < Theme.THEME_FORMAT_1_1:
                                # direction was    0..360, ccw
                                #        is now -180..180, cw
                                if name == "key_gradient_direction":
                                    value = -(value % 360)
                                    if value <= -180:
                                        value += 360

                            setattr(theme, name, value)

                theme.filename = filename
                theme.is_system = is_system
                theme.system_exists = is_system
                result = theme
            except Exceptions.ThemeFileError as xxx_todo_changeme:
                (ex) = xxx_todo_changeme
                raise Exceptions.ThemeFileError(_("Error loading ")
                    + filename, chained_exception = ex)
            finally:
                domdoc.unlink()

        finally:
            _file.close()

        return result