# -*- coding: utf-8 -*- # Copyright © 2008-2009 Chris Jones # Copyright © 2010 Francesco Fumanti # Copyright © 2011-2016 marmuta # # This file is part of Onboard. # # Onboard is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # Onboard is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from __future__ import division, print_function, unicode_literals ### Logging ### import logging _logger = logging.getLogger("LayoutLoaderSVG") ############### import os import re import sys import shutil from xml.dom import minidom from Onboard import Exceptions from Onboard import KeyCommon from Onboard.KeyCommon import StickyBehavior, ImageSlot, \ KeyPath, KeyGeometry from Onboard.Layout import LayoutRoot, LayoutBox, LayoutPanel from Onboard.utils import modifiers, Rect, \ toprettyxml, Version, open_utf8, \ permute_mask, LABEL_MODIFIERS, \ unicode_str, XDGDirs # Layout items that can be created dynamically via the 'class' XML attribute. from Onboard.WordSuggestions import WordListPanel from Onboard.KeyGtk import RectKey, WordlistKey, BarKey, \ WordKey, InputlineKey ### Config Singleton ### from Onboard.Config import Config config = Config() ######################## 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 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 _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 class SVGNode: """ Cache of SVG provided key attributes. """ id = None # svg_id bounds = None # logical bounding rect, aka border rect path = None # optional path for arbitrary shapes def __init__(self): self.children = [] def extract_key_params(self): if self.children: nodes = self.children[:2] else: nodes = [self] bounds = nodes[0].bounds paths = [node.path for node in nodes if node.path] if paths: geometry = KeyGeometry.from_paths(paths) else: geometry = None return bounds, geometry