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
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
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
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
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 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)
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()
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
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)
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()
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 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
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