You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
669 lines
22 KiB
669 lines
22 KiB
2 years ago
|
# coding=utf-8
|
||
|
# pynput
|
||
|
# Copyright (C) 2015-2022 Moses Palmér
|
||
|
#
|
||
|
# This program is free software: you can redistribute it and/or modify it under
|
||
|
# the terms of the GNU Lesser General Public License as published by the Free
|
||
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
||
|
# later version.
|
||
|
#
|
||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||
|
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
||
|
# details.
|
||
|
#
|
||
|
# You should have received a copy of the GNU Lesser General Public License
|
||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
"""
|
||
|
The keyboard implementation for *Xorg*.
|
||
|
"""
|
||
|
|
||
|
# pylint: disable=C0111
|
||
|
# The documentation is extracted from the base classes
|
||
|
|
||
|
# pylint: disable=R0903
|
||
|
# We implement stubs
|
||
|
|
||
|
# pylint: disable=W0611
|
||
|
try:
|
||
|
import pynput._util.xorg
|
||
|
except Exception as e:
|
||
|
raise ImportError('failed to acquire X connection: {}'.format(str(e)), e)
|
||
|
# pylint: enable=W0611
|
||
|
|
||
|
import enum
|
||
|
import threading
|
||
|
|
||
|
import Xlib.display
|
||
|
import Xlib.ext
|
||
|
import Xlib.ext.xtest
|
||
|
import Xlib.X
|
||
|
import Xlib.XK
|
||
|
import Xlib.protocol
|
||
|
import Xlib.keysymdef.xkb
|
||
|
|
||
|
from pynput._util import NotifierMixin
|
||
|
from pynput._util.xorg import (
|
||
|
alt_mask,
|
||
|
alt_gr_mask,
|
||
|
char_to_keysym,
|
||
|
display_manager,
|
||
|
index_to_shift,
|
||
|
keyboard_mapping,
|
||
|
ListenerMixin,
|
||
|
numlock_mask,
|
||
|
shift_to_index,
|
||
|
symbol_to_keysym)
|
||
|
from pynput._util.xorg_keysyms import (
|
||
|
CHARS,
|
||
|
DEAD_KEYS,
|
||
|
KEYPAD_KEYS,
|
||
|
KEYSYMS,
|
||
|
SYMBOLS)
|
||
|
from . import _base
|
||
|
|
||
|
|
||
|
class KeyCode(_base.KeyCode):
|
||
|
_PLATFORM_EXTENSIONS = (
|
||
|
# The symbol name for this key
|
||
|
'_symbol',
|
||
|
)
|
||
|
|
||
|
# Be explicit about fields
|
||
|
_symbol = None
|
||
|
|
||
|
@classmethod
|
||
|
def _from_symbol(cls, symbol, **kwargs):
|
||
|
"""Creates a key from a symbol.
|
||
|
|
||
|
:param str symbol: The symbol name.
|
||
|
|
||
|
:return: a key code
|
||
|
"""
|
||
|
# First try simple translation
|
||
|
keysym = Xlib.XK.string_to_keysym(symbol)
|
||
|
if keysym:
|
||
|
return cls.from_vk(keysym, _symbol=symbol, **kwargs)
|
||
|
|
||
|
# If that fails, try checking a module attribute of Xlib.keysymdef.xkb
|
||
|
if not keysym:
|
||
|
# pylint: disable=W0702; we want to ignore errors
|
||
|
try:
|
||
|
symbol = 'XK_' + symbol
|
||
|
return cls.from_vk(
|
||
|
getattr(Xlib.keysymdef.xkb, symbol, 0),
|
||
|
_symbol=symbol,
|
||
|
**kwargs)
|
||
|
except:
|
||
|
return cls.from_vk(
|
||
|
SYMBOLS.get(symbol, (0,))[0],
|
||
|
_symbol=symbol,
|
||
|
**kwargs)
|
||
|
# pylint: enable=W0702
|
||
|
|
||
|
@classmethod
|
||
|
def _from_media(cls, name, **kwargs):
|
||
|
"""Creates a media key from a partial name.
|
||
|
|
||
|
:param str name: The name. The actual symbol name will be this string
|
||
|
with ``'XF86_Audio'`` prepended.
|
||
|
|
||
|
:return: a key code
|
||
|
"""
|
||
|
return cls._from_symbol('XF86_Audio' + name, **kwargs)
|
||
|
|
||
|
|
||
|
# pylint: disable=W0212
|
||
|
class Key(enum.Enum):
|
||
|
# Default keys
|
||
|
alt = KeyCode._from_symbol('Alt_L')
|
||
|
alt_l = KeyCode._from_symbol('Alt_L')
|
||
|
alt_r = KeyCode._from_symbol('Alt_R')
|
||
|
alt_gr = KeyCode._from_symbol('Mode_switch')
|
||
|
backspace = KeyCode._from_symbol('BackSpace')
|
||
|
caps_lock = KeyCode._from_symbol('Caps_Lock')
|
||
|
cmd = KeyCode._from_symbol('Super_L')
|
||
|
cmd_l = KeyCode._from_symbol('Super_L')
|
||
|
cmd_r = KeyCode._from_symbol('Super_R')
|
||
|
ctrl = KeyCode._from_symbol('Control_L')
|
||
|
ctrl_l = KeyCode._from_symbol('Control_L')
|
||
|
ctrl_r = KeyCode._from_symbol('Control_R')
|
||
|
delete = KeyCode._from_symbol('Delete')
|
||
|
down = KeyCode._from_symbol('Down')
|
||
|
end = KeyCode._from_symbol('End')
|
||
|
enter = KeyCode._from_symbol('Return')
|
||
|
esc = KeyCode._from_symbol('Escape')
|
||
|
f1 = KeyCode._from_symbol('F1')
|
||
|
f2 = KeyCode._from_symbol('F2')
|
||
|
f3 = KeyCode._from_symbol('F3')
|
||
|
f4 = KeyCode._from_symbol('F4')
|
||
|
f5 = KeyCode._from_symbol('F5')
|
||
|
f6 = KeyCode._from_symbol('F6')
|
||
|
f7 = KeyCode._from_symbol('F7')
|
||
|
f8 = KeyCode._from_symbol('F8')
|
||
|
f9 = KeyCode._from_symbol('F9')
|
||
|
f10 = KeyCode._from_symbol('F10')
|
||
|
f11 = KeyCode._from_symbol('F11')
|
||
|
f12 = KeyCode._from_symbol('F12')
|
||
|
f13 = KeyCode._from_symbol('F13')
|
||
|
f14 = KeyCode._from_symbol('F14')
|
||
|
f15 = KeyCode._from_symbol('F15')
|
||
|
f16 = KeyCode._from_symbol('F16')
|
||
|
f17 = KeyCode._from_symbol('F17')
|
||
|
f18 = KeyCode._from_symbol('F18')
|
||
|
f19 = KeyCode._from_symbol('F19')
|
||
|
f20 = KeyCode._from_symbol('F20')
|
||
|
home = KeyCode._from_symbol('Home')
|
||
|
left = KeyCode._from_symbol('Left')
|
||
|
page_down = KeyCode._from_symbol('Page_Down')
|
||
|
page_up = KeyCode._from_symbol('Page_Up')
|
||
|
right = KeyCode._from_symbol('Right')
|
||
|
shift = KeyCode._from_symbol('Shift_L')
|
||
|
shift_l = KeyCode._from_symbol('Shift_L')
|
||
|
shift_r = KeyCode._from_symbol('Shift_R')
|
||
|
space = KeyCode._from_symbol('space', char=' ')
|
||
|
tab = KeyCode._from_symbol('Tab')
|
||
|
up = KeyCode._from_symbol('Up')
|
||
|
|
||
|
media_play_pause = KeyCode._from_media('Play')
|
||
|
media_volume_mute = KeyCode._from_media('Mute')
|
||
|
media_volume_down = KeyCode._from_media('LowerVolume')
|
||
|
media_volume_up = KeyCode._from_media('RaiseVolume')
|
||
|
media_previous = KeyCode._from_media('Prev')
|
||
|
media_next = KeyCode._from_media('Next')
|
||
|
|
||
|
insert = KeyCode._from_symbol('Insert')
|
||
|
menu = KeyCode._from_symbol('Menu')
|
||
|
num_lock = KeyCode._from_symbol('Num_Lock')
|
||
|
pause = KeyCode._from_symbol('Pause')
|
||
|
print_screen = KeyCode._from_symbol('Print')
|
||
|
scroll_lock = KeyCode._from_symbol('Scroll_Lock')
|
||
|
# pylint: enable=W0212
|
||
|
|
||
|
|
||
|
class Controller(NotifierMixin, _base.Controller):
|
||
|
_KeyCode = KeyCode
|
||
|
_Key = Key
|
||
|
|
||
|
#: The shift mask for :attr:`Key.ctrl`
|
||
|
CTRL_MASK = Xlib.X.ControlMask
|
||
|
|
||
|
#: The shift mask for :attr:`Key.shift`
|
||
|
SHIFT_MASK = Xlib.X.ShiftMask
|
||
|
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
super(Controller, self).__init__(*args, **kwargs)
|
||
|
self._display = Xlib.display.Display()
|
||
|
self._keyboard_mapping = None
|
||
|
self._borrows = {}
|
||
|
self._borrow_lock = threading.RLock()
|
||
|
|
||
|
# pylint: disable=C0103; this is treated as a class scope constant, but
|
||
|
# we cannot set it in the class scope, as it requires a Display instance
|
||
|
self.ALT_MASK = alt_mask(self._display)
|
||
|
self.ALT_GR_MASK = alt_gr_mask(self._display)
|
||
|
# pylint: enable=C0103
|
||
|
|
||
|
def __del__(self):
|
||
|
if self._display:
|
||
|
self._display.close()
|
||
|
|
||
|
@property
|
||
|
def keyboard_mapping(self):
|
||
|
"""A mapping from *keysyms* to *key codes*.
|
||
|
|
||
|
Each value is the tuple ``(key_code, shift_state)``. By sending an
|
||
|
event with the specified *key code* and shift state, the specified
|
||
|
*keysym* will be touched.
|
||
|
"""
|
||
|
if not self._keyboard_mapping:
|
||
|
self._update_keyboard_mapping()
|
||
|
return self._keyboard_mapping
|
||
|
|
||
|
def _handle(self, key, is_press):
|
||
|
"""Resolves a key identifier and sends a keyboard event.
|
||
|
|
||
|
:param event: The *X* keyboard event.
|
||
|
|
||
|
:param int keysym: The keysym to handle.
|
||
|
"""
|
||
|
event = Xlib.display.event.KeyPress if is_press \
|
||
|
else Xlib.display.event.KeyRelease
|
||
|
keysym = self._keysym(key)
|
||
|
|
||
|
# Make sure to verify that the key was resolved
|
||
|
if keysym is None:
|
||
|
raise self.InvalidKeyException(key)
|
||
|
|
||
|
# If the key has a virtual key code, use that immediately with
|
||
|
# fake_input; fake input,being an X server extension, has access to
|
||
|
# more internal state that we do
|
||
|
if key.vk is not None:
|
||
|
with display_manager(self._display) as dm:
|
||
|
Xlib.ext.xtest.fake_input(
|
||
|
dm,
|
||
|
Xlib.X.KeyPress if is_press else Xlib.X.KeyRelease,
|
||
|
dm.keysym_to_keycode(key.vk))
|
||
|
|
||
|
# Otherwise use XSendEvent; we need to use this in the general case to
|
||
|
# work around problems with keyboard layouts
|
||
|
else:
|
||
|
try:
|
||
|
keycode, shift_state = self.keyboard_mapping[keysym]
|
||
|
self._send_key(event, keycode, shift_state)
|
||
|
|
||
|
except KeyError:
|
||
|
with self._borrow_lock:
|
||
|
keycode, index, count = self._borrows[keysym]
|
||
|
self._send_key(
|
||
|
event,
|
||
|
keycode,
|
||
|
index_to_shift(self._display, index))
|
||
|
count += 1 if is_press else -1
|
||
|
self._borrows[keysym] = (keycode, index, count)
|
||
|
|
||
|
# Notify any running listeners
|
||
|
self._emit('_on_fake_event', key, is_press)
|
||
|
|
||
|
def _keysym(self, key):
|
||
|
"""Converts a key to a *keysym*.
|
||
|
|
||
|
:param KeyCode key: The key code to convert.
|
||
|
"""
|
||
|
return self._resolve_dead(key) if key.is_dead else None \
|
||
|
or self._resolve_special(key) \
|
||
|
or self._resolve_normal(key) \
|
||
|
or self._resolve_borrowed(key) \
|
||
|
or self._resolve_borrowing(key)
|
||
|
|
||
|
def _send_key(self, event, keycode, shift_state):
|
||
|
"""Sends a single keyboard event.
|
||
|
|
||
|
:param event: The *X* keyboard event.
|
||
|
|
||
|
:param int keycode: The calculated keycode.
|
||
|
|
||
|
:param int shift_state: The shift state. The actual value used is
|
||
|
:attr:`shift_state` or'd with this value.
|
||
|
"""
|
||
|
with display_manager(self._display) as dm, self.modifiers as modifiers:
|
||
|
# Under certain cimcumstances, such as when running under Xephyr,
|
||
|
# the value returned by dm.get_input_focus is an int
|
||
|
window = dm.get_input_focus().focus
|
||
|
send_event = getattr(
|
||
|
window,
|
||
|
'send_event',
|
||
|
lambda event: dm.send_event(window, event))
|
||
|
send_event(event(
|
||
|
detail=keycode,
|
||
|
state=shift_state | self._shift_mask(modifiers),
|
||
|
time=0,
|
||
|
root=dm.screen().root,
|
||
|
window=window,
|
||
|
same_screen=0,
|
||
|
child=Xlib.X.NONE,
|
||
|
root_x=0, root_y=0, event_x=0, event_y=0))
|
||
|
|
||
|
def _resolve_dead(self, key):
|
||
|
"""Tries to resolve a dead key.
|
||
|
|
||
|
:param str identifier: The identifier to resolve.
|
||
|
"""
|
||
|
# pylint: disable=W0702; we want to ignore errors
|
||
|
try:
|
||
|
keysym, _ = SYMBOLS[CHARS[key.combining]]
|
||
|
except:
|
||
|
return None
|
||
|
# pylint: enable=W0702
|
||
|
|
||
|
if keysym not in self.keyboard_mapping:
|
||
|
return None
|
||
|
|
||
|
return keysym
|
||
|
|
||
|
def _resolve_special(self, key):
|
||
|
"""Tries to resolve a special key.
|
||
|
|
||
|
A special key has the :attr:`~KeyCode.vk` attribute set.
|
||
|
|
||
|
:param KeyCode key: The key to resolve.
|
||
|
"""
|
||
|
if not key.vk:
|
||
|
return None
|
||
|
|
||
|
return key.vk
|
||
|
|
||
|
def _resolve_normal(self, key):
|
||
|
"""Tries to resolve a normal key.
|
||
|
|
||
|
A normal key exists on the keyboard, and is typed by pressing
|
||
|
and releasing a simple key, possibly in combination with a modifier.
|
||
|
|
||
|
:param KeyCode key: The key to resolve.
|
||
|
"""
|
||
|
keysym = self._key_to_keysym(key)
|
||
|
if keysym is None:
|
||
|
return None
|
||
|
|
||
|
if keysym not in self.keyboard_mapping:
|
||
|
return None
|
||
|
|
||
|
return keysym
|
||
|
|
||
|
def _resolve_borrowed(self, key):
|
||
|
"""Tries to resolve a key by looking up the already borrowed *keysyms*.
|
||
|
|
||
|
A borrowed *keysym* does not exist on the keyboard, but has been
|
||
|
temporarily added to the layout.
|
||
|
|
||
|
:param KeyCode key: The key to resolve.
|
||
|
"""
|
||
|
keysym = self._key_to_keysym(key)
|
||
|
if keysym is None:
|
||
|
return None
|
||
|
|
||
|
with self._borrow_lock:
|
||
|
if keysym not in self._borrows:
|
||
|
return None
|
||
|
|
||
|
return keysym
|
||
|
|
||
|
def _resolve_borrowing(self, key):
|
||
|
"""Tries to resolve a key by modifying the layout temporarily.
|
||
|
|
||
|
A borrowed *keysym* does not exist on the keyboard, but is temporarily
|
||
|
added to the layout.
|
||
|
|
||
|
:param KeyCode key: The key to resolve.
|
||
|
"""
|
||
|
keysym = self._key_to_keysym(key)
|
||
|
if keysym is None:
|
||
|
return None
|
||
|
|
||
|
mapping = self._display.get_keyboard_mapping(8, 255 - 8)
|
||
|
|
||
|
def i2kc(index):
|
||
|
return index + 8
|
||
|
|
||
|
def kc2i(keycode):
|
||
|
return keycode - 8
|
||
|
|
||
|
#: Finds a keycode and index by looking at already used keycodes
|
||
|
def reuse():
|
||
|
for _, (keycode, _, _) in self._borrows.items():
|
||
|
keycodes = mapping[kc2i(keycode)]
|
||
|
|
||
|
# Only the first four items are addressable by X
|
||
|
for index in range(4):
|
||
|
if not keycodes[index]:
|
||
|
return keycode, index
|
||
|
|
||
|
#: Finds a keycode and index by using a new keycode
|
||
|
def borrow():
|
||
|
for i, keycodes in enumerate(mapping):
|
||
|
if not any(keycodes):
|
||
|
return i2kc(i), 0
|
||
|
|
||
|
#: Finds a keycode and index by reusing an old, unused one
|
||
|
def overwrite():
|
||
|
for keysym, (keycode, index, count) in self._borrows.items():
|
||
|
if count < 1:
|
||
|
del self._borrows[keysym]
|
||
|
return keycode, index
|
||
|
|
||
|
#: Registers a keycode for a specific key and modifier state
|
||
|
def register(dm, keycode, index):
|
||
|
i = kc2i(keycode)
|
||
|
|
||
|
# Check for use of empty mapping with a character that has upper
|
||
|
# and lower forms
|
||
|
lower = key.char.lower()
|
||
|
upper = key.char.upper()
|
||
|
if lower != upper and len(lower) == 1 and len(upper) == 1 and all(
|
||
|
m == Xlib.XK.NoSymbol
|
||
|
for m in mapping[i]):
|
||
|
lower = self._key_to_keysym(KeyCode.from_char(lower))
|
||
|
upper = self._key_to_keysym(KeyCode.from_char(upper))
|
||
|
if lower:
|
||
|
mapping[i][0] = lower
|
||
|
self._borrows[lower] = (keycode, 0, 0)
|
||
|
if upper:
|
||
|
mapping[i][1] = upper
|
||
|
self._borrows[upper] = (keycode, 1, 0)
|
||
|
else:
|
||
|
mapping[i][index] = keysym
|
||
|
self._borrows[keysym] = (keycode, index, 0)
|
||
|
dm.change_keyboard_mapping(keycode, mapping[i:i + 1])
|
||
|
|
||
|
try:
|
||
|
with display_manager(self._display) as dm, self._borrow_lock as _:
|
||
|
# First try an already used keycode, then try a new one, and
|
||
|
# fall back on reusing one that is not currently pressed
|
||
|
register(dm, *(
|
||
|
reuse() or
|
||
|
borrow() or
|
||
|
overwrite()))
|
||
|
return keysym
|
||
|
|
||
|
except TypeError:
|
||
|
return None
|
||
|
|
||
|
def _key_to_keysym(self, key):
|
||
|
"""Converts a character key code to a *keysym*.
|
||
|
|
||
|
:param KeyCode key: The key code.
|
||
|
|
||
|
:return: a keysym if found
|
||
|
:rtype: int or None
|
||
|
"""
|
||
|
# If the key code already has a VK, simply return it
|
||
|
if key.vk is not None:
|
||
|
return key.vk
|
||
|
|
||
|
# If the character has no associated symbol, we try to map the
|
||
|
# character to a keysym
|
||
|
symbol = CHARS.get(key.char, None)
|
||
|
if symbol is None:
|
||
|
return char_to_keysym(key.char)
|
||
|
|
||
|
# Otherwise we attempt to convert the symbol to a keysym
|
||
|
# pylint: disable=W0702; we want to ignore errors
|
||
|
try:
|
||
|
return symbol_to_keysym(symbol)
|
||
|
except:
|
||
|
try:
|
||
|
return SYMBOLS[symbol][0]
|
||
|
except:
|
||
|
return None
|
||
|
# pylint: enable=W0702
|
||
|
|
||
|
def _shift_mask(self, modifiers):
|
||
|
"""The *X* modifier mask to apply for a set of modifiers.
|
||
|
|
||
|
:param set modifiers: A set of active modifiers for which to get the
|
||
|
shift mask.
|
||
|
"""
|
||
|
return (
|
||
|
0
|
||
|
| (self.ALT_MASK
|
||
|
if Key.alt in modifiers else 0)
|
||
|
|
||
|
| (self.ALT_GR_MASK
|
||
|
if Key.alt_gr in modifiers else 0)
|
||
|
|
||
|
| (self.CTRL_MASK
|
||
|
if Key.ctrl in modifiers else 0)
|
||
|
|
||
|
| (self.SHIFT_MASK
|
||
|
if Key.shift in modifiers else 0))
|
||
|
|
||
|
def _update_keyboard_mapping(self):
|
||
|
"""Updates the keyboard mapping.
|
||
|
"""
|
||
|
with display_manager(self._display) as dm:
|
||
|
self._keyboard_mapping = keyboard_mapping(dm)
|
||
|
|
||
|
|
||
|
@Controller._receiver
|
||
|
class Listener(ListenerMixin, _base.Listener):
|
||
|
_EVENTS = (
|
||
|
Xlib.X.KeyPress,
|
||
|
Xlib.X.KeyRelease)
|
||
|
|
||
|
#: A mapping from keysym to special key
|
||
|
_SPECIAL_KEYS = {
|
||
|
key.value.vk: key
|
||
|
for key in Key}
|
||
|
|
||
|
#: A mapping from numeric keypad keys to keys
|
||
|
_KEYPAD_KEYS = {
|
||
|
KEYPAD_KEYS['KP_0']: KeyCode.from_char('0'),
|
||
|
KEYPAD_KEYS['KP_1']: KeyCode.from_char('1'),
|
||
|
KEYPAD_KEYS['KP_2']: KeyCode.from_char('2'),
|
||
|
KEYPAD_KEYS['KP_3']: KeyCode.from_char('3'),
|
||
|
KEYPAD_KEYS['KP_4']: KeyCode.from_char('4'),
|
||
|
KEYPAD_KEYS['KP_5']: KeyCode.from_char('5'),
|
||
|
KEYPAD_KEYS['KP_6']: KeyCode.from_char('6'),
|
||
|
KEYPAD_KEYS['KP_7']: KeyCode.from_char('7'),
|
||
|
KEYPAD_KEYS['KP_8']: KeyCode.from_char('8'),
|
||
|
KEYPAD_KEYS['KP_9']: KeyCode.from_char('9'),
|
||
|
KEYPAD_KEYS['KP_Add']: KeyCode.from_char('+'),
|
||
|
KEYPAD_KEYS['KP_Decimal']: KeyCode.from_char(','),
|
||
|
KEYPAD_KEYS['KP_Delete']: Key.delete,
|
||
|
KEYPAD_KEYS['KP_Divide']: KeyCode.from_char('/'),
|
||
|
KEYPAD_KEYS['KP_Down']: Key.down,
|
||
|
KEYPAD_KEYS['KP_End']: Key.end,
|
||
|
KEYPAD_KEYS['KP_Enter']: Key.enter,
|
||
|
KEYPAD_KEYS['KP_Equal']: KeyCode.from_char('='),
|
||
|
KEYPAD_KEYS['KP_F1']: Key.f1,
|
||
|
KEYPAD_KEYS['KP_F2']: Key.f2,
|
||
|
KEYPAD_KEYS['KP_F3']: Key.f3,
|
||
|
KEYPAD_KEYS['KP_F4']: Key.f4,
|
||
|
KEYPAD_KEYS['KP_Home']: Key.home,
|
||
|
KEYPAD_KEYS['KP_Insert']: Key.insert,
|
||
|
KEYPAD_KEYS['KP_Left']: Key.left,
|
||
|
KEYPAD_KEYS['KP_Multiply']: KeyCode.from_char('*'),
|
||
|
KEYPAD_KEYS['KP_Page_Down']: Key.page_down,
|
||
|
KEYPAD_KEYS['KP_Page_Up']: Key.page_up,
|
||
|
KEYPAD_KEYS['KP_Right']: Key.right,
|
||
|
KEYPAD_KEYS['KP_Space']: Key.space,
|
||
|
KEYPAD_KEYS['KP_Subtract']: KeyCode.from_char('-'),
|
||
|
KEYPAD_KEYS['KP_Tab']: Key.tab,
|
||
|
KEYPAD_KEYS['KP_Up']: Key.up}
|
||
|
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
super(Listener, self).__init__(*args, **kwargs)
|
||
|
self._keyboard_mapping = None
|
||
|
|
||
|
def _run(self):
|
||
|
with self._receive():
|
||
|
super(Listener, self)._run()
|
||
|
|
||
|
def _initialize(self, display):
|
||
|
# Get the keyboard mapping to be able to translate events details to
|
||
|
# key codes
|
||
|
min_keycode = display.display.info.min_keycode
|
||
|
keycode_count = display.display.info.max_keycode - min_keycode + 1
|
||
|
self._keyboard_mapping = display.get_keyboard_mapping(
|
||
|
min_keycode, keycode_count)
|
||
|
|
||
|
def _handle(self, display, event):
|
||
|
# Convert the event to a KeyCode; this may fail, and in that case we
|
||
|
# pass None
|
||
|
try:
|
||
|
key = self._event_to_key(display, event)
|
||
|
except IndexError:
|
||
|
key = None
|
||
|
|
||
|
if event.type == Xlib.X.KeyPress:
|
||
|
self.on_press(key)
|
||
|
|
||
|
elif event.type == Xlib.X.KeyRelease:
|
||
|
self.on_release(key)
|
||
|
|
||
|
def _suppress_start(self, display):
|
||
|
display.screen().root.grab_keyboard(
|
||
|
self._event_mask, Xlib.X.GrabModeAsync, Xlib.X.GrabModeAsync,
|
||
|
Xlib.X.CurrentTime)
|
||
|
|
||
|
def _suppress_stop(self, display):
|
||
|
display.ungrab_keyboard(Xlib.X.CurrentTime)
|
||
|
|
||
|
def _on_fake_event(self, key, is_press):
|
||
|
"""The handler for fake press events sent by the controllers.
|
||
|
|
||
|
:param KeyCode key: The key pressed.
|
||
|
|
||
|
:param bool is_press: Whether this is a press event.
|
||
|
"""
|
||
|
(self.on_press if is_press else self.on_release)(
|
||
|
self._SPECIAL_KEYS.get(key.vk, key))
|
||
|
|
||
|
def _keycode_to_keysym(self, display, keycode, index):
|
||
|
"""Converts a keycode and shift state index to a keysym.
|
||
|
|
||
|
This method uses a simplified version of the *X* convention to locate
|
||
|
the correct keysym in the display table: since this method is only used
|
||
|
to locate special keys, alphanumeric keys are not treated specially.
|
||
|
|
||
|
:param display: The current *X* display.
|
||
|
|
||
|
:param keycode: The keycode.
|
||
|
|
||
|
:param index: The shift state index.
|
||
|
|
||
|
:return: a keysym
|
||
|
"""
|
||
|
keysym = display.keycode_to_keysym(keycode, index)
|
||
|
if keysym:
|
||
|
return keysym
|
||
|
elif index & 0x2:
|
||
|
return self._keycode_to_keysym(display, keycode, index & ~0x2)
|
||
|
elif index & 0x1:
|
||
|
return self._keycode_to_keysym(display, keycode, index & ~0x1)
|
||
|
else:
|
||
|
return 0
|
||
|
|
||
|
def _event_to_key(self, display, event):
|
||
|
"""Converts an *X* event to a :class:`KeyCode`.
|
||
|
|
||
|
:param display: The current *X* display.
|
||
|
|
||
|
:param event: The event to convert.
|
||
|
|
||
|
:return: a :class:`pynput.keyboard.KeyCode`
|
||
|
|
||
|
:raises IndexError: if the key code is invalid
|
||
|
"""
|
||
|
keycode = event.detail
|
||
|
index = shift_to_index(display, event.state)
|
||
|
|
||
|
# First try special keys...
|
||
|
keysym = self._keycode_to_keysym(display, keycode, index)
|
||
|
if keysym in self._SPECIAL_KEYS:
|
||
|
return self._SPECIAL_KEYS[keysym]
|
||
|
elif keysym in self._KEYPAD_KEYS:
|
||
|
# We must recalculate the index if numlock is active; index 1 is the
|
||
|
# one to use
|
||
|
try:
|
||
|
return self._KEYPAD_KEYS[
|
||
|
self._keycode_to_keysym(
|
||
|
display,
|
||
|
keycode,
|
||
|
bool(event.state & numlock_mask(display)))]
|
||
|
except KeyError:
|
||
|
# Since we recalculated the key, this may happen
|
||
|
pass
|
||
|
|
||
|
# ...then try characters...
|
||
|
name = KEYSYMS.get(keysym, None)
|
||
|
if name is not None and name in SYMBOLS:
|
||
|
char = SYMBOLS[name][1].upper() if index & 1 else SYMBOLS[name][1]
|
||
|
if char in DEAD_KEYS:
|
||
|
return KeyCode.from_dead(DEAD_KEYS[char], vk=keysym)
|
||
|
else:
|
||
|
return KeyCode.from_char(char, vk=keysym)
|
||
|
|
||
|
# ...and fall back on a virtual key code
|
||
|
return KeyCode.from_vk(keysym)
|