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