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