linuxOS_AP05/debian/test/usr/lib/python3/dist-packages/Onboard/LayoutLoaderSVG.py
2025-09-26 09:40:02 +08:00

1106 lines
41 KiB
Python

# -*- coding: utf-8 -*-
# Copyright © 2008-2009 Chris Jones <tortoise@tortuga>
# Copyright © 2010 Francesco Fumanti <francesco.fumanti@gmx.net>
# Copyright © 2011-2016 marmuta <marmvta@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
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