Merge lp:~ermshiperete/onboard/keyman into lp:onboard

Proposed by Eberhard Beilharz
Status: Needs review
Proposed branch: lp:~ermshiperete/onboard/keyman
Merge into: lp:onboard
Diff against target: 456 lines (+262/-14)
5 files modified
Onboard/Keyman.py (+203/-0)
Onboard/LayoutLoaderSVG.py (+16/-7)
Onboard/OnboardGtk.py (+33/-6)
Onboard/utils.py (+9/-0)
setup.py (+1/-1)
To merge this branch: bzr merge lp:~ermshiperete/onboard/keyman
Reviewer Review Type Date Requested Status
Onboard Devel Team Pending
Review via email: mp+414692@code.launchpad.net

Commit message

Add support for Keyman keyboards

This change will allow to show OSK for Keyman keyboards in addition to previously supported ones. See https://keyman.com for more information about Keyman.

Description of the change

Add support for Keyman keyboards

This change will allow to show OSK for Keyman keyboards in addition to previously supported ones. See https://keyman.com for more information about Keyman.

To post a comment you must log in.

Unmerged revisions

2296. By Eberhard Beilharz <email address hidden>

Add support for Keyman keyboards

This change will allow to show OSK for Keyman keyboards in addition
to previously supported ones.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'Onboard/Keyman.py'
--- Onboard/Keyman.py 1970-01-01 00:00:00 +0000
+++ Onboard/Keyman.py 2022-01-27 19:28:30 +0000
@@ -0,0 +1,203 @@
1# -*- coding: utf-8 -*-
2
3# Copyright © 2018 Daniel Glassey <dglassey@gmail.com>
4#
5# This file is part of Onboard.
6#
7# Onboard is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 3 of the License, or
10# (at your option) any later version.
11#
12# Onboard is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21Monitoring keyman keyboard
22"""
23
24from __future__ import division, print_function, unicode_literals
25
26try:
27 import dbus
28except ImportError:
29 pass
30
31from Onboard.utils import Modifiers
32
33from Onboard.Version import require_gi_versions
34require_gi_versions()
35from gi.repository import GObject
36from lxml import etree
37
38
39class KeymanDBus(GObject.GObject):
40 """
41 Keyman D-bus control and signal handling.
42 """
43 __gsignals__ = {
44 str('keyman-changed'): (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ())
45 }
46
47 KEYMAN_SCHEMA_ID = "com.Keyman"
48
49 KM_DBUS_NAME = "com.Keyman"
50 KM_DBUS_PATH = "/com/Keyman/IBus"
51 KM_DBUS_IFACE = "com.Keyman"
52 KM_DBUS_PROP_LDML = "LDMLFile"
53 KM_DBUS_PROP_NAME = "Name"
54
55 def __init__(self):
56 GObject.GObject.__init__(self)
57 self.key_labels = None
58 self.name = "None"
59 self._name_callbacks = []
60
61 if not "dbus" in globals():
62 raise ImportError("python-dbus unavailable")
63
64 # connect to session bus
65 try:
66 self._bus = dbus.SessionBus()
67 except dbus.exceptions.DBusException:
68 raise RuntimeError("D-Bus session bus unavailable")
69 self._bus.add_signal_receiver(self._on_name_owner_changed,
70 "NameOwnerChanged",
71 dbus.BUS_DAEMON_IFACE,
72 arg0=self.KM_DBUS_NAME)
73 # Initial state
74 proxy = self._bus.get_object(dbus.BUS_DAEMON_NAME, dbus.BUS_DAEMON_PATH)
75 result = proxy.NameHasOwner(self.KM_DBUS_NAME, dbus_interface=dbus.BUS_DAEMON_IFACE)
76 self._set_connection(bool(result))
77
78 def _set_connection(self, active):
79 ''' Update interface object, state and notify listeners '''
80 if active:
81 proxy = self._bus.get_object(self.KM_DBUS_NAME, self.KM_DBUS_PATH)
82 self._iface = dbus.Interface(proxy, dbus.PROPERTIES_IFACE)
83 self._iface.connect_to_signal("PropertiesChanged",
84 self._on_name_prop_changed)
85 self._LDMLFile = self._iface.Get(self.KM_DBUS_IFACE, self.KM_DBUS_PROP_LDML)
86 self.name = self._iface.Get(self.KM_DBUS_IFACE, self.KM_DBUS_PROP_NAME)
87
88 if self._LDMLFile and self.name != "None":
89 self.key_labels = KeymanLabels()
90 self.key_labels.parse_labels(self._LDMLFile)
91 else:
92 self.key_labels = None
93 else:
94 self._iface = None
95 self.name = "None"
96 self._LDMLFile = None
97 self.key_labels = None
98
99 def _on_name_owner_changed(self, name, old, new):
100 '''
101 The daemon has de/registered the name.
102 Called when ibus-kmfl un/loads a keyboard
103 '''
104 active = old == ""
105 self._set_connection(active)
106
107 def _on_name_prop_changed(self, iface, changed_props, invalidated_props):
108 ''' The Keyboard name has changed.'''
109 if self.KM_DBUS_PROP_NAME in changed_props:
110 self.name = changed_props.get(self.KM_DBUS_PROP_NAME)
111 self._LDMLFile = self._iface.Get(self.KM_DBUS_IFACE, self.KM_DBUS_PROP_LDML)
112 if self._LDMLFile and self.name != "None":
113 self.key_labels = KeymanLabels()
114 self.key_labels.parse_labels(self._LDMLFile)
115 else:
116 self.key_labels = None
117
118 # notify listeners
119 for callback in self._name_callbacks:
120 callback(self.name)
121
122 def on_keyboard_changed(self, keyboardid):
123 self.emit("keyman-changed", keyboardid)
124
125 ##########
126 # Public
127 ##########
128
129 def state_notify_add(self, callback):
130 """ Convenience function to subscribes to all notifications """
131 self.name_notify_add(callback)
132
133 def name_notify_add(self, callback):
134 self._name_callbacks.append(callback)
135
136 def is_active(self):
137 return bool(self._iface)
138
139
140class KeymanLabels():
141 keymankeys = {}
142 # keymanlabels is a dict of modmask : label (and also has "code" : keycode?)
143 # keymankeys is a dict of keycode : keymanlabels
144
145 def parse_labels(self, ldmlfile):
146 tree = etree.parse(ldmlfile)
147 root = tree.getroot()
148 keymaps = tree.findall('keyMap')
149
150 for keymap in keymaps:
151 if keymap.attrib:
152 # if there is more than one modifier set split it here
153 # because will need to duplicate the label set
154 keyman_modifiers = self.convert_ldml_modifiers_to_onboard(keymap.attrib['modifiers'])
155 else:
156 keyman_modifiers = (0,)
157 maps = keymap.findall('map')
158 for map in maps:
159 for keymanmodifier in keyman_modifiers:
160 iso = "A" + map.attrib['iso']
161 if iso == "AA03":
162 iso = "SPCE"
163 elif iso == "AE00":
164 iso = "TLDE"
165 elif iso == "AB00":
166 iso = "LSGT"
167 elif iso == "AC12":
168 iso = "BKSL"
169 if not iso in self.keymankeys:
170 self.keymankeys[iso] = { keymanmodifier : map.attrib['to'] }
171 else:
172 self.keymankeys[iso][keymanmodifier] = map.attrib['to']
173
174 def labels_from_id(self, id):
175 if id in self.keymankeys:
176 return self.keymankeys[id]
177 else:
178 print("unknown key id: ", id)
179 return {}
180
181 def convert_ldml_modifiers_to_onboard(self, modifiers):
182 list_modifiers = modifiers.split(" ")
183 keyman_modifiers = ()
184 for modifier in list_modifiers:
185 keymanmod = 0
186 keys = modifier.split("+")
187 for key in keys:
188 if "shift" == key:
189 keymanmod |= Modifiers.SHIFT
190 if "altR" == key:
191 keymanmod |= Modifiers.ALTGR
192 if "ctrlR" == key:
193 keymanmod |= Modifiers.MOD3
194 if "ctrlL" == key:
195 keymanmod |= Modifiers.CTRL
196 if "ctrl" == key:
197 keymanmod |= Modifiers.CTRL
198 if "altL" == key:
199 keymanmod |= Modifiers.ALT
200 if "alt" == key:
201 keymanmod |= Modifiers.ALT
202 keyman_modifiers = keyman_modifiers + (keymanmod,)
203 return keyman_modifiers
0204
=== modified file 'Onboard/LayoutLoaderSVG.py'
--- Onboard/LayoutLoaderSVG.py 2017-04-17 23:53:19 +0000
+++ Onboard/LayoutLoaderSVG.py 2022-01-27 19:28:30 +0000
@@ -38,7 +38,7 @@
38from Onboard.utils import (modifiers, Rect,38from Onboard.utils import (modifiers, Rect,
39 toprettyxml, Version, open_utf8,39 toprettyxml, Version, open_utf8,
40 permute_mask, LABEL_MODIFIERS,40 permute_mask, LABEL_MODIFIERS,
41 unicode_str, XDGDirs)41 KEYMAN_LABEL_MODIFIERS, unicode_str, XDGDirs)
4242
43# Layout items that can be created dynamically via the 'class' XML attribute.43# Layout items that can be created dynamically via the 'class' XML attribute.
44from Onboard.WordSuggestions import WordListPanel # noqa: flake844from Onboard.WordSuggestions import WordListPanel # noqa: flake8
@@ -86,9 +86,11 @@
8686
87 # precalc mask permutations87 # precalc mask permutations
88 _label_modifier_masks = permute_mask(LABEL_MODIFIERS)88 _label_modifier_masks = permute_mask(LABEL_MODIFIERS)
89 _keyman_label_modifier_masks = permute_mask(KEYMAN_LABEL_MODIFIERS)
8990
90 def __init__(self):91 def __init__(self):
91 self._vk = None92 self._vk = None
93 self._keyman_labels = None
92 self._svg_cache = {}94 self._svg_cache = {}
93 self._format = None # format of the currently loading layout95 self._format = None # format of the currently loading layout
94 self._layout_filename = ""96 self._layout_filename = ""
@@ -97,14 +99,14 @@
97 self._layout_regex = re.compile("([^\(]+) (?: \( ([^\)]*) \) )?",99 self._layout_regex = re.compile("([^\(]+) (?: \( ([^\)]*) \) )?",
98 re.VERBOSE)100 re.VERBOSE)
99101
100 def load(self, vk, layout_filename, color_scheme):102 def load(self, vk, keyman_labels, layout_filename, color_scheme):
101 """ Load layout root file. """103 """ Load layout root file. """
102 self._system_layout, self._system_variant = \104 self._system_layout, self._system_variant = \
103 self._get_system_keyboard_layout(vk)105 self._get_system_keyboard_layout(vk)
104 _logger.info("current system keyboard layout(variant): '{}'"106 _logger.info("current system keyboard layout(variant): '{}'"
105 .format(self._get_system_layout_string()))107 .format(self._get_system_layout_string()))
106108
107 layout = self._load(vk, layout_filename, color_scheme,109 layout = self._load(vk, keyman_labels, layout_filename, color_scheme,
108 os.path.dirname(layout_filename))110 os.path.dirname(layout_filename))
109 if layout:111 if layout:
110 # purge attributes only used during loading112 # purge attributes only used during loading
@@ -122,10 +124,10 @@
122124
123 return layout125 return layout
124126
125 def _load(self, vk, layout_filename, color_scheme,127 def _load(self, vk, keyman_labels, layout_filename, color_scheme, root_layout_dir, parent_item = None):
126 root_layout_dir, parent_item=None):
127 """ Load or include layout file at any depth level. """128 """ Load or include layout file at any depth level. """
128 self._vk = vk129 self._vk = vk
130 self._keyman_labels = keyman_labels
129 self._layout_filename = layout_filename131 self._layout_filename = layout_filename
130 self._color_scheme = color_scheme132 self._color_scheme = color_scheme
131 self._root_layout_dir = root_layout_dir133 self._root_layout_dir = root_layout_dir
@@ -254,6 +256,7 @@
254 filepath = config.find_layout_filename(filename, "layout include")256 filepath = config.find_layout_filename(filename, "layout include")
255 _logger.info("Including layout '{}'".format(filename))257 _logger.info("Including layout '{}'".format(filename))
256 incl_root = LayoutLoaderSVG()._load(self._vk,258 incl_root = LayoutLoaderSVG()._load(self._vk,
259 self._keyman_labels,
257 filepath,260 filepath,
258 self._color_scheme,261 self._color_scheme,
259 self._root_layout_dir,262 self._root_layout_dir,
@@ -632,7 +635,13 @@
632 # Get labels from keyboard mapping first.635 # Get labels from keyboard mapping first.
633 if key.type == KeyCommon.KEYCODE_TYPE and \636 if key.type == KeyCommon.KEYCODE_TYPE and \
634 key.id not in ["BKSP"]:637 key.id not in ["BKSP"]:
635 if self._vk: # xkb keyboard found?638 if self._keyman_labels: # using Keyman keyboard
639 # load the labels from self._keyman_labels
640 vkmodmasks = self._label_modifier_masks
641 if sys.version_info.major == 2:
642 vkmodmasks = [long(m) for m in vkmodmasks]
643 labels = self._keyman_labels.labels_from_id(key.id)
644 elif self._vk: # xkb keyboard found?
636 vkmodmasks = self._label_modifier_masks645 vkmodmasks = self._label_modifier_masks
637 if sys.version_info.major == 2:646 if sys.version_info.major == 2:
638 vkmodmasks = [int(m) for m in vkmodmasks]647 vkmodmasks = [int(m) for m in vkmodmasks]
@@ -666,7 +675,7 @@
666 # override with per-keysym labels675 # override with per-keysym labels
667 keysym_rules = self._get_keysym_rules(key)676 keysym_rules = self._get_keysym_rules(key)
668 if key.type == KeyCommon.KEYCODE_TYPE:677 if key.type == KeyCommon.KEYCODE_TYPE:
669 if self._vk: # xkb keyboard found?678 if not self._keyman_labels and self._vk: # xkb keyboard found but no keyman one?
670 vkmodmasks = self._label_modifier_masks679 vkmodmasks = self._label_modifier_masks
671 try:680 try:
672 if sys.version_info.major == 2:681 if sys.version_info.major == 2:
673682
=== modified file 'Onboard/OnboardGtk.py'
--- Onboard/OnboardGtk.py 2017-02-14 21:42:14 +0000
+++ Onboard/OnboardGtk.py 2022-01-27 19:28:30 +0000
@@ -30,6 +30,7 @@
30import time30import time
31import signal31import signal
32import os.path32import os.path
33from lxml import etree
3334
34from Onboard.Version import require_gi_versions35from Onboard.Version import require_gi_versions
35require_gi_versions()36require_gi_versions()
@@ -47,12 +48,13 @@
47from Onboard.KbdWindow import KbdWindow, KbdPlugWindow48from Onboard.KbdWindow import KbdWindow, KbdPlugWindow
48from Onboard.Keyboard import Keyboard49from Onboard.Keyboard import Keyboard
49from Onboard.KeyboardWidget import KeyboardWidget50from Onboard.KeyboardWidget import KeyboardWidget
51from Onboard.Keyman import KeymanDBus
50from Onboard.Indicator import Indicator52from Onboard.Indicator import Indicator
51from Onboard.LayoutLoaderSVG import LayoutLoaderSVG53from Onboard.LayoutLoaderSVG import LayoutLoaderSVG
52from Onboard.Appearance import ColorScheme54from Onboard.Appearance import ColorScheme
53from Onboard.IconPalette import IconPalette55from Onboard.IconPalette import IconPalette
54from Onboard.Exceptions import LayoutFileError56from Onboard.Exceptions import LayoutFileError
55from Onboard.utils import unicode_str57from Onboard.utils import unicode_str, Modifiers
56from Onboard.Timer import CallOnce, Timer58from Onboard.Timer import CallOnce, Timer
57from Onboard.WindowUtils import show_confirmation_dialog59from Onboard.WindowUtils import show_confirmation_dialog
58import Onboard.osk as osk60import Onboard.osk as osk
@@ -74,6 +76,8 @@
74 """76 """
7577
76 keyboard = None78 keyboard = None
79 keymandbus = None
80 _keyman_labels = None
7781
78 def __init__(self):82 def __init__(self):
7983
@@ -99,12 +103,14 @@
99 except dbus.exceptions.DBusException:103 except dbus.exceptions.DBusException:
100 err_msg = "D-Bus session bus unavailable"104 err_msg = "D-Bus session bus unavailable"
101 bus = None105 bus = None
106 self.keymandbus = KeymanDBus()
102107
103 if not bus:108 if not bus:
104 _logger.warning(err_msg + " " +109 _logger.warning(err_msg + " " +
105 "Onboard will start with reduced functionality. "110 "Onboard will start with reduced functionality. "
106 "Single-instance check, "111 "Single-instance check, "
107 "D-Bus service and "112 "Keyman support, "
113 "D-Bus service and "
108 "hover click are disabled.")114 "hover click are disabled.")
109115
110 # Yield to GNOME Shell's keyboard before any other D-Bus activity116 # Yield to GNOME Shell's keyboard before any other D-Bus activity
@@ -282,6 +288,9 @@
282 self.do_connect(self.keymap, "state-changed", self.cb_state_changed)288 self.do_connect(self.keymap, "state-changed", self.cb_state_changed)
283 # group changes289 # group changes
284 Gdk.event_handler_set(cb_any_event, self)290 Gdk.event_handler_set(cb_any_event, self)
291 # Keyman keyboard changes
292 if self.keymandbus:
293 self.keymandbus.name_notify_add(self.cb_keyman_changed)
285294
286 # connect config notifications here to keep config from holding295 # connect config notifications here to keep config from holding
287 # references to keyboard objects.296 # references to keyboard objects.
@@ -553,6 +562,12 @@
553 """ keyboard map change """562 """ keyboard map change """
554 self.reload_layout_delayed()563 self.reload_layout_delayed()
555564
565 def cb_keyman_changed(self, name):
566 """ keyman keyboard change """
567 _logger.debug("keyman changed to {}".format(name))
568 self._keyman_labels = self.keymandbus.key_labels
569 self.reload_layout_delayed()
570
556 def cb_state_changed(self, keymap):571 def cb_state_changed(self, keymap):
557 """ keyboard modifier state change """572 """ keyboard modifier state change """
558 mod_mask = keymap.get_modifier_state()573 mod_mask = keymap.get_modifier_state()
@@ -672,7 +687,7 @@
672 self.reload_layout(force_update = True)687 self.reload_layout(force_update = True)
673 self.keyboard_widget.update_transparency()688 self.keyboard_widget.update_transparency()
674689
675 def reload_layout_delayed(self):690 def reload_layout_delayed(self, force_update=False):
676 """691 """
677 Delay reloading the layout on keyboard map or group changes692 Delay reloading the layout on keyboard map or group changes
678 This is mainly for LP #1313176 when Caps-lock is set up as693 This is mainly for LP #1313176 when Caps-lock is set up as
@@ -689,14 +704,19 @@
689 Checks if the X keyboard layout has changed and704 Checks if the X keyboard layout has changed and
690 (re)loads Onboards layout accordingly.705 (re)loads Onboards layout accordingly.
691 """706 """
692 keyboard_state = (None, None)707 keyboard_state = (None, None, None)
693708
694 vk = self.get_vk()709 vk = self.get_vk()
695 if vk:710 if vk:
696 try:711 try:
697 vk.reload() # reload keyboard names712 vk.reload() # reload keyboard names
713 group = vk.get_current_group_name()
714 if self.keymandbus:
715 self._keyman_labels = self.keymandbus.key_labels
716
698 keyboard_state = (vk.get_layout_as_string(),717 keyboard_state = (vk.get_layout_as_string(),
699 vk.get_current_group_name())718 vk.get_current_group_name(),
719 self.keymandbus.name if self.keymandbus else None)
700 except osk.error:720 except osk.error:
701 self.reset_vk()721 self.reset_vk()
702 force_update = True722 force_update = True
@@ -704,6 +724,13 @@
704 "keyboard information failed")724 "keyboard information failed")
705725
706 if self.keyboard_state != keyboard_state or force_update:726 if self.keyboard_state != keyboard_state or force_update:
727 if self.keymandbus:
728 _logger.debug("Reloading layout. Layout: {}; Group: {}; Keyman: {}"
729 .format(vk.get_layout_as_string(), vk.get_current_group_name(), self.keymandbus.name))
730 else:
731 _logger.debug("Reloading layout. Layout: {}; Group: {}"
732 .format(vk.get_layout_as_string(), vk.get_current_group_name()))
733
707 self.keyboard_state = keyboard_state734 self.keyboard_state = keyboard_state
708735
709 layout_filename = config.layout_filename736 layout_filename = config.layout_filename
@@ -731,7 +758,7 @@
731758
732 color_scheme = ColorScheme.load(color_scheme_filename) \759 color_scheme = ColorScheme.load(color_scheme_filename) \
733 if color_scheme_filename else None760 if color_scheme_filename else None
734 layout = LayoutLoaderSVG().load(vk, layout_filename, color_scheme)761 layout = LayoutLoaderSVG().load(vk, self._keyman_labels, layout_filename, color_scheme)
735762
736 self.keyboard.set_layout(layout, color_scheme, vk)763 self.keyboard.set_layout(layout, color_scheme, vk)
737764
738765
=== modified file 'Onboard/utils.py'
--- Onboard/utils.py 2017-02-19 20:06:09 +0000
+++ Onboard/utils.py 2022-01-27 19:28:30 +0000
@@ -58,6 +58,15 @@
58 Modifiers.NUMLK | \58 Modifiers.NUMLK | \
59 Modifiers.ALTGR59 Modifiers.ALTGR
6060
61# Keyman uses more modifiers inc MOD3(RCTRL)
62KEYMAN_LABEL_MODIFIERS = Modifiers.SHIFT | \
63 Modifiers.CAPS | \
64 Modifiers.CTRL | \
65 Modifiers.ALT | \
66 Modifiers.NUMLK | \
67 Modifiers.MOD3 | \
68 Modifiers.ALTGR
69
61modifiers = {"shift":1,70modifiers = {"shift":1,
62 "caps":2,71 "caps":2,
63 "control":4,72 "control":4,
6473
=== modified file 'setup.py'
--- setup.py 2017-05-14 14:36:31 +0000
+++ setup.py 2022-01-27 19:28:30 +0000
@@ -231,7 +231,7 @@
231 "-Wsign-compare",231 "-Wsign-compare",
232 "-Wdeclaration-after-statement",232 "-Wdeclaration-after-statement",
233 "-Werror=declaration-after-statement",233 "-Werror=declaration-after-statement",
234 "-Wlogical-op"],234 "-fPIC" ],
235235
236 **pkgconfig('gdk-3.0', 'x11', 'xi', 'xtst', 'xkbfile',236 **pkgconfig('gdk-3.0', 'x11', 'xi', 'xtst', 'xkbfile',
237 'dconf', 'libcanberra', 'hunspell',237 'dconf', 'libcanberra', 'hunspell',

Subscribers

People subscribed via source and target branches