From bd92a728b07b80db165a894f99579ea6f67eb8c5 Mon Sep 17 00:00:00 2001 From: Dane Finlay Date: Sun, 14 Apr 2019 19:22:33 +1000 Subject: [PATCH 1/8] Add Key and Text action support for X11 and Mac OS using pynput Dragonfly's typeable and keyboard related files have been heavily modified to do this: - The *dragonfly/actions/keyboard.py* file is now a sub package instead. - The Windows Keyboard class has been moved to a new file: *dragonfly/actions/keyboard/_win32.py*. - KeySymbols classes for supported platforms have been defined that map common key names to platform-specific key symbols, e.g. "LSUPER" -> "win32con.VK_LWIN". - "from dragonfly import Keyboard, Typeable" (or equivalent) will now import classes for the current platform. --- dragonfly/actions/__init__.py | 21 +- dragonfly/actions/_generate_typeables.py | 154 +++++----- dragonfly/actions/action_key.py | 10 +- dragonfly/actions/action_text.py | 24 +- dragonfly/actions/actions.py | 17 +- dragonfly/actions/keyboard/__init__.py | 58 ++++ dragonfly/actions/keyboard/_base.py | 91 ++++++ dragonfly/actions/keyboard/_pynput.py | 275 ++++++++++++++++++ .../{keyboard.py => keyboard/_win32.py} | 166 +++++++---- dragonfly/actions/typeables.py | 205 ++++++------- dragonfly/log.py | 2 + dragonfly/os_dependent_mock.py | 16 +- dragonfly/windows/__init__.py | 1 - setup.py | 1 + 14 files changed, 755 insertions(+), 286 deletions(-) create mode 100644 dragonfly/actions/keyboard/__init__.py create mode 100644 dragonfly/actions/keyboard/_base.py create mode 100644 dragonfly/actions/keyboard/_pynput.py rename dragonfly/actions/{keyboard.py => keyboard/_win32.py} (60%) diff --git a/dragonfly/actions/__init__.py b/dragonfly/actions/__init__.py index abfd0610..6ec6a442 100644 --- a/dragonfly/actions/__init__.py +++ b/dragonfly/actions/__init__.py @@ -29,30 +29,29 @@ from .action_mimic import Mimic from .action_cmd import RunCommand from .action_context import ContextAction +from .keyboard import Keyboard, Typeable +from .typeables import typeables +from .action_key import Key +from .action_text import Text -# Import Windows OS dependent classes only for Windows if sys.platform.startswith("win"): - from .action_key import Key - from .action_text import Text - from .action_mouse import Mouse + # Import Windows only classes and functions. from .action_paste import Paste + from .action_mouse import Mouse from .action_waitwindow import WaitWindow from .action_focuswindow import FocusWindow from .action_startapp import StartApp, BringApp from .action_playsound import PlaySound - from .keyboard import Typeable, Keyboard - from .typeables import typeables from .sendinput import (KeyboardInput, MouseInput, HardwareInput, make_input_array, send_input_array) else: - from ..os_dependent_mock import Key - from ..os_dependent_mock import Text - from ..os_dependent_mock import Mouse + # Import mocked classes and functions for other platforms. from ..os_dependent_mock import Paste + from ..os_dependent_mock import Mouse from ..os_dependent_mock import WaitWindow from ..os_dependent_mock import FocusWindow from ..os_dependent_mock import StartApp, BringApp from ..os_dependent_mock import PlaySound - from ..os_dependent_mock import (Typeable, Keyboard, typeables, KeyboardInput, - MouseInput, HardwareInput, make_input_array, + from ..os_dependent_mock import (KeyboardInput, MouseInput, + HardwareInput, make_input_array, send_input_array) diff --git a/dragonfly/actions/_generate_typeables.py b/dragonfly/actions/_generate_typeables.py index 921b8a7d..4fd9a1ae 100644 --- a/dragonfly/actions/_generate_typeables.py +++ b/dragonfly/actions/_generate_typeables.py @@ -127,91 +127,91 @@ (lookup, "?", "? question"), (lookup, "=", "= equal equals")]), ("Whitespace and editing keys", 1, [ - (vkey, "win32con.VK_RETURN", "enter"), - (vkey, "win32con.VK_TAB", "tab"), - (vkey, "win32con.VK_SPACE", "space"), - (vkey, "win32con.VK_BACK", "backspace"), - (vkey, "win32con.VK_DELETE", "delete del")]), + (vkey, "key_symbols.RETURN", "enter"), + (vkey, "key_symbols.TAB", "tab"), + (vkey, "key_symbols.SPACE", "space"), + (vkey, "key_symbols.BACK", "backspace"), + (vkey, "key_symbols.DELETE", "delete del")]), ("Main modifier keys", 1, [ - (vkey, "win32con.VK_SHIFT", "shift"), - (vkey, "win32con.VK_CONTROL", "control ctrl"), - (vkey, "win32con.VK_MENU", "alt")]), + (vkey, "key_symbols.SHIFT", "shift"), + (vkey, "key_symbols.CONTROL", "control ctrl"), + (vkey, "key_symbols.ALT", "alt")]), ("Right modifier keys", 1, [ - (vkey, "win32con.VK_RSHIFT", "rshift"), - (vkey, "win32con.VK_RCONTROL", "rcontrol rctrl"), - (vkey, "win32con.VK_RMENU", "ralt")]), + (vkey, "key_symbols.RSHIFT", "rshift"), + (vkey, "key_symbols.RCONTROL", "rcontrol rctrl"), + (vkey, "key_symbols.RALT", "ralt")]), ("Special keys", 1, [ - (vkey, "win32con.VK_ESCAPE", "escape"), - (vkey, "win32con.VK_INSERT", "insert"), - (vkey, "win32con.VK_PAUSE", "pause"), - (vkey, "win32con.VK_LWIN", "win"), - (vkey, "win32con.VK_RWIN", "rwin"), - (vkey, "win32con.VK_APPS", "apps popup"), - (vkey, "win32con.VK_SNAPSHOT", "snapshot printscreen")]), + (vkey, "key_symbols.ESCAPE", "escape"), + (vkey, "key_symbols.INSERT", "insert"), + (vkey, "key_symbols.PAUSE", "pause"), + (vkey, "key_symbols.LSUPER", "win"), + (vkey, "key_symbols.RSUPER", "rwin"), + (vkey, "key_symbols.APPS", "apps popup"), + (vkey, "key_symbols.SNAPSHOT", "snapshot printscreen")]), ("Lock keys", 1, [ - (vkey, "win32con.VK_SCROLL", "scrolllock"), - (vkey, "win32con.VK_NUMLOCK", "numlock"), - (vkey, "win32con.VK_CAPITAL", "capslock")]), + (vkey, "key_symbols.SCROLL_LOCK", "scrolllock"), + (vkey, "key_symbols.NUM_LOCK", "numlock"), + (vkey, "key_symbols.CAPS_LOCK", "capslock")]), ("Navigation keys", 1, [ - (vkey, "win32con.VK_UP", "up"), - (vkey, "win32con.VK_DOWN", "down"), - (vkey, "win32con.VK_LEFT", "left"), - (vkey, "win32con.VK_RIGHT", "right"), - (vkey, "win32con.VK_PRIOR", "pageup pgup"), - (vkey, "win32con.VK_NEXT", "pagedown pgdown"), - (vkey, "win32con.VK_HOME", "home"), - (vkey, "win32con.VK_END", "end")]), + (vkey, "key_symbols.UP", "up"), + (vkey, "key_symbols.DOWN", "down"), + (vkey, "key_symbols.LEFT", "left"), + (vkey, "key_symbols.RIGHT", "right"), + (vkey, "key_symbols.PAGE_UP", "pageup pgup"), + (vkey, "key_symbols.PAGE_DOWN", "pagedown pgdown"), + (vkey, "key_symbols.HOME", "home"), + (vkey, "key_symbols.END", "end")]), ("Number pad keys", 1, [ - (vkey, "win32con.VK_MULTIPLY", "npmul"), - (vkey, "win32con.VK_ADD", "npadd"), - (vkey, "win32con.VK_SEPARATOR", "npsep"), - (vkey, "win32con.VK_SUBTRACT", "npsub"), - (vkey, "win32con.VK_DECIMAL", "npdec"), - (vkey, "win32con.VK_DIVIDE", "npdiv"), - (vkey, "win32con.VK_NUMPAD0", "numpad0 np0"), - (vkey, "win32con.VK_NUMPAD1", "numpad1 np1"), - (vkey, "win32con.VK_NUMPAD2", "numpad2 np2"), - (vkey, "win32con.VK_NUMPAD3", "numpad3 np3"), - (vkey, "win32con.VK_NUMPAD4", "numpad4 np4"), - (vkey, "win32con.VK_NUMPAD5", "numpad5 np5"), - (vkey, "win32con.VK_NUMPAD6", "numpad6 np6"), - (vkey, "win32con.VK_NUMPAD7", "numpad7 np7"), - (vkey, "win32con.VK_NUMPAD8", "numpad8 np8"), - (vkey, "win32con.VK_NUMPAD9", "numpad9 np9")]), + (vkey, "key_symbols.MULTIPLY", "npmul"), + (vkey, "key_symbols.ADD", "npadd"), + (vkey, "key_symbols.SEPARATOR", "npsep"), + (vkey, "key_symbols.SUBTRACT", "npsub"), + (vkey, "key_symbols.DECIMAL", "npdec"), + (vkey, "key_symbols.DIVIDE", "npdiv"), + (vkey, "key_symbols.NUMPAD0", "numpad0 np0"), + (vkey, "key_symbols.NUMPAD1", "numpad1 np1"), + (vkey, "key_symbols.NUMPAD2", "numpad2 np2"), + (vkey, "key_symbols.NUMPAD3", "numpad3 np3"), + (vkey, "key_symbols.NUMPAD4", "numpad4 np4"), + (vkey, "key_symbols.NUMPAD5", "numpad5 np5"), + (vkey, "key_symbols.NUMPAD6", "numpad6 np6"), + (vkey, "key_symbols.NUMPAD7", "numpad7 np7"), + (vkey, "key_symbols.NUMPAD8", "numpad8 np8"), + (vkey, "key_symbols.NUMPAD9", "numpad9 np9")]), ("Function keys", 1, [ - (vkey, "win32con.VK_F1", "f1"), - (vkey, "win32con.VK_F2", "f2"), - (vkey, "win32con.VK_F3", "f3"), - (vkey, "win32con.VK_F4", "f4"), - (vkey, "win32con.VK_F5", "f5"), - (vkey, "win32con.VK_F6", "f6"), - (vkey, "win32con.VK_F7", "f7"), - (vkey, "win32con.VK_F8", "f8"), - (vkey, "win32con.VK_F9", "f9"), - (vkey, "win32con.VK_F10", "f10"), - (vkey, "win32con.VK_F11", "f11"), - (vkey, "win32con.VK_F12", "f12"), - (vkey, "win32con.VK_F13", "f13"), - (vkey, "win32con.VK_F14", "f14"), - (vkey, "win32con.VK_F15", "f15"), - (vkey, "win32con.VK_F16", "f16"), - (vkey, "win32con.VK_F17", "f17"), - (vkey, "win32con.VK_F18", "f18"), - (vkey, "win32con.VK_F19", "f19"), - (vkey, "win32con.VK_F20", "f20"), - (vkey, "win32con.VK_F21", "f21"), - (vkey, "win32con.VK_F22", "f22"), - (vkey, "win32con.VK_F23", "f23"), - (vkey, "win32con.VK_F24", "f24")]), + (vkey, "key_symbols.F1", "f1"), + (vkey, "key_symbols.F2", "f2"), + (vkey, "key_symbols.F3", "f3"), + (vkey, "key_symbols.F4", "f4"), + (vkey, "key_symbols.F5", "f5"), + (vkey, "key_symbols.F6", "f6"), + (vkey, "key_symbols.F7", "f7"), + (vkey, "key_symbols.F8", "f8"), + (vkey, "key_symbols.F9", "f9"), + (vkey, "key_symbols.F10", "f10"), + (vkey, "key_symbols.F11", "f11"), + (vkey, "key_symbols.F12", "f12"), + (vkey, "key_symbols.F13", "f13"), + (vkey, "key_symbols.F14", "f14"), + (vkey, "key_symbols.F15", "f15"), + (vkey, "key_symbols.F16", "f16"), + (vkey, "key_symbols.F17", "f17"), + (vkey, "key_symbols.F18", "f18"), + (vkey, "key_symbols.F19", "f19"), + (vkey, "key_symbols.F20", "f20"), + (vkey, "key_symbols.F21", "f21"), + (vkey, "key_symbols.F22", "f22"), + (vkey, "key_symbols.F23", "f23"), + (vkey, "key_symbols.F24", "f24")]), ("Multimedia keys", 1, [ - (vkey, "win32con.VK_VOLUME_UP", "volumeup volup"), - (vkey, "win32con.VK_VOLUME_DOWN", "volumedown voldown"), - (vkey, "win32con.VK_VOLUME_MUTE", "volumemute volmute"), - (vkey, "win32con.VK_MEDIA_NEXT_TRACK", "tracknext"), - (vkey, "win32con.VK_MEDIA_PREV_TRACK", "trackprev"), - (vkey, "win32con.VK_MEDIA_PLAY_PAUSE", "playpause"), - (vkey, "win32con.VK_BROWSER_BACK", "browserback"), - (vkey, "win32con.VK_BROWSER_FORWARD", "browserforward")]), + (vkey, "key_symbols.VOLUME_UP", "volumeup volup"), + (vkey, "key_symbols.VOLUME_DOWN", "volumedown voldown"), + (vkey, "key_symbols.VOLUME_MUTE", "volumemute volmute"), + (vkey, "key_symbols.MEDIA_NEXT_TRACK", "tracknext"), + (vkey, "key_symbols.MEDIA_PREV_TRACK", "trackprev"), + (vkey, "key_symbols.MEDIA_PLAY_PAUSE", "playpause"), + (vkey, "key_symbols.BROWSER_BACK", "browserback"), + (vkey, "key_symbols.BROWSER_FORWARD", "browserforward")]), ) diff --git a/dragonfly/actions/action_key.py b/dragonfly/actions/action_key.py index 80c02d55..bfc3b94d 100644 --- a/dragonfly/actions/action_key.py +++ b/dragonfly/actions/action_key.py @@ -26,8 +26,8 @@ This section describes the :class:`Key` action object. This type of action is used for sending keystrokes to the foreground -application. Examples of how to use this class are given in -:ref:`RefKeySpecExamples`. +application. This works on Windows, Mac OS and with X11 (e.g. on Linux). +Examples of how to use this class are given in :ref:`RefKeySpecExamples`. .. _RefKeySpec: @@ -206,8 +206,10 @@ class Key(DynStrActionBase): This class emulates keyboard activity by sending keystrokes to the foreground application. It does this using Dragonfly's keyboard - interface implemented in the :mod:`keyboard` and :mod:`sendinput` - modules. These use the ``sendinput()`` function of the Win32 API. + interface for the current platform. The implementation for Windows + uses the ``sendinput()`` Win32 API function. The implementations + for X11 and Mac OS use + `pynput `__. """ diff --git a/dragonfly/actions/action_text.py b/dragonfly/actions/action_text.py index 4c4799d2..cdcf2788 100644 --- a/dragonfly/actions/action_text.py +++ b/dragonfly/actions/action_text.py @@ -23,7 +23,8 @@ ============================================================================ This section describes the :class:`Text` action object. This type of -action is used for typing text into the foreground application. +action is used for typing text into the foreground application. This works +on Windows, Mac OS and with X11 (e.g. on Linux). It differs from the :class:`Key` action in that :class:`Text` is used for typing literal text, while :class:`dragonfly.actions.action_key.Key` @@ -62,18 +63,21 @@ ``hardware_apps`` list in the configuration file mentioned above to make dragonfly always use hardware emulation for them. +These settings and parameters have no effect on other platforms. + Text class reference ............................................................................ """ +import sys from six import text_type from ..engines import get_engine -from ..windows.clipboard import Clipboard -from ..windows.window import Window +from ..util.clipboard import Clipboard +from ..windows import Window from .action_base import ActionError, DynStrActionBase from .action_key import Key from .keyboard import Keyboard @@ -195,7 +199,7 @@ def _parse_spec(self, spec): else: # Add hardware events. try: - typeable = Keyboard.get_typeable(character) + typeable = self._keyboard.get_typeable(character) hardware_events.extend(typeable.events(self._pause)) except ValueError: hardware_error_message = ("Keyboard interface cannot type this" @@ -206,8 +210,8 @@ def _parse_spec(self, spec): for short in unpack("<" + str(len(byte_stream) // 2) + "H", byte_stream): try: - typeable = Keyboard.get_typeable(short, - is_text=True) + typeable = self._keyboard.get_typeable(short, + is_text=True) unicode_events.extend(typeable.events(self._pause * 0.5)) except ValueError: unicode_error_message = ("Keyboard interface cannot type " @@ -255,7 +259,13 @@ def _execute_events(self, events): events = self._parse_spec(prefix + text + suffix) # Send keyboard events. - if self._use_hardware or require_hardware_emulation(): + use_hardware_events = ( + self._use_hardware or require_hardware_emulation() or + + # Always use hardware_events for non-Windows platforms. + not sys.platform.startswith("win") + ) + if use_hardware_events: error_message = events.hardware_error_message keyboard_events = events.hardware_events else: diff --git a/dragonfly/actions/actions.py b/dragonfly/actions/actions.py index a9885b97..8db0a6c8 100644 --- a/dragonfly/actions/actions.py +++ b/dragonfly/actions/actions.py @@ -24,7 +24,6 @@ """ import sys -# Import OS-agnostic classes from .action_pause import Pause from .action_function import Function from .action_playback import Playback @@ -33,25 +32,23 @@ from .action_mimic import Mimic from .action_cmd import RunCommand from .action_context import ContextAction +from .keyboard import Keyboard, Typeable +from .action_key import Key +from .action_text import Text -# Import Windows OS dependent classes only for Windows if sys.platform.startswith("win"): - from .action_key import Key - from .action_text import Text - from .action_mouse import Mouse + # Import Windows only classes and functions. from .action_paste import Paste + from .action_mouse import Mouse from .action_waitwindow import WaitWindow from .action_focuswindow import FocusWindow from .action_startapp import StartApp, BringApp from .action_playsound import PlaySound - from .keyboard import Keyboard, Typeable else: - from ..os_dependent_mock import Key - from ..os_dependent_mock import Text - from ..os_dependent_mock import Mouse + # Import mocked classes and functions for other platforms. from ..os_dependent_mock import Paste + from ..os_dependent_mock import Mouse from ..os_dependent_mock import WaitWindow from ..os_dependent_mock import FocusWindow from ..os_dependent_mock import StartApp, BringApp from ..os_dependent_mock import PlaySound - from ..os_dependent_mock import Keyboard, Typeable diff --git a/dragonfly/actions/keyboard/__init__.py b/dragonfly/actions/keyboard/__init__.py new file mode 100644 index 00000000..f2aba5a0 --- /dev/null +++ b/dragonfly/actions/keyboard/__init__.py @@ -0,0 +1,58 @@ +# +# This file is part of Dragonfly. +# (c) Copyright 2007, 2008 by Christo Butcher +# Licensed under the LGPL. +# +# Dragonfly 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. +# +# Dragonfly 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 Dragonfly. If not, see +# . +# + +""" +This module initializes the correct keyboard interface for the current +platform. +""" + +import logging +import os +import sys + +_logger = logging.getLogger("keyboard") + +# TODO Implement classes for Wayland (XDG_SESSION_TYPE == "wayland"). + +# Import the Keyboard, KeySymbols and Typeable classes for the current +# platform. +if sys.platform.startswith("win"): + # Import classes for Windows. + from ._win32 import Keyboard, Typeable, Win32KeySymbols as KeySymbols +elif sys.platform == "darwin": + # Import classes for Mac OS. + from ._pynput import Keyboard, Typeable, DarwinKeySymbols as KeySymbols +elif os.environ.get("XDG_SESSION_TYPE") == "x11": + # Import classes for X11 (typically used on Linux systems). + # The XDG_SESSION_TYPE environment variable may not be set in some + # circumstances, in which case it can be set manually in ~/.profile. + from ._pynput import Keyboard, Typeable, X11KeySymbols as KeySymbols +else: + from ._base import (BaseKeyboard as Keyboard, Typeable, + MockKeySymbols as KeySymbols) + + # Warn that no keyboard implementation is available. + # Don't raise an error because this will break continuous integration + # tests. Most of dragonfly can still be used anyway. + _logger.warning("There is no keyboard implementation for this " + "platform!") + +# Initialize a Keyboard instance. +keyboard = Keyboard() diff --git a/dragonfly/actions/keyboard/_base.py b/dragonfly/actions/keyboard/_base.py new file mode 100644 index 00000000..27e14468 --- /dev/null +++ b/dragonfly/actions/keyboard/_base.py @@ -0,0 +1,91 @@ +# +# This file is part of Dragonfly. +# (c) Copyright 2007, 2008 by Christo Butcher +# Licensed under the LGPL. +# +# Dragonfly 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. +# +# Dragonfly 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 Dragonfly. If not, see +# . +# + +""" This file defines the base keyboard interface and Typeables class. """ + + +class MockKeySymbols(object): + def __getattribute__(self, _): + # Always return -1 because no keys can be typed. + return -1 + + +class BaseKeyboard(object): + """ Base keyboard interface. """ + + @classmethod + def send_keyboard_events(cls, events): + """ Send a sequence of keyboard events. """ + raise NotImplementedError("Keyboard support is not implemented for " + "this platform!") + + @classmethod + def get_typeable(cls, char, is_text=False): + """ Get a Typeable object. """ + return Typeable(cls, char, is_text=is_text) + + +class Typeable(object): + """Container for keypress events.""" + + __slots__ = ("_code", "_modifiers", "_name", "_is_text") + + def __init__(self, code, modifiers=(), name=None, is_text=False): + """Set keypress information.""" + self._code = code + self._modifiers = modifiers + self._name = name + self._is_text = is_text + + def __str__(self): + """Return information useful for debugging.""" + return ("%s(%s)" % (self.__class__.__name__, self._name) + + repr(self.events())) + + def on_events(self, timeout=0): + """Return events for pressing this key down.""" + if self._is_text: + events = [(self._code, True, timeout, True)] + else: + events = [(m, True, 0) for m in self._modifiers] + events.append((self._code, True, timeout)) + return events + + def off_events(self, timeout=0): + """Return events for releasing this key.""" + if self._is_text: + events = [(self._code, False, timeout, True)] + else: + events = [(m, False, 0) for m in self._modifiers] + events.append((self._code, False, timeout)) + events.reverse() + return events + + def events(self, timeout=0): + """Return events for pressing and then releasing this key.""" + if self._is_text: + events = [(self._code, True, timeout, True), + (self._code, False, timeout, True)] + else: + events = [(self._code, True, 0), (self._code, False, timeout)] + for m in self._modifiers[-1::-1]: + events.insert(0, (m, True, 0)) + events.append((m, False, 0)) + return events diff --git a/dragonfly/actions/keyboard/_pynput.py b/dragonfly/actions/keyboard/_pynput.py new file mode 100644 index 00000000..683e2e16 --- /dev/null +++ b/dragonfly/actions/keyboard/_pynput.py @@ -0,0 +1,275 @@ +# +# This file is part of Dragonfly. +# (c) Copyright 2007, 2008 by Christo Butcher +# Licensed under the LGPL. +# +# Dragonfly 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. +# +# Dragonfly 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 Dragonfly. If not, see +# . +# + +""" +This file implements the a keyboard interface using the *pynput* Python +package. This implementation is used for Linux (X11) and Mac OS (Darwin). + +""" + +import logging +import sys +import time + +from pynput.keyboard import Controller, KeyCode, Key + +from ._base import BaseKeyboard, Typeable as BaseTypeable + + +class Typeable(BaseTypeable): + """ Typeable class for pynput. """ + _log = logging.getLogger("keyboard") + + def __init__(self, code, modifiers=(), name=None, is_text=False): + # Warn about unsupported keys. + if isinstance(code, KeyCode) and code.vk == -1: + self._log.warning("Unknown key '%s' cannot be typed with " + "pynput on %s", code.char, sys.platform) + BaseTypeable.__init__(self, code, modifiers, name, is_text) + + +class SafeKeyCode(object): + """ + Class to safely get key codes from pynput. + """ + + def __getattr__(self, name): + # Get the key code from pynput, returning KeyCode(vk=-1, char=name) + # if the key name isn't present. + # Keys are undefined on some platforms, e.g. "pause" on Darwin. + return getattr(Key, name, KeyCode(vk=-1, char=name)) + + +virtual_keys = SafeKeyCode() + + +class BaseKeySymbols(object): + """ Base key symbols for pynput. """ + + # Whitespace and editing keys + RETURN = virtual_keys.enter + TAB = virtual_keys.tab + SPACE = virtual_keys.space + BACK = virtual_keys.backspace + DELETE = virtual_keys.delete + + # Main modifier keys + SHIFT = virtual_keys.shift + CONTROL = virtual_keys.ctrl + ALT = virtual_keys.alt + + # Right modifier keys + RSHIFT = virtual_keys.shift_r + RCONTROL = virtual_keys.ctrl_r + RALT = virtual_keys.alt_r + + # Special keys + ESCAPE = virtual_keys.esc + INSERT = virtual_keys.insert + PAUSE = virtual_keys.pause + LSUPER = virtual_keys.cmd_l + RSUPER = virtual_keys.cmd_r + APPS = virtual_keys.menu + SNAPSHOT = virtual_keys.print_screen + + # Lock keys + SCROLL_LOCK = virtual_keys.scroll_lock + NUM_LOCK = virtual_keys.num_lock + CAPS_LOCK = virtual_keys.caps_lock + + # Navigation keys + UP = virtual_keys.up + DOWN = virtual_keys.down + LEFT = virtual_keys.left + RIGHT = virtual_keys.right + PAGE_UP = virtual_keys.page_up + PAGE_DOWN = virtual_keys.page_down + HOME = virtual_keys.home + END = virtual_keys.end + + # Number pad keys + # pynput currently only exposes these for Windows, so we'll map them to + # equivalent characters and numbers instead. + MULTIPLY = KeyCode(char="*") + ADD = KeyCode(char="+") + SEPARATOR = KeyCode(char=".") # this is locale-dependent. + SUBTRACT = KeyCode(char="-") + DECIMAL = KeyCode(char=".") + DIVIDE = KeyCode(char="/") + NUMPAD0 = KeyCode(char="0") + NUMPAD1 = KeyCode(char="1") + NUMPAD2 = KeyCode(char="2") + NUMPAD3 = KeyCode(char="3") + NUMPAD4 = KeyCode(char="4") + NUMPAD5 = KeyCode(char="5") + NUMPAD6 = KeyCode(char="6") + NUMPAD7 = KeyCode(char="7") + NUMPAD8 = KeyCode(char="8") + NUMPAD9 = KeyCode(char="9") + + # Function keys + # F13-20 don't work on X11 with pynput due to a bug. + F1 = virtual_keys.f1 + F2 = virtual_keys.f2 + F3 = virtual_keys.f3 + F4 = virtual_keys.f4 + F5 = virtual_keys.f5 + F6 = virtual_keys.f6 + F7 = virtual_keys.f7 + F8 = virtual_keys.f8 + F9 = virtual_keys.f9 + F10 = virtual_keys.f10 + F11 = virtual_keys.f11 + F12 = virtual_keys.f12 + F13 = virtual_keys.f13 + F14 = virtual_keys.f14 + F15 = virtual_keys.f15 + F16 = virtual_keys.f16 + F17 = virtual_keys.f17 + F18 = virtual_keys.f18 + F19 = virtual_keys.f19 + F20 = virtual_keys.f20 + + +class X11KeySymbols(BaseKeySymbols): + """ + Symbols for X11 from pynput. + + This class includes extra symbols matching those that dragonfly's Win32 + keyboard interface provides. + """ + + # Number pad keys + # Retrieved from /usr/include/X11/keysymdef.h on Debian 9. + MULTIPLY = KeyCode.from_vk(0xffaa) + ADD = KeyCode.from_vk(0xffab) + SEPARATOR = KeyCode.from_vk(0xffac) + SUBTRACT = KeyCode.from_vk(0xffad) + DECIMAL = KeyCode.from_vk(0xffae) + DIVIDE = KeyCode.from_vk(0xffaf) + NUMPAD0 = KeyCode.from_vk(0xffb0) + NUMPAD1 = KeyCode.from_vk(0xffb1) + NUMPAD2 = KeyCode.from_vk(0xffb2) + NUMPAD3 = KeyCode.from_vk(0xffb3) + NUMPAD4 = KeyCode.from_vk(0xffb4) + NUMPAD5 = KeyCode.from_vk(0xffb5) + NUMPAD6 = KeyCode.from_vk(0xffb6) + NUMPAD7 = KeyCode.from_vk(0xffb7) + NUMPAD8 = KeyCode.from_vk(0xffb8) + NUMPAD9 = KeyCode.from_vk(0xffb9) + + # Function keys F21-F24. + # Retrieved from /usr/include/X11/keysymdef.h on Debian 9. + # These don't currently work with pynput, but doing this stops some + # warnings. + F21 = KeyCode.from_vk(0xffd1) + F22 = KeyCode.from_vk(0xffd2) + F23 = KeyCode.from_vk(0xffd3) + F24 = KeyCode.from_vk(0xffd4) + + # Multimedia keys + # Retrieved from /usr/include/X11/XF86keysym.h on Debian 9. + # These should work on Debian-based distributions like Ubunutu, but + # might not work using different X11 server implementations because the + # symbols are vendor-specific. + # Any errors raised when typing these or any other keys will be caught + # and logged. + VOLUME_UP = KeyCode.from_vk(0x1008FF13) + VOLUME_DOWN = KeyCode.from_vk(0x1008FF11) + VOLUME_MUTE = KeyCode.from_vk(0x1008FF12) + MEDIA_NEXT_TRACK = KeyCode.from_vk(0x1008FF17) + MEDIA_PREV_TRACK = KeyCode.from_vk(0x1008FF16) + MEDIA_PLAY_PAUSE = KeyCode.from_vk(0x1008FF14) + BROWSER_BACK = KeyCode.from_vk(0x1008FF26) + BROWSER_FORWARD = KeyCode.from_vk(0x1008FF27) + + +class DarwinKeySymbols(BaseKeySymbols): + """ + Symbols for Darwin from pynput. + + This class includes some extra symbols to prevent errors in + typeables.py. + + All extras will be disabled (key code of -1). + """ + + # Extra function keys. + F21 = virtual_keys.f21 + F22 = virtual_keys.f22 + F23 = virtual_keys.f23 + F24 = virtual_keys.f24 + + # Multimedia keys. + VOLUME_UP = virtual_keys.volume_up + VOLUME_DOWN = virtual_keys.volume_down + VOLUME_MUTE = virtual_keys.volume_ + MEDIA_NEXT_TRACK = virtual_keys.media_next_track + MEDIA_PREV_TRACK = virtual_keys.media_prev_track + MEDIA_PLAY_PAUSE = virtual_keys.media_play_pause + BROWSER_BACK = virtual_keys.browser_back + BROWSER_FORWARD = virtual_keys.browser_forward + + +class Keyboard(BaseKeyboard): + """Static class wrapper around pynput.keyboard.""" + + _controller = Controller() + _log = logging.getLogger("keyboard") + + @classmethod + def send_keyboard_events(cls, events): + """ + Send a sequence of keyboard events. + + Positional arguments: + events -- a sequence of tuples of the form + (keycode, down, timeout), where + keycode (str|KeyCode): pynput key code. + down (boolean): True means the key will be pressed down, + False means the key will be released. + timeout (int): number of seconds to sleep after + the keyboard event. + + """ + cls._log.debug("Keyboard.send_keyboard_events %r", events) + for event in events: + (key, down, timeout) = event + + # Raise an error if the key is unsupported. 'key' can also be a + # string, e.g. "a", "b", "/", etc, but we don't check if those + # are valid. + if isinstance(key, KeyCode) and key.vk == -1: + raise ValueError("Unsupported character: %r" % key.char) + + # Press/release the key, catching any errors. + try: + cls._controller.touch(key, down) + except Exception as e: + cls._log.exception("Failed to type key code %s: %s", + key, e) + + # Sleep after the keyboard event if necessary. + if timeout: + time.sleep(timeout) + + @classmethod + def get_typeable(cls, char, is_text=False): + return Typeable(char, is_text=is_text) diff --git a/dragonfly/actions/keyboard.py b/dragonfly/actions/keyboard/_win32.py similarity index 60% rename from dragonfly/actions/keyboard.py rename to dragonfly/actions/keyboard/_win32.py index faee5540..03974719 100644 --- a/dragonfly/actions/keyboard.py +++ b/dragonfly/actions/keyboard/_win32.py @@ -18,69 +18,118 @@ # . # -"""This file implements a Win32 keyboard interface using sendinput.""" - +"""This file implements the Win32 keyboard interface using sendinput.""" import time -from six import text_type, PY2 - import win32con from ctypes import windll, c_char, c_wchar -from dragonfly.actions.sendinput import (KeyboardInput, make_input_array, - send_input_array) - - -class Typeable(object): - """Container for keypress events.""" - - __slots__ = ("_code", "_modifiers", "_name", "_is_text") - - def __init__(self, code, modifiers=(), name=None, is_text=False): - """Set keypress information.""" - self._code = code - self._modifiers = modifiers - self._name = name - self._is_text = is_text - - def __str__(self): - """Return information useful for debugging.""" - return ("%s(%s)" % (self.__class__.__name__, self._name) + - repr(self.events())) - - def on_events(self, timeout=0): - """Return events for pressing this key down.""" - if self._is_text: - events = [(self._code, True, timeout, True)] - else: - events = [(m, True, 0) for m in self._modifiers] - events.append((self._code, True, timeout)) - return events - - def off_events(self, timeout=0): - """Return events for releasing this key.""" - if self._is_text: - events = [(self._code, False, timeout, True)] - else: - events = [(m, False, 0) for m in self._modifiers] - events.append((self._code, False, timeout)) - events.reverse() - return events - - def events(self, timeout=0): - """Return events for pressing and then releasing this key.""" - if self._is_text: - events = [(self._code, True, timeout, True), - (self._code, False, timeout, True)] - else: - events = [(self._code, True, 0), (self._code, False, timeout)] - for m in self._modifiers[-1::-1]: - events.insert(0, (m, True, 0)) - events.append((m, False, 0)) - return events - +from six import text_type, PY2 -class Keyboard(object): +from ._base import BaseKeyboard, Typeable +from ..sendinput import KeyboardInput, make_input_array, send_input_array + + +class Win32KeySymbols(object): + """ Key symbols for win32. """ + + # Whitespace and editing keys + RETURN = win32con.VK_RETURN + TAB = win32con.VK_TAB + SPACE = win32con.VK_SPACE + BACK = win32con.VK_BACK + DELETE = win32con.VK_DELETE + + # Main modifier keys + SHIFT = win32con.VK_SHIFT + CONTROL = win32con.VK_CONTROL + ALT = win32con.VK_MENU + + # Right modifier keys + RSHIFT = win32con.VK_RSHIFT + RCONTROL = win32con.VK_RCONTROL + RALT = win32con.VK_RMENU + + # Special keys + ESCAPE = win32con.VK_ESCAPE + INSERT = win32con.VK_INSERT + PAUSE = win32con.VK_PAUSE + LSUPER = win32con.VK_LWIN + RSUPER = win32con.VK_RWIN + APPS = win32con.VK_APPS + SNAPSHOT = win32con.VK_SNAPSHOT + + # Lock keys + SCROLL_LOCK = win32con.VK_SCROLL + NUM_LOCK = win32con.VK_NUMLOCK + CAPS_LOCK = win32con.VK_CAPITAL + + # Navigation keys + UP = win32con.VK_UP + DOWN = win32con.VK_DOWN + LEFT = win32con.VK_LEFT + RIGHT = win32con.VK_RIGHT + PAGE_UP = win32con.VK_PRIOR + PAGE_DOWN = win32con.VK_NEXT + HOME = win32con.VK_HOME + END = win32con.VK_END + + # Number pad keys + MULTIPLY = win32con.VK_MULTIPLY + ADD = win32con.VK_ADD + SEPARATOR = win32con.VK_SEPARATOR + SUBTRACT = win32con.VK_SUBTRACT + DECIMAL = win32con.VK_DECIMAL + DIVIDE = win32con.VK_DIVIDE + NUMPAD0 = win32con.VK_NUMPAD0 + NUMPAD1 = win32con.VK_NUMPAD1 + NUMPAD2 = win32con.VK_NUMPAD2 + NUMPAD3 = win32con.VK_NUMPAD3 + NUMPAD4 = win32con.VK_NUMPAD4 + NUMPAD5 = win32con.VK_NUMPAD5 + NUMPAD6 = win32con.VK_NUMPAD6 + NUMPAD7 = win32con.VK_NUMPAD7 + NUMPAD8 = win32con.VK_NUMPAD8 + NUMPAD9 = win32con.VK_NUMPAD9 + + # Function keys + F1 = win32con.VK_F1 + F2 = win32con.VK_F2 + F3 = win32con.VK_F3 + F4 = win32con.VK_F4 + F5 = win32con.VK_F5 + F6 = win32con.VK_F6 + F7 = win32con.VK_F7 + F8 = win32con.VK_F8 + F9 = win32con.VK_F9 + F10 = win32con.VK_F10 + F11 = win32con.VK_F11 + F12 = win32con.VK_F12 + F13 = win32con.VK_F13 + F14 = win32con.VK_F14 + F15 = win32con.VK_F15 + F16 = win32con.VK_F16 + F17 = win32con.VK_F17 + F18 = win32con.VK_F18 + F19 = win32con.VK_F19 + F20 = win32con.VK_F20 + F21 = win32con.VK_F21 + F22 = win32con.VK_F22 + F23 = win32con.VK_F23 + F24 = win32con.VK_F24 + + # Multimedia keys + VOLUME_UP = win32con.VK_VOLUME_UP + VOLUME_DOWN = win32con.VK_VOLUME_DOWN + VOLUME_MUTE = win32con.VK_VOLUME_MUTE + MEDIA_NEXT_TRACK = win32con.VK_MEDIA_NEXT_TRACK + MEDIA_PREV_TRACK = win32con.VK_MEDIA_PREV_TRACK + MEDIA_PLAY_PAUSE = win32con.VK_MEDIA_PLAY_PAUSE + BROWSER_BACK = win32con.VK_BROWSER_BACK + BROWSER_FORWARD = win32con.VK_BROWSER_FORWARD + + +class Keyboard(BaseKeyboard): """Static class wrapper around SendInput.""" shift_code = win32con.VK_SHIFT @@ -171,6 +220,3 @@ def get_typeable(cls, char, is_text=False): return Typeable(char, is_text=True) code, modifiers = cls.get_keycode_and_modifiers(char) return Typeable(code, modifiers) - - -keyboard = Keyboard() diff --git a/dragonfly/actions/typeables.py b/dragonfly/actions/typeables.py index 47d49bb8..53fafe90 100644 --- a/dragonfly/actions/typeables.py +++ b/dragonfly/actions/typeables.py @@ -30,17 +30,18 @@ """ import logging -import win32con -from dragonfly.actions.keyboard import keyboard, Typeable -logging.basicConfig() -_log = logging.getLogger("actions.typeables") +from .keyboard import keyboard, Typeable, KeySymbols + + +_log = logging.getLogger("typeables") # -------------------------------------------------------------------------- # Mapping of name -> typeable. typeables = {} +key_symbols = KeySymbols() def _add_typeable(name, char): @@ -184,7 +185,7 @@ def _add_typeable(name, char): # Symbol keys # All symbols can be referred to by their printable representation. -# Reserved characters for the Key action spec -,:/ are still typeables, +# Reserved characters for the Key action spec -,:/ are Typeable objects, # but Key requires use of the longer character names. _add_typeable(name="!", char='!') _add_typeable(name="bang", char='!') @@ -272,119 +273,119 @@ def _add_typeable(name, char): typeables.update({ # Whitespace and editing keys - "enter": Typeable(code=win32con.VK_RETURN, name='enter'), - "tab": Typeable(code=win32con.VK_TAB, name='tab'), - "space": Typeable(code=win32con.VK_SPACE, name='space'), - "backspace": Typeable(code=win32con.VK_BACK, name='backspace'), - "delete": Typeable(code=win32con.VK_DELETE, name='delete'), - "del": Typeable(code=win32con.VK_DELETE, name='del'), + "enter": Typeable(code=key_symbols.RETURN, name='enter'), + "tab": Typeable(code=key_symbols.TAB, name='tab'), + "space": Typeable(code=key_symbols.SPACE, name='space'), + "backspace": Typeable(code=key_symbols.BACK, name='backspace'), + "delete": Typeable(code=key_symbols.DELETE, name='delete'), + "del": Typeable(code=key_symbols.DELETE, name='del'), # Main modifier keys - "shift": Typeable(code=win32con.VK_SHIFT, name='shift'), - "control": Typeable(code=win32con.VK_CONTROL, name='control'), - "ctrl": Typeable(code=win32con.VK_CONTROL, name='ctrl'), - "alt": Typeable(code=win32con.VK_MENU, name='alt'), + "shift": Typeable(code=key_symbols.SHIFT, name='shift'), + "control": Typeable(code=key_symbols.CONTROL, name='control'), + "ctrl": Typeable(code=key_symbols.CONTROL, name='ctrl'), + "alt": Typeable(code=key_symbols.ALT, name='alt'), # Right modifier keys - "rshift": Typeable(code=win32con.VK_RSHIFT, name='rshift'), - "rcontrol": Typeable(code=win32con.VK_RCONTROL, name='rcontrol'), - "rctrl": Typeable(code=win32con.VK_RCONTROL, name='rctrl'), - "ralt": Typeable(code=win32con.VK_RMENU, name='ralt'), + "rshift": Typeable(code=key_symbols.RSHIFT, name='rshift'), + "rcontrol": Typeable(code=key_symbols.RCONTROL, name='rcontrol'), + "rctrl": Typeable(code=key_symbols.RCONTROL, name='rctrl'), + "ralt": Typeable(code=key_symbols.RALT, name='ralt'), # Special keys - "escape": Typeable(code=win32con.VK_ESCAPE, name='escape'), - "insert": Typeable(code=win32con.VK_INSERT, name='insert'), - "pause": Typeable(code=win32con.VK_PAUSE, name='pause'), - "win": Typeable(code=win32con.VK_LWIN, name='win'), - "rwin": Typeable(code=win32con.VK_RWIN, name='rwin'), - "apps": Typeable(code=win32con.VK_APPS, name='apps'), - "popup": Typeable(code=win32con.VK_APPS, name='popup'), - "snapshot": Typeable(code=win32con.VK_SNAPSHOT, name='snapshot'), - "printscreen": Typeable(code=win32con.VK_SNAPSHOT, name='printscreen'), + "escape": Typeable(code=key_symbols.ESCAPE, name='escape'), + "insert": Typeable(code=key_symbols.INSERT, name='insert'), + "pause": Typeable(code=key_symbols.PAUSE, name='pause'), + "win": Typeable(code=key_symbols.LSUPER, name='win'), + "rwin": Typeable(code=key_symbols.RSUPER, name='rwin'), + "apps": Typeable(code=key_symbols.APPS, name='apps'), + "popup": Typeable(code=key_symbols.APPS, name='popup'), + "snapshot": Typeable(code=key_symbols.SNAPSHOT, name='snapshot'), + "printscreen": Typeable(code=key_symbols.SNAPSHOT, name='printscreen'), # Lock keys # win32api.GetKeyState(code) could be used to toggle lock keys sensibly # instead of using the up/down modifiers. - "scrolllock": Typeable(code=win32con.VK_SCROLL, name='scrolllock'), - "numlock": Typeable(code=win32con.VK_NUMLOCK, name='numlock'), - "capslock": Typeable(code=win32con.VK_CAPITAL, name='capslock'), + "scrolllock": Typeable(code=key_symbols.SCROLL_LOCK, name='scrolllock'), + "numlock": Typeable(code=key_symbols.NUM_LOCK, name='numlock'), + "capslock": Typeable(code=key_symbols.CAPS_LOCK, name='capslock'), # Navigation keys - "up": Typeable(code=win32con.VK_UP, name='up'), - "down": Typeable(code=win32con.VK_DOWN, name='down'), - "left": Typeable(code=win32con.VK_LEFT, name='left'), - "right": Typeable(code=win32con.VK_RIGHT, name='right'), - "pageup": Typeable(code=win32con.VK_PRIOR, name='pageup'), - "pgup": Typeable(code=win32con.VK_PRIOR, name='pgup'), - "pagedown": Typeable(code=win32con.VK_NEXT, name='pagedown'), - "pgdown": Typeable(code=win32con.VK_NEXT, name='pgdown'), - "home": Typeable(code=win32con.VK_HOME, name='home'), - "end": Typeable(code=win32con.VK_END, name='end'), + "up": Typeable(code=key_symbols.UP, name='up'), + "down": Typeable(code=key_symbols.DOWN, name='down'), + "left": Typeable(code=key_symbols.LEFT, name='left'), + "right": Typeable(code=key_symbols.RIGHT, name='right'), + "pageup": Typeable(code=key_symbols.PAGE_UP, name='pageup'), + "pgup": Typeable(code=key_symbols.PAGE_UP, name='pgup'), + "pagedown": Typeable(code=key_symbols.PAGE_DOWN, name='pagedown'), + "pgdown": Typeable(code=key_symbols.PAGE_DOWN, name='pgdown'), + "home": Typeable(code=key_symbols.HOME, name='home'), + "end": Typeable(code=key_symbols.END, name='end'), # Number pad keys - "npmul": Typeable(code=win32con.VK_MULTIPLY, name='npmul'), - "npadd": Typeable(code=win32con.VK_ADD, name='npadd'), - "npsep": Typeable(code=win32con.VK_SEPARATOR, name='npsep'), - "npsub": Typeable(code=win32con.VK_SUBTRACT, name='npsub'), - "npdec": Typeable(code=win32con.VK_DECIMAL, name='npdec'), - "npdiv": Typeable(code=win32con.VK_DIVIDE, name='npdiv'), - "numpad0": Typeable(code=win32con.VK_NUMPAD0, name='numpad0'), - "np0": Typeable(code=win32con.VK_NUMPAD0, name='np0'), - "numpad1": Typeable(code=win32con.VK_NUMPAD1, name='numpad1'), - "np1": Typeable(code=win32con.VK_NUMPAD1, name='np1'), - "numpad2": Typeable(code=win32con.VK_NUMPAD2, name='numpad2'), - "np2": Typeable(code=win32con.VK_NUMPAD2, name='np2'), - "numpad3": Typeable(code=win32con.VK_NUMPAD3, name='numpad3'), - "np3": Typeable(code=win32con.VK_NUMPAD3, name='np3'), - "numpad4": Typeable(code=win32con.VK_NUMPAD4, name='numpad4'), - "np4": Typeable(code=win32con.VK_NUMPAD4, name='np4'), - "numpad5": Typeable(code=win32con.VK_NUMPAD5, name='numpad5'), - "np5": Typeable(code=win32con.VK_NUMPAD5, name='np5'), - "numpad6": Typeable(code=win32con.VK_NUMPAD6, name='numpad6'), - "np6": Typeable(code=win32con.VK_NUMPAD6, name='np6'), - "numpad7": Typeable(code=win32con.VK_NUMPAD7, name='numpad7'), - "np7": Typeable(code=win32con.VK_NUMPAD7, name='np7'), - "numpad8": Typeable(code=win32con.VK_NUMPAD8, name='numpad8'), - "np8": Typeable(code=win32con.VK_NUMPAD8, name='np8'), - "numpad9": Typeable(code=win32con.VK_NUMPAD9, name='numpad9'), - "np9": Typeable(code=win32con.VK_NUMPAD9, name='np9'), + "npmul": Typeable(code=key_symbols.MULTIPLY, name='npmul'), + "npadd": Typeable(code=key_symbols.ADD, name='npadd'), + "npsep": Typeable(code=key_symbols.SEPARATOR, name='npsep'), + "npsub": Typeable(code=key_symbols.SUBTRACT, name='npsub'), + "npdec": Typeable(code=key_symbols.DECIMAL, name='npdec'), + "npdiv": Typeable(code=key_symbols.DIVIDE, name='npdiv'), + "numpad0": Typeable(code=key_symbols.NUMPAD0, name='numpad0'), + "np0": Typeable(code=key_symbols.NUMPAD0, name='np0'), + "numpad1": Typeable(code=key_symbols.NUMPAD1, name='numpad1'), + "np1": Typeable(code=key_symbols.NUMPAD1, name='np1'), + "numpad2": Typeable(code=key_symbols.NUMPAD2, name='numpad2'), + "np2": Typeable(code=key_symbols.NUMPAD2, name='np2'), + "numpad3": Typeable(code=key_symbols.NUMPAD3, name='numpad3'), + "np3": Typeable(code=key_symbols.NUMPAD3, name='np3'), + "numpad4": Typeable(code=key_symbols.NUMPAD4, name='numpad4'), + "np4": Typeable(code=key_symbols.NUMPAD4, name='np4'), + "numpad5": Typeable(code=key_symbols.NUMPAD5, name='numpad5'), + "np5": Typeable(code=key_symbols.NUMPAD5, name='np5'), + "numpad6": Typeable(code=key_symbols.NUMPAD6, name='numpad6'), + "np6": Typeable(code=key_symbols.NUMPAD6, name='np6'), + "numpad7": Typeable(code=key_symbols.NUMPAD7, name='numpad7'), + "np7": Typeable(code=key_symbols.NUMPAD7, name='np7'), + "numpad8": Typeable(code=key_symbols.NUMPAD8, name='numpad8'), + "np8": Typeable(code=key_symbols.NUMPAD8, name='np8'), + "numpad9": Typeable(code=key_symbols.NUMPAD9, name='numpad9'), + "np9": Typeable(code=key_symbols.NUMPAD9, name='np9'), # Function keys - "f1": Typeable(code=win32con.VK_F1, name='f1'), - "f2": Typeable(code=win32con.VK_F2, name='f2'), - "f3": Typeable(code=win32con.VK_F3, name='f3'), - "f4": Typeable(code=win32con.VK_F4, name='f4'), - "f5": Typeable(code=win32con.VK_F5, name='f5'), - "f6": Typeable(code=win32con.VK_F6, name='f6'), - "f7": Typeable(code=win32con.VK_F7, name='f7'), - "f8": Typeable(code=win32con.VK_F8, name='f8'), - "f9": Typeable(code=win32con.VK_F9, name='f9'), - "f10": Typeable(code=win32con.VK_F10, name='f10'), - "f11": Typeable(code=win32con.VK_F11, name='f11'), - "f12": Typeable(code=win32con.VK_F12, name='f12'), - "f13": Typeable(code=win32con.VK_F13, name='f13'), - "f14": Typeable(code=win32con.VK_F14, name='f14'), - "f15": Typeable(code=win32con.VK_F15, name='f15'), - "f16": Typeable(code=win32con.VK_F16, name='f16'), - "f17": Typeable(code=win32con.VK_F17, name='f17'), - "f18": Typeable(code=win32con.VK_F18, name='f18'), - "f19": Typeable(code=win32con.VK_F19, name='f19'), - "f20": Typeable(code=win32con.VK_F20, name='f20'), - "f21": Typeable(code=win32con.VK_F21, name='f21'), - "f22": Typeable(code=win32con.VK_F22, name='f22'), - "f23": Typeable(code=win32con.VK_F23, name='f23'), - "f24": Typeable(code=win32con.VK_F24, name='f24'), + "f1": Typeable(code=key_symbols.F1, name='f1'), + "f2": Typeable(code=key_symbols.F2, name='f2'), + "f3": Typeable(code=key_symbols.F3, name='f3'), + "f4": Typeable(code=key_symbols.F4, name='f4'), + "f5": Typeable(code=key_symbols.F5, name='f5'), + "f6": Typeable(code=key_symbols.F6, name='f6'), + "f7": Typeable(code=key_symbols.F7, name='f7'), + "f8": Typeable(code=key_symbols.F8, name='f8'), + "f9": Typeable(code=key_symbols.F9, name='f9'), + "f10": Typeable(code=key_symbols.F10, name='f10'), + "f11": Typeable(code=key_symbols.F11, name='f11'), + "f12": Typeable(code=key_symbols.F12, name='f12'), + "f13": Typeable(code=key_symbols.F13, name='f13'), + "f14": Typeable(code=key_symbols.F14, name='f14'), + "f15": Typeable(code=key_symbols.F15, name='f15'), + "f16": Typeable(code=key_symbols.F16, name='f16'), + "f17": Typeable(code=key_symbols.F17, name='f17'), + "f18": Typeable(code=key_symbols.F18, name='f18'), + "f19": Typeable(code=key_symbols.F19, name='f19'), + "f20": Typeable(code=key_symbols.F20, name='f20'), + "f21": Typeable(code=key_symbols.F21, name='f21'), + "f22": Typeable(code=key_symbols.F22, name='f22'), + "f23": Typeable(code=key_symbols.F23, name='f23'), + "f24": Typeable(code=key_symbols.F24, name='f24'), # Multimedia keys - "volumeup": Typeable(code=win32con.VK_VOLUME_UP, name='volumeup'), - "volup": Typeable(code=win32con.VK_VOLUME_UP, name='volup'), - "volumedown": Typeable(code=win32con.VK_VOLUME_DOWN, name='volumedown'), - "voldown": Typeable(code=win32con.VK_VOLUME_DOWN, name='voldown'), - "volumemute": Typeable(code=win32con.VK_VOLUME_MUTE, name='volumemute'), - "volmute": Typeable(code=win32con.VK_VOLUME_MUTE, name='volmute'), - "tracknext": Typeable(code=win32con.VK_MEDIA_NEXT_TRACK, name='tracknext'), - "trackprev": Typeable(code=win32con.VK_MEDIA_PREV_TRACK, name='trackprev'), - "playpause": Typeable(code=win32con.VK_MEDIA_PLAY_PAUSE, name='playpause'), - "browserback": Typeable(code=win32con.VK_BROWSER_BACK, name='browserback'), - "browserforward": Typeable(code=win32con.VK_BROWSER_FORWARD, name='browserforward'), + "volumeup": Typeable(code=key_symbols.VOLUME_UP, name='volumeup'), + "volup": Typeable(code=key_symbols.VOLUME_UP, name='volup'), + "volumedown": Typeable(code=key_symbols.VOLUME_DOWN, name='volumedown'), + "voldown": Typeable(code=key_symbols.VOLUME_DOWN, name='voldown'), + "volumemute": Typeable(code=key_symbols.VOLUME_MUTE, name='volumemute'), + "volmute": Typeable(code=key_symbols.VOLUME_MUTE, name='volmute'), + "tracknext": Typeable(code=key_symbols.MEDIA_NEXT_TRACK, name='tracknext'), + "trackprev": Typeable(code=key_symbols.MEDIA_PREV_TRACK, name='trackprev'), + "playpause": Typeable(code=key_symbols.MEDIA_PLAY_PAUSE, name='playpause'), + "browserback": Typeable(code=key_symbols.BROWSER_BACK, name='browserback'), + "browserforward": Typeable(code=key_symbols.BROWSER_FORWARD, name='browserforward'), }) diff --git a/dragonfly/log.py b/dragonfly/log.py index dc3e625e..cffbac2e 100644 --- a/dragonfly/log.py +++ b/dragonfly/log.py @@ -63,6 +63,8 @@ "monitor.init": (_warning, _info), "dfly.test": (_debug, _debug), "accessibility": (_info, _info), + "keyboard": (_warning, _warning), + "typeables": (_warning, _warning), } diff --git a/dragonfly/os_dependent_mock.py b/dragonfly/os_dependent_mock.py index 535bdfc3..8e146709 100755 --- a/dragonfly/os_dependent_mock.py +++ b/dragonfly/os_dependent_mock.py @@ -32,10 +32,9 @@ def mock_action(*args, **kwargs): def mock_dyn_str_action(*args, **kwargs): return DynStrActionBase(*args, **kwargs) -Text = mock_dyn_str_action -Key = mock_dyn_str_action -Mouse = mock_dyn_str_action + Paste = mock_dyn_str_action +Mouse = mock_dyn_str_action WaitWindow = mock_action FocusWindow = mock_action StartApp = mock_action @@ -44,7 +43,6 @@ def mock_dyn_str_action(*args, **kwargs): class _WindowInfo(object): - # TODO Use proxy contexts instead executable = "" title = "" handle = "" @@ -65,10 +63,6 @@ class HardwareInput(MockBase): pass -class Keyboard(MockBase): - pass - - class KeyboardInput(MockBase): pass @@ -84,12 +78,6 @@ class MouseInput(MockBase): pass -class Typeable(object): - pass - -typeables = {} - - def make_input_array(inputs): return inputs diff --git a/dragonfly/windows/__init__.py b/dragonfly/windows/__init__.py index ad6fc011..df8d885e 100644 --- a/dragonfly/windows/__init__.py +++ b/dragonfly/windows/__init__.py @@ -20,7 +20,6 @@ import sys -# OS agnostic imports from .rectangle import Rectangle, unit from .point import Point diff --git a/setup.py b/setup.py index f1dfdf22..fa38d57f 100644 --- a/setup.py +++ b/setup.py @@ -62,6 +62,7 @@ def read(*names): "setuptools >= 0.6c7", "comtypes;platform_system=='Windows'", "pywin32;platform_system=='Windows'", + "pynput >= 1.4.2;platform_system!='Windows'", "six", "pyperclip >= 1.7.0", "enum34;python_version<'3.4'", From 0b21fb55b37044493373c2364b5241c9b674aa7b Mon Sep 17 00:00:00 2001 From: Dane Finlay Date: Sun, 14 Apr 2019 19:24:26 +1000 Subject: [PATCH 2/8] Fix small problem with the Key action Python 2.7 raises errors in 'except' blocks as expected, but Python 3 doesn't. This commit changes Key to check if the typeable exists using 'get()' instead of catching a KeyError. --- dragonfly/actions/action_key.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dragonfly/actions/action_key.py b/dragonfly/actions/action_key.py index bfc3b94d..28446ceb 100644 --- a/dragonfly/actions/action_key.py +++ b/dragonfly/actions/action_key.py @@ -294,12 +294,10 @@ def _parse_single(self, spec): else: raise ActionError("Invalid key spec: %s" % spec) - try: - code = typeables[keyname] - except KeyError: + code = typeables.get(keyname) + if code is None: raise ActionError("Invalid key name: %r" % keyname) - if inner_pause is not None: s = inner_pause try: From 05cb280463fe2b77f61699e6032b9d4f6363c632 Mon Sep 17 00:00:00 2001 From: Dane Finlay Date: Sun, 14 Apr 2019 19:40:15 +1000 Subject: [PATCH 3/8] Unmock the Paste action and allow it to work with other platforms This is possible now that the Key and Text actions are working. The system clipboard is used through the pyperclip Clipboard class on Mac OS and X11. This commit also fixes a bug where None can be passed to pyperclip.copy() and raise an error. --- dragonfly/actions/__init__.py | 3 +-- dragonfly/actions/action_paste.py | 38 +++++++++++++++++++++++-------- dragonfly/actions/actions.py | 3 +-- dragonfly/util/clipboard.py | 2 ++ 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/dragonfly/actions/__init__.py b/dragonfly/actions/__init__.py index 6ec6a442..d9710855 100644 --- a/dragonfly/actions/__init__.py +++ b/dragonfly/actions/__init__.py @@ -33,10 +33,10 @@ from .typeables import typeables from .action_key import Key from .action_text import Text +from .action_paste import Paste if sys.platform.startswith("win"): # Import Windows only classes and functions. - from .action_paste import Paste from .action_mouse import Mouse from .action_waitwindow import WaitWindow from .action_focuswindow import FocusWindow @@ -46,7 +46,6 @@ make_input_array, send_input_array) else: # Import mocked classes and functions for other platforms. - from ..os_dependent_mock import Paste from ..os_dependent_mock import Mouse from ..os_dependent_mock import WaitWindow from ..os_dependent_mock import FocusWindow diff --git a/dragonfly/actions/action_paste.py b/dragonfly/actions/action_paste.py index c6e2e801..ca44f820 100644 --- a/dragonfly/actions/action_paste.py +++ b/dragonfly/actions/action_paste.py @@ -24,15 +24,22 @@ """ +import sys + from six import text_type, string_types, PY2 from ..actions.action_base import DynStrActionBase from ..actions.action_key import Key -from ..windows.clipboard import Clipboard -from win32con import CF_UNICODETEXT, CF_TEXT -import pywintypes +if sys.platform.startswith("win"): + from ..windows.clipboard import Clipboard +else: + from ..util import Clipboard + +# Define some win32 constants so that this module can work on other +# platforms. +CF_UNICODETEXT, CF_TEXT = 13, 1 #--------------------------------------------------------------------------- @@ -54,20 +61,28 @@ class Paste(DynStrActionBase): This action inserts the given *contents* into the Windows system clipboard, and then performs the *paste* action to paste it into the foreground application. By default, the *paste* action is the - :kbd:`Shift-insert` keystroke. The default clipboard format to use - is the *Unicode* text format. + :kbd:`Ctrl-v` keystroke or :kbd`Super-v` on a mac. The default + clipboard format to use is the *Unicode* text format. + + Clipboard formats are not used if not running on Windows. """ _default_format = CF_UNICODETEXT # Default paste action. - _default_paste = Key("s-insert/20") + # Fallback on Shift-insert if 'v' isn't available. Use Super-v on macs. + try: + _default_paste = (Key("w-v/20") if sys.platform == "darwin" + else Key("c-v/20")) + except: + print("Falling back to Shift+insert for Paste's action.") + _default_paste = Key("s-insert/20") def __init__(self, contents, format=None, paste=None, static=False): if not format: format = self._default_format - if not paste: + if paste is None: paste = self._default_paste if isinstance(contents, string_types): spec = contents @@ -89,13 +104,16 @@ def _execute_events(self, events): original = Clipboard() try: original.copy_from_system() - except pywintypes.error as e: + except Exception as e: self._log.warning("Failed to store original clipboard contents:" - " %s" % e) + " %s", e) if (self.format == CF_UNICODETEXT and not isinstance(events, text_type)): if PY2: - events = text_type(events, encoding='windows-1252', + # Use a Unicode object with the correct encoding. + on_windows = sys.platform.startswith("win32") + encoding = 'windows-1252' if on_windows else 'utf-8' + events = text_type(events, encoding=encoding, errors='ignore') else: events = text_type(events) diff --git a/dragonfly/actions/actions.py b/dragonfly/actions/actions.py index 8db0a6c8..d99de622 100644 --- a/dragonfly/actions/actions.py +++ b/dragonfly/actions/actions.py @@ -35,10 +35,10 @@ from .keyboard import Keyboard, Typeable from .action_key import Key from .action_text import Text +from .action_paste import Paste if sys.platform.startswith("win"): # Import Windows only classes and functions. - from .action_paste import Paste from .action_mouse import Mouse from .action_waitwindow import WaitWindow from .action_focuswindow import FocusWindow @@ -46,7 +46,6 @@ from .action_playsound import PlaySound else: # Import mocked classes and functions for other platforms. - from ..os_dependent_mock import Paste from ..os_dependent_mock import Mouse from ..os_dependent_mock import WaitWindow from ..os_dependent_mock import FocusWindow diff --git a/dragonfly/util/clipboard.py b/dragonfly/util/clipboard.py index 716078b3..9a4375db 100644 --- a/dragonfly/util/clipboard.py +++ b/dragonfly/util/clipboard.py @@ -69,6 +69,8 @@ def get_system_text(cls): @classmethod def set_system_text(cls, content): + if not content: + content = "" pyperclip.copy(content) @classmethod From 5e68e8f3bb7f561d54e9de38047362abc5b61c25 Mon Sep 17 00:00:00 2001 From: Dane Finlay Date: Sun, 14 Apr 2019 20:11:19 +1000 Subject: [PATCH 4/8] Fix mistake in DarwinKeySymbols class --- dragonfly/actions/keyboard/_pynput.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dragonfly/actions/keyboard/_pynput.py b/dragonfly/actions/keyboard/_pynput.py index 683e2e16..bd705e24 100644 --- a/dragonfly/actions/keyboard/_pynput.py +++ b/dragonfly/actions/keyboard/_pynput.py @@ -220,7 +220,7 @@ class DarwinKeySymbols(BaseKeySymbols): # Multimedia keys. VOLUME_UP = virtual_keys.volume_up VOLUME_DOWN = virtual_keys.volume_down - VOLUME_MUTE = virtual_keys.volume_ + VOLUME_MUTE = virtual_keys.volume_mute MEDIA_NEXT_TRACK = virtual_keys.media_next_track MEDIA_PREV_TRACK = virtual_keys.media_prev_track MEDIA_PLAY_PAUSE = virtual_keys.media_play_pause From 22e173e4bb20756e97eb59eeaf26a89e830659e2 Mon Sep 17 00:00:00 2001 From: Dane Finlay Date: Wed, 17 Apr 2019 16:08:08 +1000 Subject: [PATCH 5/8] Add script for testing the Key and Text actions on X11 using xev The "xev" program is used to capture key presses, so it must be installed. This script is not a unit test file. --- dragonfly/actions/_test_x11_text_key.py | 129 ++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 dragonfly/actions/_test_x11_text_key.py diff --git a/dragonfly/actions/_test_x11_text_key.py b/dragonfly/actions/_test_x11_text_key.py new file mode 100644 index 00000000..8cd7d81b --- /dev/null +++ b/dragonfly/actions/_test_x11_text_key.py @@ -0,0 +1,129 @@ +#!/usr/bin/python + +""" +Script to test the Key and Text action on X11 using the "xev" program. + +There should be two lines printed for each key press: a down event and +an up event. Each line will contain the key symbol (e.g. Return). +There will be a small delay per key press for both test actions to help view +the output from "xev" as the keys are pressed. + +X11Errors may be logged for keys remapped or not mapped by xkb. +""" + +from __future__ import print_function + +import subprocess +import sys +import threading +import time + +from dragonfly import Key, Text + +XEV_PATH = "/usr/bin/xev" + + +def main(): + # Define keys to type. + keys = [] + alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + digits = "0123456789" + keys.extend(alphas) + keys.extend(digits) + + # Add common symbol characters. Use long names for reserved characters. + symbols = "!@#%^&*()_+`~[]{}|\\;'\".<>?=" + keys.extend(symbols) + keys.extend(["comma", "colon", "minus", "slash", "space"]) + + # Add virtual keys. Some keys are repeated purposefully to maintain key + # state (e.g. caps lock). + keys.extend([ + # Whitespace and editing keys + "enter", "tab", "space", "backspace", "delete", + + # Special keys + "escape", "insert", "insert", "pause", "apps", + + # Lock keys + "scrolllock", "scrolllock", "numlock", "numlock", "capslock", + "capslock", + + # Navigation keys + "up", "down", "left", "right", "pageup", "pagedown", "home", "end", + + # Number pad keys + "npmul", "npadd", "npsep", "npsub", "npdec", "npdiv", "np0", "np1", + "np2", "np3", "np4", "np5", "np6", "np7", "np8", "np9", + + # Function keys (not including F13-24) + "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", + "f12", + + # Multimedia keys (press toggle keys twice) + "volumeup", "volup", "volumedown", "voldown", "volumemute", + "volmute", "tracknext", "trackprev", "playpause", "playpause", + "browserback", "browserforward", + ]) + + # Add modifiers. + keys.extend([ + "shift:down", "shift:up", + "ctrl:down", "ctrl:up", + "alt:down", "alt:up", + "rshift:down", "rshift:up", + "rctrl:down", "rctrl:up", + "ralt:down", "ralt:up", + "win:down", "win:up", "rwin:down", "rwin:up" + ]) + + # Add some common key combinations. + keys.extend([ + "s-insert", + "c-left", + "c-home", + "a-f", + "c-a", + ]) + + # Define text to type using all alphanumeric characters and valid + # symbols. + text = alphas + digits + symbols + ",:-/ \n\t" + + # Start xev and selectively print lines from it in the background. + # Exit if the command fails. + try: + proc = subprocess.Popen(XEV_PATH, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE) + def print_key_events(): + for line in iter(proc.stdout.readline, b''): + if "keysym" in line: + print(line, end='') + + t = threading.Thread(target=print_key_events) + t.setDaemon(True) # make this thread terminate with the main thread + t.start() + except Exception as e: + print("Couldn't start xev: %s" % e, file=sys.stderr) + exit(1) + + # Wait a few seconds before beginning. + print("Please ensure xev is focused...") + time.sleep(5) + + print("--------------------Starting Key tests--------------------") + Key("/10,".join(keys)).execute() + print("Done.") + + print("--------------------Starting Text tests-------------------") + Text(text, pause=0.1).execute() + print("Done.") + + print("Stopping xev.") + proc.terminate() + + +if __name__ == '__main__': + main() From 8c6df0ca31f5ee34231389c95752409b0c4294b9 Mon Sep 17 00:00:00 2001 From: Dane Finlay Date: Fri, 19 Apr 2019 21:17:59 +1000 Subject: [PATCH 6/8] Fix a few problems with the X11 action test script - Press a few more keys twice: F11, Super_L and Super_R. - Decode lines from 'xev' before using them. - Remove the "npsep" key because it sometimes doesn't work. --- dragonfly/actions/_test_x11_text_key.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/dragonfly/actions/_test_x11_text_key.py b/dragonfly/actions/_test_x11_text_key.py index 8cd7d81b..3bc7cfbe 100644 --- a/dragonfly/actions/_test_x11_text_key.py +++ b/dragonfly/actions/_test_x11_text_key.py @@ -52,33 +52,30 @@ def main(): # Navigation keys "up", "down", "left", "right", "pageup", "pagedown", "home", "end", - # Number pad keys - "npmul", "npadd", "npsep", "npsub", "npdec", "npdiv", "np0", "np1", + # Number pad keys (except npsep) + "npmul", "npadd", "npsub", "npdec", "npdiv", "np0", "np1", "np2", "np3", "np4", "np5", "np6", "np7", "np8", "np9", # Function keys (not including F13-24) "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", - "f12", + "f11", "f12", # Multimedia keys (press toggle keys twice) "volumeup", "volup", "volumedown", "voldown", "volumemute", "volmute", "tracknext", "trackprev", "playpause", "playpause", "browserback", "browserforward", - ]) - # Add modifiers. - keys.extend([ + # Modifiers. "shift:down", "shift:up", "ctrl:down", "ctrl:up", "alt:down", "alt:up", "rshift:down", "rshift:up", "rctrl:down", "rctrl:up", "ralt:down", "ralt:up", - "win:down", "win:up", "rwin:down", "rwin:up" - ]) + "win:down", "win:up", "win:down", "win:up", + "rwin:down", "rwin:up", "rwin:down", "rwin:up", - # Add some common key combinations. - keys.extend([ + # Some common key combinations. "s-insert", "c-left", "c-home", @@ -99,8 +96,9 @@ def main(): stdin=subprocess.PIPE) def print_key_events(): for line in iter(proc.stdout.readline, b''): + line = line.decode() if "keysym" in line: - print(line, end='') + print(line.strip()) t = threading.Thread(target=print_key_events) t.setDaemon(True) # make this thread terminate with the main thread From db2c307ed952716c8785e76c645c69de75f5c24a Mon Sep 17 00:00:00 2001 From: Dane Finlay Date: Fri, 19 Apr 2019 21:26:00 +1000 Subject: [PATCH 7/8] Fix some comments and an error message in keyboard/_pynput.py --- dragonfly/actions/keyboard/_pynput.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dragonfly/actions/keyboard/_pynput.py b/dragonfly/actions/keyboard/_pynput.py index bd705e24..69c198df 100644 --- a/dragonfly/actions/keyboard/_pynput.py +++ b/dragonfly/actions/keyboard/_pynput.py @@ -125,7 +125,8 @@ class BaseKeySymbols(object): NUMPAD9 = KeyCode(char="9") # Function keys - # F13-20 don't work on X11 with pynput due to a bug. + # F13-20 don't work on X11 with pynput because they are not usually + # part of the keyboard map. F1 = virtual_keys.f1 F2 = virtual_keys.f2 F3 = virtual_keys.f3 @@ -177,8 +178,9 @@ class X11KeySymbols(BaseKeySymbols): # Function keys F21-F24. # Retrieved from /usr/include/X11/keysymdef.h on Debian 9. - # These don't currently work with pynput, but doing this stops some - # warnings. + # These keys don't work on X11 with pynput because they are not usually + # part of the keyboard map. They are set here to avoid some warnings + # and because the Windows keyboard supports them. F21 = KeyCode.from_vk(0xffd1) F22 = KeyCode.from_vk(0xffd2) F23 = KeyCode.from_vk(0xffd3) @@ -257,7 +259,7 @@ def send_keyboard_events(cls, events): # string, e.g. "a", "b", "/", etc, but we don't check if those # are valid. if isinstance(key, KeyCode) and key.vk == -1: - raise ValueError("Unsupported character: %r" % key.char) + raise ValueError("Unsupported key: %r" % key.char) # Press/release the key, catching any errors. try: From 0fadf169d96b72557bee7182c7d5f49b5c4f4589 Mon Sep 17 00:00:00 2001 From: Dane Finlay Date: Tue, 23 Apr 2019 01:58:33 +1000 Subject: [PATCH 8/8] Fix two issues with dragonfly's mocked actions - Make each mocked action refer to a class instead. - Replace mock_dyn_str_action with a mocked Mouse class, as that action is the only DynStrActionBase sub-class left that isn't implemented for other platforms. --- dragonfly/os_dependent_mock.py | 35 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/dragonfly/os_dependent_mock.py b/dragonfly/os_dependent_mock.py index 8e146709..828f898f 100755 --- a/dragonfly/os_dependent_mock.py +++ b/dragonfly/os_dependent_mock.py @@ -20,26 +20,32 @@ Heavily modified to allow more dragonfly functionality to work regardless of operating system. """ + from .actions import ActionBase, DynStrActionBase -# Mock ActionBase and DynStrActionBase classes +class MockBase(object): + def __init__(self, *args, **kwargs): + pass + -def mock_action(*args, **kwargs): - return ActionBase() +class MockAction(ActionBase): + """ Mock class for dragonfly actions. """ + def __init__(self, *args, **kwargs): + ActionBase.__init__(self) -def mock_dyn_str_action(*args, **kwargs): - return DynStrActionBase(*args, **kwargs) +class Mouse(DynStrActionBase): + """ Mock Mouse action class. """ + def __init__(self, spec=None, static=False): + DynStrActionBase.__init__(self, spec, static) -Paste = mock_dyn_str_action -Mouse = mock_dyn_str_action -WaitWindow = mock_action -FocusWindow = mock_action -StartApp = mock_action -BringApp = mock_action -PlaySound = mock_action +WaitWindow = MockAction +FocusWindow = MockAction +StartApp = MockAction +BringApp = MockAction +PlaySound = MockAction class _WindowInfo(object): @@ -54,11 +60,6 @@ def get_foreground(): return _WindowInfo -class MockBase(object): - def __init__(self, *args, **kwargs): - pass - - class HardwareInput(MockBase): pass