Skip to content

Commit

Permalink
Merge pull request #336 from dictation-toolbox/improve-win32-kbd
Browse files Browse the repository at this point in the history
Improve Windows keyboard action support for letter and number keys
  • Loading branch information
drmfinlay committed Apr 30, 2021
2 parents 8cbfece + 6e91e6c commit ef70d66
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 29 deletions.
28 changes: 17 additions & 11 deletions dragonfly/actions/action_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
Key("shift:down, down/25:4, shift:up").execute()
The following code locks the screen by pressing the *Windows* key together
with the *l*: ::
with the *l* key: ::
Key("w-l").execute()
Expand All @@ -197,32 +197,38 @@
modifier keys (e.g. ctrl).
It can be enabled by changing the ``unicode_keyboard`` setting in
`~/.dragonfly2-speech/settings.cfg` to ``True``::
`~/.dragonfly2-speech/settings.cfg` to ``True``: ::
unicode_keyboard = True
The ``use_hardware`` parameter can be set to ``True`` if you need to
selectively require hardware events for a :class:`Key` action::
selectively require hardware events for a :class:`Key` action: ::
# Only copy if 'c' is a typeable key.
# Passing use_hardware=True will guarantee that Ctrl+C is always
# pressed, regardless of the layout. See below.
Key("c-c", use_hardware=True).execute()
If the Unicode keyboard is not enabled or the ``use_hardware`` parameter is
``True``, then no keys will be typed and an error will be logged for
untypeable keys::
untypeable keys: ::
action.exec (ERROR): Execution failed: Keyboard interface cannot type this character: 'c'
action.exec (ERROR): Execution failed: Keyboard interface cannot type this character: 'μ'
Keys in ranges 0-9, a-z and A-Z are always typeable. If keys in these ranges
cannot be typed using the current keyboard layout, then the equivalent key
will be used instead. For example, the following code will result in the 'я'
key being pressed when using the main Cyrillic keyboard layout: ::
# This is equivalent to Key(u"я, Я, c-я").
Key("z, Z, c-z", use_hardware=True).execute()
Unlike the :class:`Text` action, individual :class:`Key` actions can send
both hardware *and* Unicode events. So the following example will work if
the Unicode keyboard is enabled::
the Unicode keyboard is enabled: ::
# Type 'σμ' and then press ctrl-z.
# Type 'σμ' and then press Ctrl+Z.
Key(u"σ, μ, c-z").execute()
Note that the 'z' in this example will be typed if the current layout cannot
type the character.
X11 key support
............................................................................
Expand Down
8 changes: 3 additions & 5 deletions dragonfly/actions/action_paste.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,9 @@ def __init__(self, contents, format=None, paste=None, static=False):
if not format:
format = self._default_format
if paste is None:
try:
paste = Key(self._default_paste_spec)
except ActionError:
# Fallback on Shift-insert if 'v' isn't available.
paste = Key("s-insert/20")
# Pass use_hardware=True to guarantee that Ctrl+V is always
# pressed, regardless of the keyboard layout.
paste = Key(self._default_paste_spec, use_hardware=True)
if isinstance(contents, string_types):
spec = contents
self.contents = None
Expand Down
12 changes: 9 additions & 3 deletions dragonfly/actions/action_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
unicode_keyboard = True
If you need to simulate typing arbitrary Unicode characters *and* have
*individual* :class:`Text` actions respect modifier keys normally for normal
characters, set the configuration as above and use the ``use_hardware``
Expand All @@ -66,7 +65,6 @@
action = Text("σμ") + Key("ctrl:down") + Text("]", use_hardware=True) + Key("ctrl:up")
action.execute()
Some applications require hardware emulation versus Unicode keyboard
emulation. If you use such applications, add their executable names to the
``hardware_apps`` list in the configuration file mentioned above to make
Expand All @@ -77,7 +75,15 @@
the specified characters are not typeable using the current window's
keyboard layout, then an error will be logged and no keys will be typed::
action.exec (ERROR): Execution failed: Keyboard interface cannot type this character: 'c'
action.exec (ERROR): Execution failed: Keyboard interface cannot type this character: 'μ'
Keys in ranges 0-9, a-z and A-Z are always typeable. If keys in these ranges
cannot be typed using the current keyboard layout, then the equivalent key
will be used instead. For example, the following code will result in the 'я'
key being pressed when using the main Cyrillic keyboard layout: ::
# This is equivalent to Text(u"яЯ").
Text("zZ").execute()
These settings and parameters have no effect on other platforms.
Expand Down
45 changes: 35 additions & 10 deletions dragonfly/actions/keyboard/_win32.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,15 +136,26 @@ class Win32KeySymbols(object):
BROWSER_BACK = win32con.VK_BROWSER_BACK
BROWSER_FORWARD = win32con.VK_BROWSER_FORWARD

# Include virtual-key codes for digits and letters defined here:
# https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
DIGITS_MAP = {chr(x): x for x in range(0x30, 0x39)}
LOWERCASE_ALPHABET_MAP = {chr(x): x - 0x20 for x in range(0x61, 0x7b)}
UPPERCASE_ALPHABET_MAP = {chr(x): x | 0x100 for x in range(0x41, 0x5b)}
CHAR_VK_MAP = DIGITS_MAP.copy()
CHAR_VK_MAP.update(LOWERCASE_ALPHABET_MAP)
CHAR_VK_MAP.update(UPPERCASE_ALPHABET_MAP)


class Typeable(BaseTypeable):

__slots__ = ("_code", "_modifiers", "_name", "_is_text", "_char")
__slots__ = ("_code", "_modifiers", "_name", "_is_text", "_char",
"_char_vk")

def __init__(self, code, modifiers=(), name=None, is_text=False,
char=None):
BaseTypeable.__init__(self, code, modifiers, name, is_text)
self._char = char
self._char_vk = char and char in Win32KeySymbols.CHAR_VK_MAP

def update(self, hardware_events_required):
# Nothing to do for virtual keys.
Expand All @@ -156,12 +167,20 @@ def update(self, hardware_events_required):
self._is_text = False
code, modifiers = Keyboard.get_keycode_and_modifiers(self._char)
except ValueError:
if hardware_events_required:
if hardware_events_required and self._char_vk:
# Fallback on hardware events using the keycode in
# CHAR_VK_MAP instead. This will result in events for the
# equivalent key.
lookup_func = lambda char: Win32KeySymbols.CHAR_VK_MAP[char]
code, modifiers = Keyboard.get_keycode_and_modifiers(
self._char, lookup_func)
elif hardware_events_required:
# This Typeable cannot be typed in the current context.
return False

# Fallback on Unicode events.
code, modifiers = self._char, ()
self._is_text = True
else:
# Fallback on Unicode events.
code, modifiers = self._char, ()
self._is_text = True

# Set key code and modifiers.
self._code, self._modifiers = code, modifiers
Expand Down Expand Up @@ -291,8 +310,11 @@ def _get_initial_keycode(cls, char):
return code

@classmethod
def xget_virtual_keycode(cls, char):
code = cls._get_initial_keycode(char)
def xget_virtual_keycode(cls, char, lookup_func=None):
if lookup_func is None:
code = cls._get_initial_keycode(char)
else:
code = lookup_func(char)

# Construct a list of the virtual key code and modifiers.
codes = [code & 0x00ff]
Expand All @@ -302,8 +324,11 @@ def xget_virtual_keycode(cls, char):
return codes

@classmethod
def get_keycode_and_modifiers(cls, char):
code = cls._get_initial_keycode(char)
def get_keycode_and_modifiers(cls, char, lookup_func=None):
if lookup_func is None:
code = cls._get_initial_keycode(char)
else:
code = lookup_func(char)

# Construct a list of the virtual key code and modifiers.
modifiers = []
Expand Down

0 comments on commit ef70d66

Please sign in to comment.