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
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',

Subscribers

People subscribed via source and target branches