Merge lp:~ermshiperete/onboard/keyman into lp:onboard
- keyman
- Merge into trunk
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 |
Related bugs: |
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:/
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:/
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
1 | === added file 'Onboard/Keyman.py' |
2 | --- Onboard/Keyman.py 1970-01-01 00:00:00 +0000 |
3 | +++ Onboard/Keyman.py 2022-01-27 19:28:30 +0000 |
4 | @@ -0,0 +1,203 @@ |
5 | +# -*- coding: utf-8 -*- |
6 | + |
7 | +# Copyright © 2018 Daniel Glassey <dglassey@gmail.com> |
8 | +# |
9 | +# This file is part of Onboard. |
10 | +# |
11 | +# Onboard is free software; you can redistribute it and/or modify |
12 | +# it under the terms of the GNU General Public License as published by |
13 | +# the Free Software Foundation; either version 3 of the License, or |
14 | +# (at your option) any later version. |
15 | +# |
16 | +# Onboard is distributed in the hope that it will be useful, |
17 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
18 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
19 | +# GNU General Public License for more details. |
20 | +# |
21 | +# You should have received a copy of the GNU General Public License |
22 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
23 | + |
24 | +""" |
25 | +Monitoring keyman keyboard |
26 | +""" |
27 | + |
28 | +from __future__ import division, print_function, unicode_literals |
29 | + |
30 | +try: |
31 | + import dbus |
32 | +except ImportError: |
33 | + pass |
34 | + |
35 | +from Onboard.utils import Modifiers |
36 | + |
37 | +from Onboard.Version import require_gi_versions |
38 | +require_gi_versions() |
39 | +from gi.repository import GObject |
40 | +from lxml import etree |
41 | + |
42 | + |
43 | +class KeymanDBus(GObject.GObject): |
44 | + """ |
45 | + Keyman D-bus control and signal handling. |
46 | + """ |
47 | + __gsignals__ = { |
48 | + str('keyman-changed'): (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()) |
49 | + } |
50 | + |
51 | + KEYMAN_SCHEMA_ID = "com.Keyman" |
52 | + |
53 | + KM_DBUS_NAME = "com.Keyman" |
54 | + KM_DBUS_PATH = "/com/Keyman/IBus" |
55 | + KM_DBUS_IFACE = "com.Keyman" |
56 | + KM_DBUS_PROP_LDML = "LDMLFile" |
57 | + KM_DBUS_PROP_NAME = "Name" |
58 | + |
59 | + def __init__(self): |
60 | + GObject.GObject.__init__(self) |
61 | + self.key_labels = None |
62 | + self.name = "None" |
63 | + self._name_callbacks = [] |
64 | + |
65 | + if not "dbus" in globals(): |
66 | + raise ImportError("python-dbus unavailable") |
67 | + |
68 | + # connect to session bus |
69 | + try: |
70 | + self._bus = dbus.SessionBus() |
71 | + except dbus.exceptions.DBusException: |
72 | + raise RuntimeError("D-Bus session bus unavailable") |
73 | + self._bus.add_signal_receiver(self._on_name_owner_changed, |
74 | + "NameOwnerChanged", |
75 | + dbus.BUS_DAEMON_IFACE, |
76 | + arg0=self.KM_DBUS_NAME) |
77 | + # Initial state |
78 | + proxy = self._bus.get_object(dbus.BUS_DAEMON_NAME, dbus.BUS_DAEMON_PATH) |
79 | + result = proxy.NameHasOwner(self.KM_DBUS_NAME, dbus_interface=dbus.BUS_DAEMON_IFACE) |
80 | + self._set_connection(bool(result)) |
81 | + |
82 | + def _set_connection(self, active): |
83 | + ''' Update interface object, state and notify listeners ''' |
84 | + if active: |
85 | + proxy = self._bus.get_object(self.KM_DBUS_NAME, self.KM_DBUS_PATH) |
86 | + self._iface = dbus.Interface(proxy, dbus.PROPERTIES_IFACE) |
87 | + self._iface.connect_to_signal("PropertiesChanged", |
88 | + self._on_name_prop_changed) |
89 | + self._LDMLFile = self._iface.Get(self.KM_DBUS_IFACE, self.KM_DBUS_PROP_LDML) |
90 | + self.name = self._iface.Get(self.KM_DBUS_IFACE, self.KM_DBUS_PROP_NAME) |
91 | + |
92 | + if self._LDMLFile and self.name != "None": |
93 | + self.key_labels = KeymanLabels() |
94 | + self.key_labels.parse_labels(self._LDMLFile) |
95 | + else: |
96 | + self.key_labels = None |
97 | + else: |
98 | + self._iface = None |
99 | + self.name = "None" |
100 | + self._LDMLFile = None |
101 | + self.key_labels = None |
102 | + |
103 | + def _on_name_owner_changed(self, name, old, new): |
104 | + ''' |
105 | + The daemon has de/registered the name. |
106 | + Called when ibus-kmfl un/loads a keyboard |
107 | + ''' |
108 | + active = old == "" |
109 | + self._set_connection(active) |
110 | + |
111 | + def _on_name_prop_changed(self, iface, changed_props, invalidated_props): |
112 | + ''' The Keyboard name has changed.''' |
113 | + if self.KM_DBUS_PROP_NAME in changed_props: |
114 | + self.name = changed_props.get(self.KM_DBUS_PROP_NAME) |
115 | + self._LDMLFile = self._iface.Get(self.KM_DBUS_IFACE, self.KM_DBUS_PROP_LDML) |
116 | + if self._LDMLFile and self.name != "None": |
117 | + self.key_labels = KeymanLabels() |
118 | + self.key_labels.parse_labels(self._LDMLFile) |
119 | + else: |
120 | + self.key_labels = None |
121 | + |
122 | + # notify listeners |
123 | + for callback in self._name_callbacks: |
124 | + callback(self.name) |
125 | + |
126 | + def on_keyboard_changed(self, keyboardid): |
127 | + self.emit("keyman-changed", keyboardid) |
128 | + |
129 | + ########## |
130 | + # Public |
131 | + ########## |
132 | + |
133 | + def state_notify_add(self, callback): |
134 | + """ Convenience function to subscribes to all notifications """ |
135 | + self.name_notify_add(callback) |
136 | + |
137 | + def name_notify_add(self, callback): |
138 | + self._name_callbacks.append(callback) |
139 | + |
140 | + def is_active(self): |
141 | + return bool(self._iface) |
142 | + |
143 | + |
144 | +class KeymanLabels(): |
145 | + keymankeys = {} |
146 | + # keymanlabels is a dict of modmask : label (and also has "code" : keycode?) |
147 | + # keymankeys is a dict of keycode : keymanlabels |
148 | + |
149 | + def parse_labels(self, ldmlfile): |
150 | + tree = etree.parse(ldmlfile) |
151 | + root = tree.getroot() |
152 | + keymaps = tree.findall('keyMap') |
153 | + |
154 | + for keymap in keymaps: |
155 | + if keymap.attrib: |
156 | + # if there is more than one modifier set split it here |
157 | + # because will need to duplicate the label set |
158 | + keyman_modifiers = self.convert_ldml_modifiers_to_onboard(keymap.attrib['modifiers']) |
159 | + else: |
160 | + keyman_modifiers = (0,) |
161 | + maps = keymap.findall('map') |
162 | + for map in maps: |
163 | + for keymanmodifier in keyman_modifiers: |
164 | + iso = "A" + map.attrib['iso'] |
165 | + if iso == "AA03": |
166 | + iso = "SPCE" |
167 | + elif iso == "AE00": |
168 | + iso = "TLDE" |
169 | + elif iso == "AB00": |
170 | + iso = "LSGT" |
171 | + elif iso == "AC12": |
172 | + iso = "BKSL" |
173 | + if not iso in self.keymankeys: |
174 | + self.keymankeys[iso] = { keymanmodifier : map.attrib['to'] } |
175 | + else: |
176 | + self.keymankeys[iso][keymanmodifier] = map.attrib['to'] |
177 | + |
178 | + def labels_from_id(self, id): |
179 | + if id in self.keymankeys: |
180 | + return self.keymankeys[id] |
181 | + else: |
182 | + print("unknown key id: ", id) |
183 | + return {} |
184 | + |
185 | + def convert_ldml_modifiers_to_onboard(self, modifiers): |
186 | + list_modifiers = modifiers.split(" ") |
187 | + keyman_modifiers = () |
188 | + for modifier in list_modifiers: |
189 | + keymanmod = 0 |
190 | + keys = modifier.split("+") |
191 | + for key in keys: |
192 | + if "shift" == key: |
193 | + keymanmod |= Modifiers.SHIFT |
194 | + if "altR" == key: |
195 | + keymanmod |= Modifiers.ALTGR |
196 | + if "ctrlR" == key: |
197 | + keymanmod |= Modifiers.MOD3 |
198 | + if "ctrlL" == key: |
199 | + keymanmod |= Modifiers.CTRL |
200 | + if "ctrl" == key: |
201 | + keymanmod |= Modifiers.CTRL |
202 | + if "altL" == key: |
203 | + keymanmod |= Modifiers.ALT |
204 | + if "alt" == key: |
205 | + keymanmod |= Modifiers.ALT |
206 | + keyman_modifiers = keyman_modifiers + (keymanmod,) |
207 | + return keyman_modifiers |
208 | |
209 | === modified file 'Onboard/LayoutLoaderSVG.py' |
210 | --- Onboard/LayoutLoaderSVG.py 2017-04-17 23:53:19 +0000 |
211 | +++ Onboard/LayoutLoaderSVG.py 2022-01-27 19:28:30 +0000 |
212 | @@ -38,7 +38,7 @@ |
213 | from Onboard.utils import (modifiers, Rect, |
214 | toprettyxml, Version, open_utf8, |
215 | permute_mask, LABEL_MODIFIERS, |
216 | - unicode_str, XDGDirs) |
217 | + KEYMAN_LABEL_MODIFIERS, unicode_str, XDGDirs) |
218 | |
219 | # Layout items that can be created dynamically via the 'class' XML attribute. |
220 | from Onboard.WordSuggestions import WordListPanel # noqa: flake8 |
221 | @@ -86,9 +86,11 @@ |
222 | |
223 | # precalc mask permutations |
224 | _label_modifier_masks = permute_mask(LABEL_MODIFIERS) |
225 | + _keyman_label_modifier_masks = permute_mask(KEYMAN_LABEL_MODIFIERS) |
226 | |
227 | def __init__(self): |
228 | self._vk = None |
229 | + self._keyman_labels = None |
230 | self._svg_cache = {} |
231 | self._format = None # format of the currently loading layout |
232 | self._layout_filename = "" |
233 | @@ -97,14 +99,14 @@ |
234 | self._layout_regex = re.compile("([^\(]+) (?: \( ([^\)]*) \) )?", |
235 | re.VERBOSE) |
236 | |
237 | - def load(self, vk, layout_filename, color_scheme): |
238 | + def load(self, vk, keyman_labels, layout_filename, color_scheme): |
239 | """ Load layout root file. """ |
240 | self._system_layout, self._system_variant = \ |
241 | self._get_system_keyboard_layout(vk) |
242 | _logger.info("current system keyboard layout(variant): '{}'" |
243 | .format(self._get_system_layout_string())) |
244 | |
245 | - layout = self._load(vk, layout_filename, color_scheme, |
246 | + layout = self._load(vk, keyman_labels, layout_filename, color_scheme, |
247 | os.path.dirname(layout_filename)) |
248 | if layout: |
249 | # purge attributes only used during loading |
250 | @@ -122,10 +124,10 @@ |
251 | |
252 | return layout |
253 | |
254 | - def _load(self, vk, layout_filename, color_scheme, |
255 | - root_layout_dir, parent_item=None): |
256 | + def _load(self, vk, keyman_labels, layout_filename, color_scheme, root_layout_dir, parent_item = None): |
257 | """ Load or include layout file at any depth level. """ |
258 | self._vk = vk |
259 | + self._keyman_labels = keyman_labels |
260 | self._layout_filename = layout_filename |
261 | self._color_scheme = color_scheme |
262 | self._root_layout_dir = root_layout_dir |
263 | @@ -254,6 +256,7 @@ |
264 | filepath = config.find_layout_filename(filename, "layout include") |
265 | _logger.info("Including layout '{}'".format(filename)) |
266 | incl_root = LayoutLoaderSVG()._load(self._vk, |
267 | + self._keyman_labels, |
268 | filepath, |
269 | self._color_scheme, |
270 | self._root_layout_dir, |
271 | @@ -632,7 +635,13 @@ |
272 | # Get labels from keyboard mapping first. |
273 | if key.type == KeyCommon.KEYCODE_TYPE and \ |
274 | key.id not in ["BKSP"]: |
275 | - if self._vk: # xkb keyboard found? |
276 | + if self._keyman_labels: # using Keyman keyboard |
277 | + # load the labels from self._keyman_labels |
278 | + vkmodmasks = self._label_modifier_masks |
279 | + if sys.version_info.major == 2: |
280 | + vkmodmasks = [long(m) for m in vkmodmasks] |
281 | + labels = self._keyman_labels.labels_from_id(key.id) |
282 | + elif self._vk: # xkb keyboard found? |
283 | vkmodmasks = self._label_modifier_masks |
284 | if sys.version_info.major == 2: |
285 | vkmodmasks = [int(m) for m in vkmodmasks] |
286 | @@ -666,7 +675,7 @@ |
287 | # override with per-keysym labels |
288 | keysym_rules = self._get_keysym_rules(key) |
289 | if key.type == KeyCommon.KEYCODE_TYPE: |
290 | - if self._vk: # xkb keyboard found? |
291 | + if not self._keyman_labels and self._vk: # xkb keyboard found but no keyman one? |
292 | vkmodmasks = self._label_modifier_masks |
293 | try: |
294 | if sys.version_info.major == 2: |
295 | |
296 | === modified file 'Onboard/OnboardGtk.py' |
297 | --- Onboard/OnboardGtk.py 2017-02-14 21:42:14 +0000 |
298 | +++ Onboard/OnboardGtk.py 2022-01-27 19:28:30 +0000 |
299 | @@ -30,6 +30,7 @@ |
300 | import time |
301 | import signal |
302 | import os.path |
303 | +from lxml import etree |
304 | |
305 | from Onboard.Version import require_gi_versions |
306 | require_gi_versions() |
307 | @@ -47,12 +48,13 @@ |
308 | from Onboard.KbdWindow import KbdWindow, KbdPlugWindow |
309 | from Onboard.Keyboard import Keyboard |
310 | from Onboard.KeyboardWidget import KeyboardWidget |
311 | +from Onboard.Keyman import KeymanDBus |
312 | from Onboard.Indicator import Indicator |
313 | from Onboard.LayoutLoaderSVG import LayoutLoaderSVG |
314 | from Onboard.Appearance import ColorScheme |
315 | from Onboard.IconPalette import IconPalette |
316 | from Onboard.Exceptions import LayoutFileError |
317 | -from Onboard.utils import unicode_str |
318 | +from Onboard.utils import unicode_str, Modifiers |
319 | from Onboard.Timer import CallOnce, Timer |
320 | from Onboard.WindowUtils import show_confirmation_dialog |
321 | import Onboard.osk as osk |
322 | @@ -74,6 +76,8 @@ |
323 | """ |
324 | |
325 | keyboard = None |
326 | + keymandbus = None |
327 | + _keyman_labels = None |
328 | |
329 | def __init__(self): |
330 | |
331 | @@ -99,12 +103,14 @@ |
332 | except dbus.exceptions.DBusException: |
333 | err_msg = "D-Bus session bus unavailable" |
334 | bus = None |
335 | + self.keymandbus = KeymanDBus() |
336 | |
337 | if not bus: |
338 | _logger.warning(err_msg + " " + |
339 | "Onboard will start with reduced functionality. " |
340 | "Single-instance check, " |
341 | - "D-Bus service and " |
342 | + "Keyman support, " |
343 | + "D-Bus service and " |
344 | "hover click are disabled.") |
345 | |
346 | # Yield to GNOME Shell's keyboard before any other D-Bus activity |
347 | @@ -282,6 +288,9 @@ |
348 | self.do_connect(self.keymap, "state-changed", self.cb_state_changed) |
349 | # group changes |
350 | Gdk.event_handler_set(cb_any_event, self) |
351 | + # Keyman keyboard changes |
352 | + if self.keymandbus: |
353 | + self.keymandbus.name_notify_add(self.cb_keyman_changed) |
354 | |
355 | # connect config notifications here to keep config from holding |
356 | # references to keyboard objects. |
357 | @@ -553,6 +562,12 @@ |
358 | """ keyboard map change """ |
359 | self.reload_layout_delayed() |
360 | |
361 | + def cb_keyman_changed(self, name): |
362 | + """ keyman keyboard change """ |
363 | + _logger.debug("keyman changed to {}".format(name)) |
364 | + self._keyman_labels = self.keymandbus.key_labels |
365 | + self.reload_layout_delayed() |
366 | + |
367 | def cb_state_changed(self, keymap): |
368 | """ keyboard modifier state change """ |
369 | mod_mask = keymap.get_modifier_state() |
370 | @@ -672,7 +687,7 @@ |
371 | self.reload_layout(force_update = True) |
372 | self.keyboard_widget.update_transparency() |
373 | |
374 | - def reload_layout_delayed(self): |
375 | + def reload_layout_delayed(self, force_update=False): |
376 | """ |
377 | Delay reloading the layout on keyboard map or group changes |
378 | This is mainly for LP #1313176 when Caps-lock is set up as |
379 | @@ -689,14 +704,19 @@ |
380 | Checks if the X keyboard layout has changed and |
381 | (re)loads Onboards layout accordingly. |
382 | """ |
383 | - keyboard_state = (None, None) |
384 | + keyboard_state = (None, None, None) |
385 | |
386 | vk = self.get_vk() |
387 | if vk: |
388 | try: |
389 | vk.reload() # reload keyboard names |
390 | + group = vk.get_current_group_name() |
391 | + if self.keymandbus: |
392 | + self._keyman_labels = self.keymandbus.key_labels |
393 | + |
394 | keyboard_state = (vk.get_layout_as_string(), |
395 | - vk.get_current_group_name()) |
396 | + vk.get_current_group_name(), |
397 | + self.keymandbus.name if self.keymandbus else None) |
398 | except osk.error: |
399 | self.reset_vk() |
400 | force_update = True |
401 | @@ -704,6 +724,13 @@ |
402 | "keyboard information failed") |
403 | |
404 | if self.keyboard_state != keyboard_state or force_update: |
405 | + if self.keymandbus: |
406 | + _logger.debug("Reloading layout. Layout: {}; Group: {}; Keyman: {}" |
407 | + .format(vk.get_layout_as_string(), vk.get_current_group_name(), self.keymandbus.name)) |
408 | + else: |
409 | + _logger.debug("Reloading layout. Layout: {}; Group: {}" |
410 | + .format(vk.get_layout_as_string(), vk.get_current_group_name())) |
411 | + |
412 | self.keyboard_state = keyboard_state |
413 | |
414 | layout_filename = config.layout_filename |
415 | @@ -731,7 +758,7 @@ |
416 | |
417 | color_scheme = ColorScheme.load(color_scheme_filename) \ |
418 | if color_scheme_filename else None |
419 | - layout = LayoutLoaderSVG().load(vk, layout_filename, color_scheme) |
420 | + layout = LayoutLoaderSVG().load(vk, self._keyman_labels, layout_filename, color_scheme) |
421 | |
422 | self.keyboard.set_layout(layout, color_scheme, vk) |
423 | |
424 | |
425 | === modified file 'Onboard/utils.py' |
426 | --- Onboard/utils.py 2017-02-19 20:06:09 +0000 |
427 | +++ Onboard/utils.py 2022-01-27 19:28:30 +0000 |
428 | @@ -58,6 +58,15 @@ |
429 | Modifiers.NUMLK | \ |
430 | Modifiers.ALTGR |
431 | |
432 | +# Keyman uses more modifiers inc MOD3(RCTRL) |
433 | +KEYMAN_LABEL_MODIFIERS = Modifiers.SHIFT | \ |
434 | + Modifiers.CAPS | \ |
435 | + Modifiers.CTRL | \ |
436 | + Modifiers.ALT | \ |
437 | + Modifiers.NUMLK | \ |
438 | + Modifiers.MOD3 | \ |
439 | + Modifiers.ALTGR |
440 | + |
441 | modifiers = {"shift":1, |
442 | "caps":2, |
443 | "control":4, |
444 | |
445 | === modified file 'setup.py' |
446 | --- setup.py 2017-05-14 14:36:31 +0000 |
447 | +++ setup.py 2022-01-27 19:28:30 +0000 |
448 | @@ -231,7 +231,7 @@ |
449 | "-Wsign-compare", |
450 | "-Wdeclaration-after-statement", |
451 | "-Werror=declaration-after-statement", |
452 | - "-Wlogical-op"], |
453 | + "-fPIC" ], |
454 | |
455 | **pkgconfig('gdk-3.0', 'x11', 'xi', 'xtst', 'xkbfile', |
456 | 'dconf', 'libcanberra', 'hunspell', |