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.
347 lines
11 KiB
347 lines
11 KiB
# 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 *Windows*.
|
|
"""
|
|
|
|
# pylint: disable=C0111
|
|
# The documentation is extracted from the base classes
|
|
|
|
# pylint: disable=R0903
|
|
# We implement stubs
|
|
|
|
import contextlib
|
|
import ctypes
|
|
import enum
|
|
import six
|
|
|
|
from ctypes import wintypes
|
|
|
|
import pynput._util.win32_vks as VK
|
|
|
|
from pynput._util import AbstractListener
|
|
from pynput._util.win32 import (
|
|
INPUT,
|
|
INPUT_union,
|
|
KEYBDINPUT,
|
|
KeyTranslator,
|
|
ListenerMixin,
|
|
MapVirtualKey,
|
|
SendInput,
|
|
SystemHook,
|
|
VkKeyScan)
|
|
from . import _base
|
|
|
|
|
|
class KeyCode(_base.KeyCode):
|
|
_PLATFORM_EXTENSIONS = (
|
|
# Any extra flags.
|
|
'_flags',
|
|
|
|
#: The scan code.
|
|
'_scan',
|
|
)
|
|
|
|
# Be explicit about fields
|
|
_flags = None
|
|
_scan = None
|
|
|
|
def _parameters(self, is_press):
|
|
"""The parameters to pass to ``SendInput`` to generate this key.
|
|
|
|
:param bool is_press: Whether to generate a press event.
|
|
|
|
:return: all arguments to pass to ``SendInput`` for this key
|
|
|
|
:rtype: dict
|
|
"""
|
|
if self.vk:
|
|
vk = self.vk
|
|
scan = self._scan \
|
|
or MapVirtualKey(vk, MapVirtualKey.MAPVK_VK_TO_VSC)
|
|
flags = 0
|
|
else:
|
|
res = VkKeyScan(self.char)
|
|
if (res >> 8) & 0xFF == 0:
|
|
vk = res & 0xFF
|
|
scan = self._scan \
|
|
or MapVirtualKey(vk, MapVirtualKey.MAPVK_VK_TO_VSC)
|
|
flags = 0
|
|
else:
|
|
vk = 0
|
|
scan = ord(self.char)
|
|
flags = KEYBDINPUT.UNICODE
|
|
state_flags = (KEYBDINPUT.KEYUP if not is_press else 0)
|
|
return dict(
|
|
dwFlags=(self._flags or 0) | flags | state_flags,
|
|
wVk=vk,
|
|
wScan=scan)
|
|
|
|
@classmethod
|
|
def _from_ext(cls, vk, **kwargs):
|
|
"""Creates an extended key code.
|
|
|
|
:param vk: The virtual key code.
|
|
|
|
:param kwargs: Any other parameters to pass.
|
|
|
|
:return: a key code
|
|
"""
|
|
return cls.from_vk(vk, _flags=KEYBDINPUT.EXTENDEDKEY, **kwargs)
|
|
|
|
|
|
# pylint: disable=W0212
|
|
class Key(enum.Enum):
|
|
alt = KeyCode.from_vk(VK.MENU)
|
|
alt_l = KeyCode.from_vk(VK.LMENU)
|
|
alt_r = KeyCode._from_ext(VK.RMENU)
|
|
alt_gr = KeyCode.from_vk(VK.RMENU)
|
|
backspace = KeyCode.from_vk(VK.BACK)
|
|
caps_lock = KeyCode.from_vk(VK.CAPITAL)
|
|
cmd = KeyCode.from_vk(VK.LWIN)
|
|
cmd_l = KeyCode.from_vk(VK.LWIN)
|
|
cmd_r = KeyCode.from_vk(VK.RWIN)
|
|
ctrl = KeyCode.from_vk(VK.CONTROL)
|
|
ctrl_l = KeyCode.from_vk(VK.LCONTROL)
|
|
ctrl_r = KeyCode._from_ext(VK.RCONTROL)
|
|
delete = KeyCode._from_ext(VK.DELETE)
|
|
down = KeyCode._from_ext(VK.DOWN)
|
|
end = KeyCode._from_ext(VK.END)
|
|
enter = KeyCode.from_vk(VK.RETURN)
|
|
esc = KeyCode.from_vk(VK.ESCAPE)
|
|
f1 = KeyCode.from_vk(VK.F1)
|
|
f2 = KeyCode.from_vk(VK.F2)
|
|
f3 = KeyCode.from_vk(VK.F3)
|
|
f4 = KeyCode.from_vk(VK.F4)
|
|
f5 = KeyCode.from_vk(VK.F5)
|
|
f6 = KeyCode.from_vk(VK.F6)
|
|
f7 = KeyCode.from_vk(VK.F7)
|
|
f8 = KeyCode.from_vk(VK.F8)
|
|
f9 = KeyCode.from_vk(VK.F9)
|
|
f10 = KeyCode.from_vk(VK.F10)
|
|
f11 = KeyCode.from_vk(VK.F11)
|
|
f12 = KeyCode.from_vk(VK.F12)
|
|
f13 = KeyCode.from_vk(VK.F13)
|
|
f14 = KeyCode.from_vk(VK.F14)
|
|
f15 = KeyCode.from_vk(VK.F15)
|
|
f16 = KeyCode.from_vk(VK.F16)
|
|
f17 = KeyCode.from_vk(VK.F17)
|
|
f18 = KeyCode.from_vk(VK.F18)
|
|
f19 = KeyCode.from_vk(VK.F19)
|
|
f20 = KeyCode.from_vk(VK.F20)
|
|
f21 = KeyCode.from_vk(VK.F21)
|
|
f22 = KeyCode.from_vk(VK.F22)
|
|
f23 = KeyCode.from_vk(VK.F23)
|
|
f24 = KeyCode.from_vk(VK.F24)
|
|
home = KeyCode._from_ext(VK.HOME)
|
|
left = KeyCode._from_ext(VK.LEFT)
|
|
page_down = KeyCode._from_ext(VK.NEXT)
|
|
page_up = KeyCode._from_ext(VK.PRIOR)
|
|
right = KeyCode._from_ext(VK.RIGHT)
|
|
shift = KeyCode.from_vk(VK.LSHIFT)
|
|
shift_l = KeyCode.from_vk(VK.LSHIFT)
|
|
shift_r = KeyCode.from_vk(VK.RSHIFT)
|
|
space = KeyCode.from_vk(VK.SPACE, char=' ')
|
|
tab = KeyCode.from_vk(VK.TAB)
|
|
up = KeyCode._from_ext(VK.UP)
|
|
|
|
media_play_pause = KeyCode._from_ext(VK.MEDIA_PLAY_PAUSE)
|
|
media_volume_mute = KeyCode._from_ext(VK.VOLUME_MUTE)
|
|
media_volume_down = KeyCode._from_ext(VK.VOLUME_DOWN)
|
|
media_volume_up = KeyCode._from_ext(VK.VOLUME_UP)
|
|
media_previous = KeyCode._from_ext(VK.MEDIA_PREV_TRACK)
|
|
media_next = KeyCode._from_ext(VK.MEDIA_NEXT_TRACK)
|
|
|
|
insert = KeyCode._from_ext(VK.INSERT)
|
|
menu = KeyCode.from_vk(VK.APPS)
|
|
num_lock = KeyCode._from_ext(VK.NUMLOCK)
|
|
pause = KeyCode.from_vk(VK.PAUSE)
|
|
print_screen = KeyCode._from_ext(VK.SNAPSHOT)
|
|
scroll_lock = KeyCode.from_vk(VK.SCROLL)
|
|
# pylint: enable=W0212
|
|
|
|
|
|
class Controller(_base.Controller):
|
|
_KeyCode = KeyCode
|
|
_Key = Key
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(Controller, self).__init__(*args, **kwargs)
|
|
|
|
def _handle(self, key, is_press):
|
|
SendInput(
|
|
1,
|
|
ctypes.byref(INPUT(
|
|
type=INPUT.KEYBOARD,
|
|
value=INPUT_union(
|
|
ki=KEYBDINPUT(**key._parameters(is_press))))),
|
|
ctypes.sizeof(INPUT))
|
|
|
|
|
|
class Listener(ListenerMixin, _base.Listener):
|
|
#: The Windows hook ID for low level keyboard events, ``WH_KEYBOARD_LL``
|
|
_EVENTS = 13
|
|
|
|
_WM_INPUTLANGCHANGE = 0x0051
|
|
_WM_KEYDOWN = 0x0100
|
|
_WM_KEYUP = 0x0101
|
|
_WM_SYSKEYDOWN = 0x0104
|
|
_WM_SYSKEYUP = 0x0105
|
|
|
|
# A bit flag attached to messages indicating that the payload is an actual
|
|
# UTF-16 character code
|
|
_UTF16_FLAG = 0x1000
|
|
|
|
# A special virtual key code designating unicode characters
|
|
_VK_PACKET = 0xE7
|
|
|
|
#: The messages that correspond to a key press
|
|
_PRESS_MESSAGES = (_WM_KEYDOWN, _WM_SYSKEYDOWN)
|
|
|
|
#: The messages that correspond to a key release
|
|
_RELEASE_MESSAGES = (_WM_KEYUP, _WM_SYSKEYUP)
|
|
|
|
#: Additional window messages to propagate to the subclass handler.
|
|
_WM_NOTIFICATIONS = (
|
|
_WM_INPUTLANGCHANGE,
|
|
)
|
|
|
|
#: A mapping from keysym to special key
|
|
_SPECIAL_KEYS = {
|
|
key.value.vk: key
|
|
for key in Key}
|
|
|
|
_HANDLED_EXCEPTIONS = (
|
|
SystemHook.SuppressException,)
|
|
|
|
class _KBDLLHOOKSTRUCT(ctypes.Structure):
|
|
"""Contains information about a mouse event passed to a
|
|
``WH_KEYBOARD_LL`` hook procedure, ``LowLevelKeyboardProc``.
|
|
"""
|
|
_fields_ = [
|
|
('vkCode', wintypes.DWORD),
|
|
('scanCode', wintypes.DWORD),
|
|
('flags', wintypes.DWORD),
|
|
('time', wintypes.DWORD),
|
|
('dwExtraInfo', ctypes.c_void_p)]
|
|
|
|
#: A pointer to a :class:`KBDLLHOOKSTRUCT`
|
|
_LPKBDLLHOOKSTRUCT = ctypes.POINTER(_KBDLLHOOKSTRUCT)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(Listener, self).__init__(*args, **kwargs)
|
|
self._translator = KeyTranslator()
|
|
self._event_filter = self._options.get(
|
|
'event_filter',
|
|
lambda msg, data: True)
|
|
|
|
def _convert(self, code, msg, lpdata):
|
|
if code != SystemHook.HC_ACTION:
|
|
return
|
|
|
|
data = ctypes.cast(lpdata, self._LPKBDLLHOOKSTRUCT).contents
|
|
is_packet = data.vkCode == self._VK_PACKET
|
|
|
|
# Suppress further propagation of the event if it is filtered
|
|
if self._event_filter(msg, data) is False:
|
|
return None
|
|
elif is_packet:
|
|
return (msg | self._UTF16_FLAG, data.scanCode)
|
|
else:
|
|
return (msg, data.vkCode)
|
|
|
|
@AbstractListener._emitter
|
|
def _process(self, wparam, lparam):
|
|
msg = wparam
|
|
vk = lparam
|
|
|
|
# If the key has the UTF-16 flag, we treat it as a unicode character,
|
|
# otherwise convert the event to a KeyCode; this may fail, and in that
|
|
# case we pass None
|
|
is_utf16 = msg & self._UTF16_FLAG
|
|
if is_utf16:
|
|
msg = msg ^ self._UTF16_FLAG
|
|
scan = vk
|
|
key = KeyCode.from_char(six.unichr(scan))
|
|
else:
|
|
try:
|
|
key = self._event_to_key(msg, vk)
|
|
except OSError:
|
|
key = None
|
|
|
|
if msg in self._PRESS_MESSAGES:
|
|
self.on_press(key)
|
|
|
|
elif msg in self._RELEASE_MESSAGES:
|
|
self.on_release(key)
|
|
|
|
# pylint: disable=R0201
|
|
@contextlib.contextmanager
|
|
def _receive(self):
|
|
"""An empty context manager; we do not need to fake keyboard events.
|
|
"""
|
|
yield
|
|
# pylint: enable=R0201
|
|
|
|
def _on_notification(self, code, wparam, lparam):
|
|
"""Receives ``WM_INPUTLANGCHANGE`` and updates the cached layout.
|
|
"""
|
|
if code == self._WM_INPUTLANGCHANGE:
|
|
self._translator.update_layout()
|
|
|
|
def _event_to_key(self, msg, vk):
|
|
"""Converts an :class:`_KBDLLHOOKSTRUCT` to a :class:`KeyCode`.
|
|
|
|
:param msg: The message received.
|
|
|
|
:param vk: The virtual key code to convert.
|
|
|
|
:return: a :class:`pynput.keyboard.KeyCode`
|
|
|
|
:raises OSError: if the message and data could not be converted
|
|
"""
|
|
# If the virtual key code corresponds to a Key value, we prefer that
|
|
if vk in self._SPECIAL_KEYS:
|
|
return self._SPECIAL_KEYS[vk]
|
|
else:
|
|
return KeyCode(**self._translate(
|
|
vk,
|
|
msg in self._PRESS_MESSAGES))
|
|
|
|
def _translate(self, vk, is_press):
|
|
"""Translates a virtual key code to a parameter list passable to
|
|
:class:`pynput.keyboard.KeyCode`.
|
|
|
|
:param int vk: The virtual key code.
|
|
|
|
:param bool is_press: Whether this is a press event.
|
|
|
|
:return: a parameter list to the :class:`pynput.keyboard.KeyCode`
|
|
constructor
|
|
"""
|
|
return self._translator(vk, is_press)
|
|
|
|
def canonical(self, key):
|
|
# If the key has a scan code, and we can find the character for it,
|
|
# return that, otherwise call the super class
|
|
scan = getattr(key, '_scan', None)
|
|
if scan is not None:
|
|
char = self._translator.char_from_scan(scan)
|
|
if char is not None:
|
|
return KeyCode.from_char(char)
|
|
|
|
return super(Listener, self).canonical(key)
|
|
|