/
keyboard_helper.py
360 lines (319 loc) · 14.4 KB
/
keyboard_helper.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# This file is part of Xpra.
# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>)
# Copyright (C) 2010-2022 Antoine Martin <antoine@xpra.org>
# Copyright (C) 2008, 2010 Nathaniel Smith <njs@pobox.com>
# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
# later version. See the file COPYING for details.
import os
from xpra.client.keyboard_shortcuts_parser import parse_shortcut_modifiers, parse_shortcuts, get_modifier_names
from xpra.util import csv, std, envbool, ellipsizer
from xpra.os_util import bytestostr
from xpra.log import Logger
log = Logger("keyboard")
LAYOUT_GROUPS = envbool("XPRA_LAYOUT_GROUPS", True)
DEBUG_KEY_EVENTS = tuple(x.lower() for x in os.environ.get("XPRA_DEBUG_KEY_EVENTS", "").split(","))
class KeyboardHelper:
def __init__(self, net_send, keyboard_sync=True,
shortcut_modifiers="auto", key_shortcuts=(),
raw=False, layout="", layouts=(),
variant="", variants=(), options=""):
self.reset_state()
self.send = net_send
self.locked = False
self.keyboard_sync = keyboard_sync
self.shortcuts_enabled = True
self.shortcut_modifiers_str = shortcut_modifiers
self.shortcut_modifiers = ()
self.key_shortcuts_strs = key_shortcuts
self.key_shortcuts = {}
#command line overrides:
self.raw = raw
self.layout_option = layout if (layout or "").lower() not in ("client", "auto") else ""
self.variant_option = variant
self.layouts_option = layouts
self.variants_option = variants
self.options = options
#the platform class which allows us to map the keys:
from xpra.platform.keyboard import Keyboard
self.keyboard = Keyboard() #pylint: disable=not-callable
log("KeyboardHelper(%s) keyboard=%s",
(net_send, keyboard_sync, key_shortcuts,
raw, layout, layouts, variant, variants, options), self.keyboard)
key_repeat = self.keyboard.get_keyboard_repeat()
if key_repeat:
self.key_repeat_delay, self.key_repeat_interval = key_repeat
def mask_to_names(self, mask):
return self.keyboard.mask_to_names(mask)
def set_modifier_mappings(self, mappings):
self.keyboard.set_modifier_mappings(mappings)
def reset_state(self):
self.keycodes = []
self.x11_keycodes = {}
self.mod_meanings = {}
self.mod_managed = []
self.mod_pointermissing = []
self.layout = ""
self.layouts = []
self.variant = ""
self.variants = []
self.options = ""
self.query = ""
self.query_struct = {}
self.layout_groups = LAYOUT_GROUPS
self.raw = False
self.hash = None
self.key_repeat_delay = -1
self.key_repeat_interval = -1
self.keyboard_sync = False
self.key_shortcuts = {}
def cleanup(self):
self.reset_state()
def nosend(*_args):
""" make sure we don't send keyboard updates during cleanup """
self.send = nosend
def keymap_changed(self, *args):
""" This method is overriden in tghe GTK Keyboard Helper """
def parse_shortcuts(self):
#parse shortcuts:
modifier_names = self.get_modifier_names()
self.shortcut_modifiers = parse_shortcut_modifiers(self.shortcut_modifiers_str, modifier_names)
self.key_shortcuts = parse_shortcuts(self.key_shortcuts_strs, self.shortcut_modifiers, modifier_names)
return self.key_shortcuts
def get_modifier_names(self):
return get_modifier_names(self.mod_meanings)
def key_handled_as_shortcut(self, window, key_name, modifiers, depressed):
#find the shortcuts that may match this key:
shortcuts = self.key_shortcuts.get(key_name)
log("key_handled_as_shortcut%s shortcuts_enabled=%s, shortcuts=%s",
(window, key_name, modifiers, depressed),
self.shortcuts_enabled, shortcuts)
if not self.shortcuts_enabled:
return False
if not shortcuts:
return False
if len(shortcuts)>1:
#sort shortcuts based on how many modifiers are required,
#so that if multiple shortcuts use the same key,
#we will try to match the one with the most modifiers first.
#ie: Num_Lock+Menu will be tested before Menu
#(this is needed because Num_Lock is then discarded when comparing the list of required modifiers!)
shortcuts = sorted(shortcuts, key=lambda x : len(x[0]), reverse=True)
for shortcut in shortcuts:
if self._check_shortcut(window, key_name, modifiers, depressed, shortcut):
return True
return False
def _check_shortcut(self, window, key_name, modifiers, depressed, shortcut):
req_mods, action, args = shortcut
extra_modifiers = list(modifiers)
for rm in req_mods:
if rm not in modifiers:
#modifier is missing, bail out
log("not matched %s for %s: %s not in %s",
shortcut, key_name, rm, modifiers)
return False
try:
extra_modifiers.remove(rm)
except ValueError:
pass #same modifier listed twice?
kmod, _, ignored = self.keyboard.get_keymap_modifiers()
if not kmod and self.keyboard.modifier_keys:
#fallback to server supplied map:
kmod = self.keyboard.modifier_keys
#ie: {'ISO_Level3_Shift': 'mod5', 'Meta_L': 'mod1', ...}
log("keymap modifiers: %s", kmod)
ignoremod = ("Caps_Lock", "Num_Lock")
for x in ignoremod:
mod = kmod.get(x)
if mod in extra_modifiers:
extra_modifiers.remove(mod)
for mod in ignored:
if mod in extra_modifiers:
extra_modifiers.remove(mod)
if extra_modifiers:
log("skipping partial shortcut match %s, modifiers unmatched: %s", shortcut, extra_modifiers)
return False
log("matched shortcut %s", shortcut)
if not depressed:
#when the key is released, just ignore it - do NOT send it to the server!
return True
if action=="pass":
return False
try:
method = getattr(window, action)
log("key_handled_as_shortcut(%s,%s,%s,%s) found shortcut=%s, will call %s%s",
window, key_name, modifiers, depressed, shortcut, method, args)
except AttributeError as e:
log.error("key dropped, invalid method name in shortcut %s: %s", action, e)
return True
try:
method(*args)
log("key_handled_as_shortcut(%s,%s,%s,%s) has been handled: %s",
window, key_name, modifiers, depressed, method)
except Exception:
log.error("Error: key_handled_as_shortcut(%s,%s,%s,%s)", window, key_name, modifiers, depressed)
log.error(" failed to execute shortcut=%s", shortcut)
log.error("", exc_info=True)
return True
def handle_key_action(self, window, wid, key_event):
"""
Intercept key shortcuts and gives the Keyboard class
a chance to fire more than one send_key_action.
(win32 uses this for AltGr emulation)
"""
if not self.key_handled_as_shortcut(window, key_event.keyname, key_event.modifiers, key_event.pressed):
self.keyboard.process_key_event(self.send_key_action, wid, key_event)
return False
def debug_key_event(self, wid, key_event):
if not DEBUG_KEY_EVENTS:
return
def keyname(v):
if v.endswith("_L") or v.endswith("_R"):
return v[:-2].lower()
return v.lower()
def dbg(v):
return v and keyname(v) in DEBUG_KEY_EVENTS
debug = ("all" in DEBUG_KEY_EVENTS) or dbg(key_event.keyname) or dbg(key_event.string)
modifiers = key_event.modifiers
if not debug and modifiers:
#see if one of the modifier matches:
#either the raw name (ie: "mod2") or its actual meaning (ie: "NumLock")
for m in modifiers:
if m in DEBUG_KEY_EVENTS:
debug = True
break
name = keyname(self.keyboard.modifier_names.get(m) or "")
if name and name in DEBUG_KEY_EVENTS:
debug = True
break
if debug:
log.info("key event %s on window %i", key_event, wid)
def send_key_action(self, wid, key_event):
log("send_key_action(%s, %s)", wid, key_event)
packet = ["key-action", wid]
for x in ("keyname", "pressed", "modifiers", "keyval", "string", "keycode", "group"):
packet.append(getattr(key_event, x))
self.debug_key_event(wid, key_event)
self.send(*packet)
def get_layout_spec(self):
""" add / honour overrides """
layout, layouts, variant, variants, options = self.keyboard.get_layout_spec()
log("%s.get_layout_spec()=%s", self.keyboard, (layout, layouts, variant, variants, options))
def inl(v, l):
try:
if v in l or v is None:
return l
return [v]+list(l)
except Exception:
if v is not None:
return [v]
return []
layout = self.layout_option or layout
layouts = inl(layout, self.layouts_option or layouts)
variant = self.variant_option or variant
variants = inl(variant, self.variants_option or variants)
options = self.options or options
val = (layout, layouts, self.variant_option or variant, self.variants_option or variants, self.options)
log("get_layout_spec()=%s", val)
return val
def get_keymap_spec(self):
query_struct = self.keyboard.get_keymap_spec()
if query_struct:
if self.layout_option:
query_struct["layout"] = self.layout_option
if self.layouts_option:
query_struct["layouts"] = csv(self.layouts_option)
if self.variant_option:
query_struct["variant"] = self.variant_option
if self.variants_option:
query_struct["variants"] = csv(self.variants_option)
if self.options:
if self.options.lower()=="none":
query_struct["options"] = ""
else:
query_struct["options"] = self.options
return query_struct
def query_xkbmap(self):
log("query_xkbmap()")
self.layout, self.layouts, self.variant, self.variants, self.options = self.get_layout_spec()
self.query_struct = self.get_keymap_spec()
log(f"query_xkbmap() query_struct={self.query_struct}")
self.keycodes = self.get_full_keymap()
log(f"query_xkbmap() keycodes={self.keycodes}")
self.x11_keycodes = self.keyboard.get_x11_keymap()
log(f"query_xkbmap() {self.keyboard}.get_x11_keymap()={self.x11_keycodes}")
mods = self.keyboard.get_keymap_modifiers()
self.mod_meanings, self.mod_managed, self.mod_pointermissing = mods
log(f"query_xkbmap() get_keymap_modifiers()={mods}")
self.update_hash()
log(f"layout={self.layout}, layouts={self.layouts}, variant={self.variant}, variants={self.variants}")
log(f"query-struct={self.query_struct}")
log("keycodes=%s", ellipsizer(self.keycodes))
log("x11 keycodes=%s", ellipsizer(self.x11_keycodes))
log(f"mod managed: {self.mod_managed}")
log(f"mod meanings: {self.mod_meanings}")
log(f"mod pointermissing: {self.mod_pointermissing}")
log(f"hash={self.hash}")
def update(self):
if not self.locked:
self.query_xkbmap()
self.parse_shortcuts()
def layout_str(self):
return " / ".join([bytestostr(x) for x in (
self.layout_option or self.layout, self.variant_option or self.variant) if bool(x)])
def send_layout(self):
log("send_layout() layout_option=%s, layout=%s, variant_option=%s, variant=%s, options=%s",
self.layout_option, self.layout, self.variant_option, self.variant, self.options)
self.send("layout-changed",
self.layout_option or self.layout or "",
self.variant_option or self.variant or "",
self.options or "")
def send_keymap(self):
log("send_keymap()")
props = self.get_prefixed_keymap_properties()
#legacy format: flat with 'xkbmap_' prefix:
props["keymap"] = self.get_keymap_properties()
self.send("keymap-changed", props)
def update_hash(self):
import hashlib
h = hashlib.sha256()
def hashadd(v):
h.update(("/%s" % str(v)).encode("utf8"))
for x in (self.mod_meanings, self.mod_pointermissing, self.keycodes, self.x11_keycodes):
hashadd(x)
if self.query_struct:
#flatten the dict in a predicatable order:
for k in sorted(self.query_struct.keys()):
hashadd(self.query_struct.get(k))
self.hash = "/".join([str(x) for x in (self.layout, self.variant, h.hexdigest()) if bool(x)])
def get_full_keymap(self):
return []
def get_prefixed_keymap_properties(self):
key_props = self.get_keymap_properties()
#legacy format: flat with 'xkbmap_' prefix:
return dict((f"xkbmap_{k}", v) for k, v in key_props.items())
def get_keymap_properties(self):
props = {}
for x in (
"layout", "layouts", "variant", "variants",
"raw", "layout_groups",
"query_struct", "mod_meanings",
"mod_managed", "mod_pointermissing", "keycodes", "x11_keycodes",
):
v = getattr(self, x)
if v:
props[x] = v
return props
def log_keyboard_info(self):
#show the user a summary of what we have detected:
kb_info = {}
if self.query_struct:
for x in ("rules", "model", "layout"):
v = self.query_struct.get(x)
if v:
kb_info[x] = v
if self.layout:
kb_info["layout"] = self.layout
if not kb_info:
log.info(" using default keyboard settings")
else:
log.info(" keyboard settings: %s", csv("%s=%s" % (std(k), std(v)) for k,v in kb_info.items()))