1''' 2Defines the manager for global hot keys. 3 4@author: Eitan Isaacson 5@organization: IBM Corporation 6@copyright: Copyright (c) 2006, 2007 IBM Corporation 7@license: BSD 8 9All rights reserved. This program and the accompanying materials are made 10available under the terms of the BSD which accompanies this distribution, and 11is available at U{http://www.opensource.org/licenses/bsd-license.php} 12''' 13from gi.repository import Gtk as gtk 14from gi.repository import Gdk as gdk 15from gi.repository.Gio import Settings as GSettings 16 17from .i18n import _ 18import pyatspi 19 20HOTKEYS_GSCHEMA = 'org.a11y.Accerciser.hotkeys' 21HOTKEYS_BASEPATH = '/org/a11y/accerciser/hotkeys/' 22 23COL_COMPONENT = 0 24COL_DESC = 1 25COL_CALLBACK = 2 26COL_KEYPRESS = 3 27COL_MOD = 4 28COL_LOCALIZED_COMP = 5 29 30def _charToKeySym(key): 31 ''' 32 A convinience function to convert either a character, or key name to it's 33 respective keyval 34 35 @param key: The character or key name to convert. 36 @type key: string 37 38 @return: A key symbol 39 @rtype: long 40 ''' 41 try: 42 rv = gdk.unicode_to_keyval(ord(key)) 43 except: 44 rv = getattr(gdk, 'KEY_%s' % key) 45 return rv 46 47class HotkeyManager(gtk.ListStore): 48 ''' 49 A model that stores all of the global key bindings. All accerciser components 50 that need global hotkeys should register the key combination and callback 51 with the main instance of this class. 52 ''' 53 def __init__(self): 54 ''' 55 Constructor for the L{HotkeyManager} 56 ''' 57 gtk.ListStore.__init__(self, str, str, object, int, int, str) 58 self.connect('row-changed', self._onComboChanged) 59 60 masks = [mask for mask in pyatspi.allModifiers()] 61 pyatspi.Registry.registerKeystrokeListener( 62 self._accEventKeyPressed, mask=masks, kind=(pyatspi.KEY_PRESSED_EVENT,)) 63 64 65 def _accEventKeyPressed(self, event): 66 ''' 67 Handle certain key presses globally. Pass on to the hotkey manager the 68 key combinations pressed for further processing. 69 70 @param event: The event that is being handled. 71 @type event: L{pyatspi.event.Event} 72 ''' 73 handled = self.hotkeyPress(event.hw_code, event.modifiers) 74 event.consume = handled 75 76 77 def hotkeyPress(self, key, modifiers): 78 ''' 79 Call the appropriate callbacks for given key combination. This method 80 should be called by an at-spi keyboard:press event handler in the 81 main program. 82 83 @param key: The pressed key code. 84 @type key: integer 85 @param modifiers: The modifiers that were depressed during the keystroke. 86 @type modifiers: integer 87 ''' 88 km = gdk.Keymap.get_default() 89 90 callback = None 91 92 for combo in self: 93 success, entries = km.get_entries_for_keyval(combo[COL_KEYPRESS]) 94 if not success: continue 95 if key in [int(entry.keycode) for entry in entries] and \ 96 modifiers & combo[COL_MOD] == combo[COL_MOD]: 97 callback = combo[COL_CALLBACK] 98 if callback: 99 callback() 100 return bool(callback) 101 102 def addKeyCombo(self, component, localized_component, description, 103 callback, keypress, modifiers): 104 ''' 105 Adds the given key combination with the appropriate callbacks to 106 the L{HotkeyManager}. If an identical description with the identical 107 component already exists in the model, just reassign with the new callback. 108 109 I{Note:} It is important that the component and description strings be 110 unique. 111 112 @param component: The component name, usually the plugin name, or "Core". 113 @type component: string 114 @param description: A description of the action performed during the given 115 keycombo. 116 @type description: string 117 @param callback: The callback to call when the given key combination 118 is pressed. 119 @type callback: callable 120 @param keypress: The key symbol of the keystroke that performs given operation. 121 @type keypress: long 122 @param modifiers: The modifiers that must be depressed for function to 123 be perfomed. 124 @type modifiers: int 125 ''' 126 component_desc_pairs = list(zip([row[COL_COMPONENT] for row in self], 127 [row[COL_DESC] for row in self])) 128 if (component, description) in component_desc_pairs: 129 path = component_desc_pairs.index((component, description)) 130 self[path][COL_CALLBACK] = callback 131 else: 132 gspath = self._getComboGSettingsPath(component, description) 133 gsettings = GSettings.new_with_path(HOTKEYS_GSCHEMA, gspath) 134 if gsettings.get_string('hotkey-combo'): 135 final_keypress, final_modifiers = gtk.accelerator_parse( 136 gsettings.get_string('hotkey-combo')) 137 else: 138 final_keypress, final_modifiers = keypress, modifiers 139 self.append([component, description, callback, 140 int(final_keypress), final_modifiers, localized_component]) 141 142 def removeKeyCombo(self, component, description, callback, key, modifiers): 143 ''' 144 Removes the given callback from L{HotkeyManager}. It does not erase the 145 entire key combo entry. 146 147 @param component: The component name, usually the plugin name, or "Core". 148 @type component: string 149 @param description: A description of the action performed during the given 150 keycombo. 151 @type description: string 152 @param callback: The callback to call when the given key combination 153 is pressed. 154 @type callback: callable 155 @param key: The key symbol of the keystroke that performs given operation. 156 @type key: long 157 @param modifiers: The modifiers that must be depressed for function to 158 be perfomed. 159 @type modifiers: int 160 ''' 161 iter = self.get_iter_first() 162 while iter: 163 if self[iter][COL_CALLBACK] == callback: 164 # We never really remove it, just set the callback to None 165 self[iter][COL_CALLBACK] = '' 166 iter = self.iter_next(iter) 167 168 def _onComboChanged(self, model, path, iter): 169 ''' 170 Callback for row changes. Copies the changed key combos over to gsettings. 171 172 @param model: The model that emitted the signal. Should be this class instance. 173 @type model: L{gtk.TreeModel} 174 @param path: The path of the row that has changed. 175 @type path: tuple 176 @param iter: The iter of the row that has changed. 177 @type iter: L{gtk.TreeIter} 178 ''' 179 if not model[iter][COL_COMPONENT] or not model[iter][COL_DESC]: 180 return 181 182 gspath = self._getComboGSettingsPath(model[iter][COL_COMPONENT], 183 model[iter][COL_DESC]) 184 gsettings = GSettings.new_with_path(HOTKEYS_GSCHEMA, gspath) 185 combo_name = gtk.accelerator_name(model[iter][COL_KEYPRESS], 186 gdk.ModifierType(model[iter][COL_MOD])) 187 188 key = gsettings.get_string('hotkey-combo') 189 190 if key != combo_name and key != '/': 191 gsettings.set_string('hotkey-combo', combo_name) 192 193 194 def _getComboGSettingsPath(self, component, description): 195 ''' 196 Useful method that build and returns a gsettings path for a key combo. 197 198 @param component: The component of the hotkey. 199 @type component: string 200 @param description: The description of the hotkey action 201 @type description: string 202 203 @return: A full gsettings path 204 @rtype: string 205 ''' 206 dash_component = self.__dasherize(component) 207 dash_description = self.__dasherize(description) 208 209 path = '/'.join([dash_component, dash_description]) 210 211 return HOTKEYS_BASEPATH + path + '/' 212 213 214 def __dasherize(self, item): 215 ''' 216 This method dasherize and decapitalize a given string. 217 218 @param component: The given string 219 @type component: string 220 221 @return: A dasherized and decapitalized string 222 @rtype: string 223 ''' 224 return item.lower().replace(' ', '-') 225 226class HotkeyTreeView(gtk.TreeView): 227 ''' 228 A tree view of the variuos global hotkey combinations. The keys and 229 modifiers could also be changed through this widget. 230 ''' 231 def __init__(self, hotkey_manager): 232 ''' 233 Construct the tree view with the given L{HotkeyManager}. 234 235 @ivar hotkey_manager: The manager we wish to view. 236 @type hotkey_manager: L{HotkeyManager} 237 238 @param hotkey_manager: The manager we wish to view. 239 @type hotkey_manager: L{HotkeyManager} 240 ''' 241 gtk.TreeView.__init__(self) 242 self.hotkey_manager = hotkey_manager 243 modelfilter = self.hotkey_manager.filter_new(None) 244 modelfilter.set_visible_func(self._rowVisibleFunc, None) 245 self.set_model(modelfilter) 246 crt = gtk.CellRendererText() 247 tvc = gtk.TreeViewColumn(_('Component')) 248 tvc.pack_start(crt, True) 249 tvc.add_attribute(crt, 'text', COL_COMPONENT) 250 tvc.set_cell_data_func(crt, self._componentDataFunc, COL_COMPONENT) 251 self.append_column(tvc) 252 253 crt = gtk.CellRendererText() 254 tvc = gtk.TreeViewColumn(_('Task')) 255 tvc.pack_start(crt, True) 256 tvc.add_attribute(crt, 'text', COL_DESC) 257 tvc.set_cell_data_func(crt, self._translateDataFunc, COL_DESC) 258 self.append_column(tvc) 259 260 crt = gtk.CellRendererText() 261 tvc = gtk.TreeViewColumn(_('Key')) 262 tvc.set_min_width(64) 263 tvc.pack_start(crt, True) 264 crt.props.editable = True 265 tvc.add_attribute(crt, 'text', COL_KEYPRESS) 266 tvc.set_cell_data_func(crt, self._keyCellFunc) 267 crt.connect('edited', self._onKeyChanged) 268 self.append_column(tvc) 269 270 crt = gtk.CellRendererToggle() 271 tvc = gtk.TreeViewColumn(_('Alt')) 272 tvc.pack_start(crt, True) 273 tvc.set_cell_data_func(crt, self._modCellFunc, gdk.ModifierType.MOD1_MASK) 274 crt.connect('toggled', self._onModToggled, gdk.ModifierType.MOD1_MASK) 275 self.append_column(tvc) 276 277 crt = gtk.CellRendererToggle() 278 tvc = gtk.TreeViewColumn(_('Ctrl')) 279 tvc.pack_start(crt, True) 280 tvc.set_cell_data_func(crt, self._modCellFunc, \ 281 gdk.ModifierType.CONTROL_MASK) 282 crt.connect('toggled', self._onModToggled, gdk.ModifierType.CONTROL_MASK) 283 self.append_column(tvc) 284 285 crt = gtk.CellRendererToggle() 286 tvc = gtk.TreeViewColumn(_('Shift')) 287 tvc.pack_start(crt, True) 288 tvc.set_cell_data_func(crt, self._modCellFunc, gdk.ModifierType.SHIFT_MASK) 289 crt.connect('toggled', self._onModToggled, gdk.ModifierType.SHIFT_MASK) 290 self.append_column(tvc) 291 292 def _translateDataFunc(self, column, cell, model, iter, column_id): 293 ''' 294 Show the component name as a translated string. 295 296 @param column: The treeview column of the cell renderer. 297 @type column: L{gtk.TreeViewColumn} 298 @param cell: The cell rendere we need to modify. 299 @type cell: L{gtk.CellRendererText} 300 @param model: The treeview's model. 301 @type model: L{gtk.ListStore} 302 @param iter: The iter of the given cell data. 303 @type iter: L{gtk.TreeIter} 304 ''' 305 cell.set_property('text', _(model[iter][column_id])) 306 307 def _componentDataFunc(self, column, cell, model, iter, column_id): 308 ''' 309 Show the component name as a translated string. 310 311 @param column: The treeview column of the cell renderer. 312 @type column: L{gtk.TreeViewColumn} 313 @param cell: The cell rendere we need to modify. 314 @type cell: L{gtk.CellRendererText} 315 @param model: The treeview's model. 316 @type model: L{gtk.ListStore} 317 @param iter: The iter of the given cell data. 318 @type iter: L{gtk.TreeIter} 319 ''' 320 cell.set_property('text', model[iter][COL_LOCALIZED_COMP] or \ 321 model[iter][COL_COMPONENT]) 322 323 def _keyCellFunc(self, column, cell, model, iter, foo=None): 324 ''' 325 Show the key symbol as a string for easy readability. 326 327 @param column: The treeview column of the cell renderer. 328 @type column: L{gtk.TreeViewColumn} 329 @param column: The cell rendere we need to modify. 330 @type column: L{gtk.CellRendererText} 331 @param model: The treeview's model. 332 @type model: L{gtk.ListStore} 333 @param iter: The iter of the given cell data. 334 @type iter: L{gtk.TreeIter} 335 ''' 336 if model[iter][COL_KEYPRESS] > 0: 337 cell.set_property('text', 338 gdk.keyval_name(model[iter][COL_KEYPRESS])) 339 cell.set_property('sensitive', True) 340 else: 341 cell.set_property('text', '<select key>') 342 cell.set_property('sensitive', False) 343 344 def _modCellFunc(self, column, cell, model, iter, mask): 345 ''' 346 Show the given modifier mask as toggled or not. 347 348 @param column: The treeview column of the cell renderer. 349 @type column: L{gtk.TreeViewColumn} 350 @param column: The cell rendere we need to modify. 351 @type column: L{gtk.CellRendererText} 352 @param model: The treeview's model. 353 @type model: L{gtk.ListStore} 354 @param iter: The iter of the given cell data. 355 @type iter: L{gtk.TreeIter} 356 @param mask: A modifier mask. 357 @type mask: integer 358 ''' 359 cell.set_property('active', bool(mask & model[iter][COL_MOD])) 360 361 def _onKeyChanged(self, cellrenderertext, path, new_text): 362 ''' 363 A callback for the key cellrenderer when 'edited'. Model must be 364 changed accordingly. 365 366 @param cellrenderertext: The cell renderer that emitted the signal 367 @type cellrenderertext: L{gtk.CellRendererText} 368 @param path: Path of the edited cellrenderer. 369 @type path: tuple 370 @param new_text: The new text that was entered. 371 @type new_text: string 372 ''' 373 keysym = -1 374 if new_text: 375 try: 376 keysym = _charToKeySym(new_text) 377 except: 378 keysym = _charToKeySym(new_text[0]) 379 self.hotkey_manager[path][COL_KEYPRESS] = int(keysym) 380 381 def _onModToggled(self, renderer_toggle, path, mask): 382 ''' 383 A callback for the modifiers' cellrenderers when 'toggled'. 384 Model must be changed accordingly. 385 386 @param renderer_toggle: The cell renderer that emitted the signal 387 @type renderer_toggle: L{gtk.CellRendererToggle} 388 @param path: Path of the edited cellrenderer. 389 @type path: tuple 390 @param mask: Modifier mask that must be inverted. 391 @type new_text: integer 392 ''' 393 self.hotkey_manager[path][COL_MOD] ^= mask 394 395 def _rowVisibleFunc(self, model, iter, foo=None): 396 ''' 397 A filter function to hide the rows that do not contain valid callbacks. 398 This is usually the case when a plugin is disabled. 399 400 @param model: The view's model. 401 @type model: L{gtk.ListStore} 402 @param iter: The iter of the row in question. 403 @type iter: L{gtk.TreeIter} 404 405 @return: True if row should be displayed. 406 @rtype: boolean 407 ''' 408 return bool(model[iter][COL_CALLBACK]) 409