1# Orca 2# 3# Copyright 2005-2008 Sun Microsystems Inc. 4# 5# This library is free software; you can redistribute it and/or 6# modify it under the terms of the GNU Lesser General Public 7# License as published by the Free Software Foundation; either 8# version 2.1 of the License, or (at your option) any later version. 9# 10# This library is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13# Lesser General Public License for more details. 14# 15# You should have received a copy of the GNU Lesser General Public 16# License along with this library; if not, write to the 17# Free Software Foundation, Inc., Franklin Street, Fifth Floor, 18# Boston MA 02110-1301 USA. 19 20"""Provides support for defining keybindings and matching them to input 21events.""" 22 23__id__ = "$Id$" 24__version__ = "$Revision$" 25__date__ = "$Date$" 26__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." 27__license__ = "LGPL" 28 29from gi.repository import Gdk 30 31import gi 32gi.require_version('Atspi', '2.0') 33from gi.repository import Atspi 34 35import functools 36import pyatspi 37 38from . import debug 39from . import settings 40from . import orca_state 41 42from .orca_i18n import _ 43 44_keysymsCache = {} 45_keycodeCache = {} 46 47MODIFIER_ORCA = 8 48NO_MODIFIER_MASK = 0 49ALT_MODIFIER_MASK = 1 << pyatspi.MODIFIER_ALT 50CTRL_MODIFIER_MASK = 1 << pyatspi.MODIFIER_CONTROL 51ORCA_MODIFIER_MASK = 1 << MODIFIER_ORCA 52ORCA_ALT_MODIFIER_MASK = (1 << MODIFIER_ORCA | 53 1 << pyatspi.MODIFIER_ALT) 54ORCA_CTRL_MODIFIER_MASK = (1 << MODIFIER_ORCA | 55 1 << pyatspi.MODIFIER_CONTROL) 56ORCA_CTRL_ALT_MODIFIER_MASK = (1 << MODIFIER_ORCA | 57 1 << pyatspi.MODIFIER_CONTROL | 58 1 << pyatspi.MODIFIER_ALT) 59ORCA_SHIFT_MODIFIER_MASK = (1 << MODIFIER_ORCA | 60 1 << pyatspi.MODIFIER_SHIFT) 61SHIFT_MODIFIER_MASK = 1 << pyatspi.MODIFIER_SHIFT 62SHIFT_ALT_MODIFIER_MASK = (1 << pyatspi.MODIFIER_SHIFT | 63 1 << pyatspi.MODIFIER_ALT) 64CTRL_ALT_MODIFIER_MASK = (1 << pyatspi.MODIFIER_CONTROL | 65 1 << pyatspi.MODIFIER_ALT) 66COMMAND_MODIFIER_MASK = (1 << pyatspi.MODIFIER_ALT | 67 1 << pyatspi.MODIFIER_CONTROL | 68 1 << pyatspi.MODIFIER_META2 | 69 1 << pyatspi.MODIFIER_META3) 70NON_LOCKING_MODIFIER_MASK = (1 << pyatspi.MODIFIER_SHIFT | 71 1 << pyatspi.MODIFIER_ALT | 72 1 << pyatspi.MODIFIER_CONTROL | 73 1 << pyatspi.MODIFIER_META2 | 74 1 << pyatspi.MODIFIER_META3 | 75 1 << MODIFIER_ORCA) 76defaultModifierMask = NON_LOCKING_MODIFIER_MASK 77 78def getKeycode(keysym): 79 """Converts an XKeysym string (e.g., 'KP_Enter') to a keycode that 80 should match the event.hw_code for key events. 81 82 This whole situation is caused by the fact that Solaris chooses 83 to give us different keycodes for the same key, and the keypad 84 is the primary place where this happens: if NumLock is not on, 85 there is no telling the difference between keypad keys and the 86 other navigation keys (e.g., arrows, page up/down, etc.). One, 87 for example, would expect to get KP_End for the '1' key on the 88 keypad if NumLock were not on. Instead, we get 'End' and the 89 keycode for it matches the keycode for the other 'End' key. Odd. 90 If NumLock is on, we at least get KP_* keys. 91 92 So...when setting up keybindings, we say we're interested in 93 KeySyms, but those keysyms are carefully chosen so as to result 94 in a keycode that matches the actual key on the keyboard. This 95 is why we use KP_1 instead of KP_End and so on in our keybindings. 96 97 Arguments: 98 - keysym: a string that is a valid representation of an XKeysym. 99 100 Returns an integer representing a key code that should match the 101 event.hw_code for key events. 102 """ 103 104 if not keysym: 105 return 0 106 107 if keysym not in _keycodeCache: 108 keymap = Gdk.Keymap.get_default() 109 110 # Find the numerical value of the keysym 111 # 112 keyval = Gdk.keyval_from_name(keysym) 113 if keyval == 0: 114 return 0 115 116 # Now find the keycodes for the keysym. Since a keysym can 117 # be associated with more than one key, we'll shoot for the 118 # keysym that's in group 0, regardless of shift level (each 119 # entry is of the form [keycode, group, level]). 120 # 121 _keycodeCache[keysym] = 0 122 success, entries = keymap.get_entries_for_keyval(keyval) 123 124 for entry in entries: 125 if entry.group == 0: 126 _keycodeCache[keysym] = entry.keycode 127 break 128 if _keycodeCache[keysym] == 0: 129 _keycodeCache[keysym] = entries[0].keycode 130 131 #print keysym, keyval, entries, _keycodeCache[keysym] 132 133 return _keycodeCache[keysym] 134 135def getModifierNames(mods): 136 """Gets the modifier names of a numeric modifier mask as a human 137 consumable string. 138 """ 139 140 text = "" 141 if mods & ORCA_MODIFIER_MASK: 142 if settings.keyboardLayout == settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP: 143 # Translators: this is presented in a GUI to represent the 144 # "insert" key when used as the Orca modifier. 145 text += _("Insert") + "+" 146 else: 147 # Translators: this is presented in a GUI to represent the 148 # "caps lock" modifier. 149 text += _("Caps_Lock") + "+" 150 elif mods & (1 << pyatspi.MODIFIER_SHIFTLOCK): 151 # Translators: this is presented in a GUI to represent the 152 # "caps lock" modifier. 153 # 154 text += _("Caps_Lock") + "+" 155 #if mods & (1 << pyatspi.MODIFIER_NUMLOCK): 156 # text += _("Num_Lock") + "+" 157 if mods & 128: 158 # Translators: this is presented in a GUI to represent the 159 # "right alt" modifier. 160 # 161 text += _("Alt_R") + "+" 162 if mods & (1 << pyatspi.MODIFIER_META3): 163 # Translators: this is presented in a GUI to represent the 164 # "super" modifier. 165 # 166 text += _("Super") + "+" 167 if mods & (1 << pyatspi.MODIFIER_META2): 168 # Translators: this is presented in a GUI to represent the 169 # "meta 2" modifier. 170 # 171 text += _("Meta2") + "+" 172 #if mods & (1 << pyatspi.MODIFIER_META): 173 # text += _("Meta") + "+" 174 if mods & ALT_MODIFIER_MASK: 175 # Translators: this is presented in a GUI to represent the 176 # "left alt" modifier. 177 # 178 text += _("Alt_L") + "+" 179 if mods & CTRL_MODIFIER_MASK: 180 # Translators: this is presented in a GUI to represent the 181 # "control" modifier. 182 # 183 text += _("Ctrl") + "+" 184 if mods & SHIFT_MODIFIER_MASK: 185 # Translators: this is presented in a GUI to represent the 186 # "shift " modifier. 187 # 188 text += _("Shift") + "+" 189 return text 190 191def getClickCountString(count): 192 """Returns a human-consumable string representing the number of 193 clicks, such as 'double click' and 'triple click'.""" 194 195 if count == 2: 196 # Translators: Orca keybindings support double 197 # and triple "clicks" or key presses, similar to 198 # using a mouse. 199 # 200 return _("double click") 201 if count == 3: 202 # Translators: Orca keybindings support double 203 # and triple "clicks" or key presses, similar to 204 # using a mouse. 205 # 206 return _("triple click") 207 return "" 208 209class KeyBinding: 210 """A single key binding, consisting of a keycode, a modifier mask, 211 and the InputEventHandler. 212 """ 213 214 def __init__(self, keysymstring, modifier_mask, modifiers, handler, 215 click_count = 1): 216 """Creates a new key binding. 217 218 Arguments: 219 - keysymstring: the keysymstring - this is typically a string 220 from /usr/include/X11/keysymdef.h with the preceding 'XK_' 221 removed (e.g., XK_KP_Enter becomes the string 'KP_Enter'). 222 - modifier_mask: bit mask where a set bit tells us what modifiers 223 we care about (see pyatspi.MODIFIER_*) 224 - modifiers: the state the modifiers we care about must be in for 225 this key binding to match an input event (see also 226 pyatspi.MODIFIER_*) 227 - handler: the InputEventHandler for this key binding 228 """ 229 230 self.keysymstring = keysymstring 231 self.modifier_mask = modifier_mask 232 self.modifiers = modifiers 233 self.handler = handler 234 self.click_count = click_count 235 self.keycode = None 236 237 def matches(self, keycode, modifiers): 238 """Returns true if this key binding matches the given keycode and 239 modifier state. 240 """ 241 242 # We lazily bind the keycode. The primary reason for doing this 243 # is so that atspi does not have to be initialized before setting 244 # keybindings in the user's preferences file. 245 # 246 if not self.keycode: 247 self.keycode = getKeycode(self.keysymstring) 248 249 if self.keycode == keycode: 250 result = modifiers & self.modifier_mask 251 return result == self.modifiers 252 else: 253 return False 254 255 def description(self): 256 """Returns the description of this binding's functionality.""" 257 258 try: 259 return self.handler.description 260 except: 261 return '' 262 263 def asString(self): 264 """Returns a more human-consumable string representing this binding.""" 265 266 mods = getModifierNames(self.modifiers) 267 clickCount = getClickCountString(self.click_count) 268 keysym = self.keysymstring 269 string = '%s%s %s' % (mods, keysym, clickCount) 270 271 return string.strip() 272 273 def keyDefs(self): 274 """ return a list of Atspi key definitions for the given binding. 275 This may return more than one binding if the Orca modifier is bound 276 to more than one key. 277 If AT-SPI is older than 2.40, then this function will not work and 278 will return an empty set. 279 """ 280 ret = [] 281 if not self.keycode: 282 self.keycode = getKeycode(self.keysymstring) 283 284 if self.modifiers & ORCA_MODIFIER_MASK: 285 device = orca_state.device 286 if device is None: 287 return ret 288 modList = [] 289 otherMods = self.modifiers & ~ORCA_MODIFIER_MASK 290 numLockMod = device.get_modifier(getKeycode("Num_Lock")) 291 lockedMods = device.get_locked_modifiers() 292 numLockOn = lockedMods & numLockMod 293 for key in settings.orcaModifierKeys: 294 keycode = getKeycode(key) 295 if keycode == 0 and key == "Shift_Lock": 296 keycode = getKeycode("Caps_Lock") 297 mod = device.map_modifier(keycode) 298 if key != "KP_Insert" or not numLockOn: 299 modList.append(mod | otherMods) 300 else: 301 modList = [self.modifiers] 302 for mod in modList: 303 kd = Atspi.KeyDefinition() 304 kd.keycode = self.keycode 305 kd.modifiers = mod 306 ret.append(kd) 307 return ret 308 309class KeyBindings: 310 """Structure that maintains a set of KeyBinding instances. 311 """ 312 313 def __init__(self): 314 self.keyBindings = [] 315 316 def __str__(self): 317 result = "[\n" 318 for keyBinding in self.keyBindings: 319 result += " [%x %x %s %d %s]\n" % \ 320 (keyBinding.modifier_mask, 321 keyBinding.modifiers, 322 keyBinding.keysymstring, 323 keyBinding.click_count, 324 keyBinding.handler.description) 325 result += "]" 326 return result 327 328 def add(self, keyBinding): 329 """Adds the given KeyBinding instance to this set of keybindings. 330 """ 331 332 self.keyBindings.append(keyBinding) 333 334 def remove(self, keyBinding): 335 """Removes the given KeyBinding instance from this set of keybindings. 336 """ 337 338 try: 339 i = self.keyBindings.index(keyBinding) 340 except: 341 pass 342 else: 343 del self.keyBindings[i] 344 345 def removeByHandler(self, handler): 346 """Removes the given KeyBinding instance from this set of keybindings. 347 """ 348 i = len(self.keyBindings) 349 while i > 0: 350 if self.keyBindings[i - 1].handler == handler: 351 del self.keyBindings[i - 1] 352 i = i - 1 353 354 def hasKeyBinding (self, newKeyBinding, typeOfSearch="strict"): 355 """Return True if keyBinding is already in self.keyBindings. 356 357 The typeOfSearch can be: 358 "strict": matches description, modifiers, key, and 359 click count 360 "description": matches only description. 361 "keys": matches the modifiers, key, and modifier mask, 362 and click count 363 "keysNoMask": matches the modifiers, key, and click count 364 """ 365 366 hasIt = False 367 368 for keyBinding in self.keyBindings: 369 if typeOfSearch == "strict": 370 if (keyBinding.handler.description \ 371 == newKeyBinding.handler.description) \ 372 and (keyBinding.keysymstring \ 373 == newKeyBinding.keysymstring) \ 374 and (keyBinding.modifier_mask \ 375 == newKeyBinding.modifier_mask) \ 376 and (keyBinding.modifiers \ 377 == newKeyBinding.modifiers) \ 378 and (keyBinding.click_count \ 379 == newKeyBinding.click_count): 380 hasIt = True 381 elif typeOfSearch == "description": 382 if keyBinding.handler.description \ 383 == newKeyBinding.handler.description: 384 hasIt = True 385 elif typeOfSearch == "keys": 386 if (keyBinding.keysymstring \ 387 == newKeyBinding.keysymstring) \ 388 and (keyBinding.modifier_mask \ 389 == newKeyBinding.modifier_mask) \ 390 and (keyBinding.modifiers \ 391 == newKeyBinding.modifiers) \ 392 and (keyBinding.click_count \ 393 == newKeyBinding.click_count): 394 hasIt = True 395 elif typeOfSearch == "keysNoMask": 396 if (keyBinding.keysymstring \ 397 == newKeyBinding.keysymstring) \ 398 and (keyBinding.modifiers \ 399 == newKeyBinding.modifiers) \ 400 and (keyBinding.click_count \ 401 == newKeyBinding.click_count): 402 hasIt = True 403 404 return hasIt 405 406 def getBoundBindings(self, uniqueOnly=False): 407 """Returns the KeyBinding instances which are bound to a keystroke. 408 409 Arguments: 410 - uniqueOnly: Should alternative bindings for the same handler be 411 filtered out (default: False) 412 """ 413 414 bound = [kb for kb in self.keyBindings if kb.keysymstring] 415 if uniqueOnly: 416 handlers = [kb.handler.description for kb in bound] 417 bound = [bound[i] for i in map(handlers.index, set(handlers))] 418 419 return bound 420 421 def getBindingsForHandler(self, handler): 422 """Returns the KeyBinding instances associated with handler.""" 423 424 return [kb for kb in self.keyBindings if kb.handler == handler] 425 426 def getInputHandler(self, keyboardEvent): 427 """Returns the input handler of the key binding that matches the 428 given keycode and modifiers, or None if no match exists. 429 """ 430 431 candidates = [] 432 clickCount = keyboardEvent.getClickCount() 433 for keyBinding in self.keyBindings: 434 if keyBinding.matches(keyboardEvent.hw_code, 435 keyboardEvent.modifiers): 436 if keyBinding.modifier_mask == keyboardEvent.modifiers and \ 437 keyBinding.click_count == clickCount: 438 return keyBinding.handler 439 # If there's no keysymstring, it's unbound and cannot be 440 # a match. 441 # 442 if keyBinding.keysymstring: 443 candidates.append(keyBinding) 444 445 if keyboardEvent.isKeyPadKeyWithNumlockOn(): 446 return None 447 448 # If we're still here, we don't have an exact match. Prefer 449 # the one whose click count is closest to, but does not exceed, 450 # the actual click count. 451 # 452 comparison = lambda x, y: y.click_count - x.click_count 453 candidates.sort(key=functools.cmp_to_key(comparison)) 454 for candidate in candidates: 455 if candidate.click_count <= clickCount: 456 return candidate.handler 457 458 return None 459 460 def load(self, keymap, handlers): 461 """ Takes the keymappings and tries to find a matching named 462 function in handlers. 463 keymap is a list of lists, each list contains 5 elements 464 If addUnbound is set to true, then at the end of loading all the 465 keybindings, any remaining functions will be unbound. 466 """ 467 468 469 for i in keymap: 470 keysymstring = i[0] 471 modifierMask = i[1] 472 modifiers = i[2] 473 handler = i[3] 474 try: 475 clickCount = i[4] 476 except: 477 clickCount = 1 478 479 if handler in handlers: 480 # add the keybinding 481 self.add(KeyBinding( \ 482 keysymstring, modifierMask, modifiers, \ 483 handlers[handler], clickCount)) 484 else: 485 debug.println(debug.LEVEL_WARNING, \ 486 "WARNING: could not find %s handler to associate " \ 487 "with keybinding." % handler) 488