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