35
33
from Onboard import Exceptions
36
34
from Onboard import KeyCommon
37
from Onboard.KeyCommon import StickyBehavior, ImageSlot, \
35
from Onboard.KeyCommon import (StickyBehavior, ImageSlot,
39
37
from Onboard.Layout import LayoutRoot, LayoutBox, LayoutPanel
40
from Onboard.utils import modifiers, Rect, \
41
toprettyxml, Version, open_utf8, \
42
permute_mask, LABEL_MODIFIERS, \
38
from Onboard.utils import (modifiers, Rect,
39
toprettyxml, Version, open_utf8,
40
permute_mask, LABEL_MODIFIERS,
45
43
# Layout items that can be created dynamically via the 'class' XML attribute.
46
from Onboard.WordSuggestions import WordListPanel
47
from Onboard.KeyGtk import RectKey, WordlistKey, BarKey, \
44
from Onboard.WordSuggestions import WordListPanel # noqa: flake8
45
from Onboard.KeyGtk import (RectKey, WordlistKey, BarKey, # noqa: flake8
46
WordKey, InputlineKey)
50
### Config Singleton ###
51
48
from Onboard.Config import Config
53
########################
56
52
class LayoutLoaderSVG:
101
97
def load(self, vk, layout_filename, color_scheme):
102
98
""" Load layout root file. """
103
99
self._system_layout, self._system_variant = \
104
self._get_system_keyboard_layout(vk)
105
_logger.info("current system keyboard layout(variant): '{}'" \
100
self._get_system_keyboard_layout(vk)
101
_logger.info("current system keyboard layout(variant): '{}'"
106
102
.format(self._get_system_layout_string()))
108
104
layout = self._load(vk, layout_filename, color_scheme,
111
107
# purge attributes only used during loading
112
108
for item in layout.iter_items():
113
if not item.templates is None:
109
if item.templates is not None:
114
110
item.templates = None
115
if not item.keysym_rules is None:
111
if item.keysym_rules is not None:
116
112
item.keysym_rules = None
124
def _load(self, vk, layout_filename, color_scheme, root_layout_dir, parent_item = None):
119
def _load(self, vk, layout_filename, color_scheme,
120
root_layout_dir, parent_item=None):
125
121
""" Load or include layout file at any depth level. """
127
123
self._layout_filename = layout_filename
129
125
self._root_layout_dir = root_layout_dir
130
126
return self._load_layout(layout_filename, parent_item)
132
def _load_layout(self, layout_filename, parent_item = None):
128
def _load_layout(self, layout_filename, parent_item=None):
133
129
self._svg_cache = {}
146
142
# check layout format, no format version means legacy layout
147
143
format = self.LAYOUT_FORMAT_LEGACY
148
144
if dom.hasAttribute("format"):
149
format = Version.from_string(dom.attributes["format"].value)
145
format = Version.from_string(dom.attributes["format"].value)
150
146
self._format = format
152
root = LayoutPanel() # root, representing the 'keyboard' tag
153
root.set_id("__root__") # id for debug prints
148
root = LayoutPanel() # root, representing the 'keyboard' tag
149
root.set_id("__root__") # id for debug prints
155
151
# Init included root with the parent item's svg filename.
156
152
# -> Allows to skip specifying svg filenames in includes.
161
157
self._parse_dom_node(dom, root)
164
_logger.warning(_format("Loading legacy layout, format '{}'. "
161
_format("Loading legacy layout, format '{}'. "
165
162
"Please consider upgrading to current format '{}'",
166
163
format, self.LAYOUT_FORMAT))
167
164
items = self._parse_legacy_layout(dom)
247
243
parent.append_items(incl_root.items)
248
244
parent.update_keysym_rules(incl_root.keysym_rules)
249
245
parent.update_templates(incl_root.templates)
250
incl_root.items = None # help garbage collector
246
incl_root.items = None # help garbage collector
251
247
incl_root.keysym_rules = None
252
248
incl_root.templates = None
260
256
id = attributes.get("id")
262
258
raise Exceptions.LayoutFileError(
263
"'id' attribute required for template '{} {}' "
266
str(list(attributes.values())),
259
"'id' attribute required for template '{}' "
261
.format(str(list(attributes.values())),
267
262
self._layout_filename))
269
264
parent.update_templates({(id, RectKey) : attributes})
301
296
item = item_class()
303
298
value = attributes.get("id")
304
if not value is None:
299
if value is not None:
307
302
value = attributes.get("group")
308
if not value is None:
303
if value is not None:
309
304
item.group = value
311
306
value = attributes.get("layer")
312
if not value is None:
307
if value is not None:
313
308
item.layer_id = value
315
310
value = attributes.get("filename")
316
if not value is None:
311
if value is not None:
317
312
item.filename = value
319
314
value = attributes.get("visible")
320
if not value is None:
315
if value is not None:
321
316
item.visible = value == "true"
323
318
value = attributes.get("sensitive")
324
if not value is None:
319
if value is not None:
325
320
item.sensitive = value == "true"
327
322
value = attributes.get("border")
328
if not value is None:
323
if value is not None:
329
324
item.border = float(value)
331
326
value = attributes.get("expand")
332
if not value is None:
327
if value is not None:
333
328
item.expand = value == "true"
335
330
value = attributes.get("unlatch_layer")
336
if not value is None:
331
if value is not None:
337
332
item.unlatch_layer = value == "true"
339
334
value = attributes.get("scannable")
349
344
def _parse_sublayout(self, node, parent):
350
345
attributes = dict(node.attributes.items())
351
346
item = self._init_item(attributes, LayoutPanel)
352
item.sublayout_parent = parent # make templates accessible in the subl.
348
# make templates accessible in the sublayout
349
item.sublayout_parent = parent
355
352
def _parse_box(self, node):
423
420
_logger.info("Ignoring key '{}'."
424
" No svg object found for '{}'." \
421
" No svg object found for '{}'."
425
422
.format(key.id, key.svg_id))
427
424
return result # ignore keys not found in an svg file
435
432
key.set_id(full_id, theme_id, svg_id)
437
434
if "_" in key.get_id():
438
_logger.warning("underscore in key id '{}', please use dashes" \
435
_logger.warning("underscore in key id '{}', please use dashes"
439
436
.format(key.get_id()))
441
438
value = attributes.get("modifier")
444
441
key.modifier = modifiers[value]
445
442
except KeyError as ex:
447
raise Exceptions.LayoutFileError("Unrecognized modifier %s in" \
444
raise Exceptions.LayoutFileError(
445
"Unrecognized modifier %s in"
448
446
"definition of %s" (strerror, full_id))
450
448
value = attributes.get("action")
453
451
key.action = KeyCommon.actions[value]
454
452
except KeyError as ex:
456
raise Exceptions.LayoutFileError("Unrecognized key action {} in" \
454
raise Exceptions.LayoutFileError(
455
"Unrecognized key action {} in"
457
456
"definition of {}".format(strerror, full_id))
459
458
if "char" in attributes:
462
461
elif "keysym" in attributes:
463
462
value = attributes["keysym"]
464
463
key.type = KeyCommon.KEYSYM_TYPE
465
if value[1] == "x":#Deals for when keysym is hex
466
key.code = int(value,16)
464
if value[1] == "x": # for hex keysym
465
key.code = int(value, 16)
468
key.code = int(value,10)
467
key.code = int(value, 10)
469
468
elif "keypress_name" in attributes:
470
469
key.code = attributes["keypress_name"]
471
470
key.type = KeyCommon.KEYPRESS_NAME_TYPE
498
497
# get the optional image filename
499
498
if "image" in attributes:
500
if not key.image_filenames: key.image_filenames = {}
501
key.image_filenames[ImageSlot.NORMAL] = attributes["image"].split(";")[0]
499
if not key.image_filenames:
500
key.image_filenames = {}
501
key.image_filenames[ImageSlot.NORMAL] = \
502
attributes["image"].split(";")[0]
502
503
if "image_active" in attributes:
503
if not key.image_filenames: key.image_filenames = {}
504
if not key.image_filenames:
505
key.image_filenames = {}
504
506
key.image_filenames[ImageSlot.ACTIVE] = attributes["image_active"]
552
554
key.label_y_align = float(attributes["label_y_align"])
554
556
if "label_margin" in attributes:
555
values = attributes["label_margin"].replace(" ","").split(",")
556
margin = [float(x) if x else key.label_margin[i] \
557
values = attributes["label_margin"].replace(" ", "").split(",")
558
margin = [float(x) if x else key.label_margin[i]
557
559
for i, x in enumerate(values[:2])]
558
margin += margin[:1]*(2 - len(margin))
560
margin += margin[:1] * (2 - len(margin))
560
562
key.label_margin = margin
583
585
key.sticky_behavior = StickyBehavior.from_string(value)
584
586
except KeyError as ex:
586
raise Exceptions.LayoutFileError("Unrecognized sticky behavior {} in" \
588
raise Exceptions.LayoutFileError(
589
"Unrecognized sticky behavior {} in"
587
590
"definition of {}".format(strerror, full_id))
589
592
if "tooltip" in attributes:
603
606
# Get labels from keyboard mapping first.
604
607
if key.type == KeyCommon.KEYCODE_TYPE and \
605
not key.id in ["BKSP"]:
606
if self._vk: # xkb keyboard found?
608
key.id not in ["BKSP"]:
609
if self._vk: # xkb keyboard found?
607
610
vkmodmasks = self._label_modifier_masks
608
611
if sys.version_info.major == 2:
609
vkmodmasks = [long(m) for m in vkmodmasks]
612
vkmodmasks = [int(m) for m in vkmodmasks]
610
613
vklabels = self._vk.labels_from_keycode(key.code, vkmodmasks)
611
614
if sys.version_info.major == 2:
612
615
vklabels = [x.decode("UTF-8") for x in vklabels]
620
623
# If key is a macro (snippet) generate label from its number.
621
624
elif key.type == KeyCommon.MACRO_TYPE:
622
label, text = config.snippets.get(int(key.code), \
625
label, text = config.snippets.get(int(key.code), (None, None))
624
626
tooltip = _format("Snippet {}", key.code)
626
628
labels[0] = " -- "
638
640
# override with per-keysym labels
639
641
keysym_rules = self._get_keysym_rules(key)
640
642
if key.type == KeyCommon.KEYCODE_TYPE:
641
if self._vk: # xkb keyboard found?
643
if self._vk: # xkb keyboard found?
642
644
vkmodmasks = self._label_modifier_masks
644
646
if sys.version_info.major == 2:
645
vkmodmasks = [long(m) for m in vkmodmasks]
647
vkmodmasks = [int(m) for m in vkmodmasks]
646
648
vkkeysyms = self._vk.keysyms_from_keycode(key.code,
648
650
except AttributeError:
654
656
attributes = keysym_rules.get(keysym)
656
658
label = attributes.get("label")
657
if not label is None:
659
if label is not None:
658
660
mask = vkmodmasks[i]
659
661
labels[mask] = label
661
663
# Translate labels - Gettext behaves oddly when translating
663
return { mask : lab and _(lab) or None
664
for mask, lab in labels.items()}
665
return {mask : lab and _(lab) or None
666
for mask, lab in labels.items()}
666
668
def _parse_layout_labels(self, attributes):
667
669
""" Deprecated label definitions up to v0.98.x """
699
701
except Exceptions.LayoutFileError as ex:
700
702
raise Exceptions.LayoutFileError(
701
703
"error loading '{}'".format(filename),
702
chained_exception = (ex))
704
chained_exception=(ex))
705
707
def _parse_svg(self, node):
715
717
if tag == "rect":
716
718
svg_node.bounds = \
717
Rect(float(child.attributes['x'].value),
718
float(child.attributes['y'].value),
719
float(child.attributes['width'].value),
720
float(child.attributes['height'].value))
719
Rect(float(child.attributes['x'].value),
720
float(child.attributes['y'].value),
721
float(child.attributes['width'].value),
722
float(child.attributes['height'].value))
722
724
elif tag == "path":
723
725
data = child.attributes['d'].value
726
728
svg_node.path = KeyPath.from_svg_path(data)
727
729
except ValueError as ex:
728
730
raise Exceptions.LayoutFileError(
729
"while reading geometry with id '{}'".format(id),
730
chained_exception = (ex))
731
"while reading geometry with id '{}'"
733
chained_exception=(ex))
732
735
svg_node.bounds = svg_node.path.get_bounds()
768
771
def _get_system_keyboard_layout(self, vk):
769
772
""" get names of the currently active layout group and variant """
771
if vk: # xkb keyboard found?
774
if vk: # xkb keyboard found?
772
775
group = vk.get_current_group()
773
776
names = vk.get_rules_names()
874
876
for key in layer_area.iter_keys():
875
877
w = key.get_border_rect().w
876
878
histogram[w] = histogram.get(w, 0) + 1
877
most_frequent_width = max(list(zip(list(histogram.values()), list(histogram.keys()))))[1] \
879
most_frequent_width = max(list(zip(list(histogram.values()),
880
list(histogram.keys()))))[1] \
880
883
# Legacy onboard had automatic tab-keys for pane switching.
881
884
# Simulate this by generating layer buttons from scratch.
949
952
# check layout format
950
953
format = LayoutLoaderSVG.LAYOUT_FORMAT_LEGACY
951
954
if keyboard_node.hasAttribute("format"):
952
format = Version.from_string(keyboard_node.attributes["format"].value)
955
format = Version.from_string(
956
keyboard_node.attributes["format"].value)
953
957
keyboard_node.attributes["id"] = dst_basename
955
959
if format < LayoutLoaderSVG.LAYOUT_FORMAT_LAYOUT_TREE:
956
raise Exceptions.LayoutFileError( \
957
_format("copy_layouts failed, unsupported layout format '{}'.",
960
raise Exceptions.LayoutFileError(
961
_format("copy_layouts failed, "
962
"unsupported layout format '{}'.",
960
965
# replace the basename of all svg filenames
963
968
if node.hasAttribute("filename"):
964
969
filename = node.attributes["filename"].value
966
# Create a replacement layer name for the unlikely
967
# case that the svg-filename doesn't contain a
968
# layer section (as in path/basename-layer.ext).
969
fallback_layer_name = fallback_layers.get(filename,
970
"Layer" + str(len(fallback_layers)))
971
# Create a replacement layer name for the
972
# unlikely case that the svg-filename doesn't
973
# contain a layer section
974
# (as in path/basename-layer.ext).
975
fallback_layer_name = fallback_layers.get(
977
"Layer" + str(len(fallback_layers)))
971
978
fallback_layers[filename] = fallback_layer_name
973
980
# replace the basename of this filename
974
new_filename = LayoutLoaderSVG._replace_basename( \
975
filename, dst_basename, fallback_layer_name)
982
LayoutLoaderSVG._replace_basename(
983
filename, dst_basename,
977
node.attributes["filename"].value = new_filename
986
node.attributes["filename"].value = \
978
988
svg_filenames[filename] = new_filename
998
1008
dst = os.path.join(dst_dir, name)
1000
_logger.info(_format("copying svg file '{}' to '{}'", \
1010
_logger.info(_format("copying svg file '{}' to '{}'",
1002
1012
shutil.copyfile(src, dst)
1003
1013
except OSError as ex:
1004
_logger.error("copy_layout failed: " + \
1014
_logger.error("copy_layout failed: " +
1005
1015
unicode_str(ex))
1006
1016
except Exceptions.LayoutFileError as ex:
1007
1017
_logger.error(unicode_str(ex))
1011
1020
def remove_layout(filename):
1012
1021
for fn in LayoutLoaderSVG.get_layout_svg_filenames(filename):