Hide keyboard shortcuts

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. 

7 

8import os 

9 

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 

14 

15log = Logger("keyboard") 

16 

17LAYOUT_GROUPS = envbool("XPRA_LAYOUT_GROUPS", True) 

18DEBUG_KEY_EVENTS = tuple(x.lower() for x in os.environ.get("XPRA_DEBUG_KEY_EVENTS", "").split(",")) 

19 

20 

21class KeyboardHelper: 

22 

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 

52 

53 def mask_to_names(self, mask): 

54 return self.keyboard.mask_to_names(mask) 

55 

56 def set_modifier_mappings(self, mappings): 

57 self.keyboard.set_modifier_mappings(mappings) 

58 

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 

75 

76 self.hash = None 

77 

78 self.key_repeat_delay = -1 

79 self.key_repeat_interval = -1 

80 self.keyboard_sync = False 

81 self.key_shortcuts = {} 

82 

83 def cleanup(self): 

84 self.reset_state() 

85 def nosend(*_args): 

86 pass 

87 self.send = nosend 

88 

89 def keymap_changed(self, *args): 

90 pass 

91 

92 

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 

99 

100 def get_modifier_names(self): 

101 return get_modifier_names(self.xkbmap_mod_meanings) 

102 

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 

124 

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 

172 

173 

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 

183 

184 

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) 

209 

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) 

217 

218 

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 

240 

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 

261 

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) 

293 

294 def update(self): 

295 if not self.locked: 

296 self.query_xkbmap() 

297 self.parse_shortcuts() 

298 

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)]) 

302 

303 

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 "") 

311 

312 def send_keymap(self): 

313 log("send_keymap()") 

314 self.send("keymap-changed", self.get_keymap_properties()) 

315 

316 

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)]) 

331 

332 def get_full_keymap(self): 

333 return [] 

334 

335 

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 

349 

350 

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()))