Skip to content

Commit

Permalink
Merge pull request #352 from dictation-toolbox/cleanup-kb-code
Browse files Browse the repository at this point in the history
Cleanup keyboard code
  • Loading branch information
drmfinlay authored Aug 23, 2021
2 parents 307b832 + 4ebd104 commit 0a0eae5
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 233 deletions.
60 changes: 36 additions & 24 deletions dragonfly/actions/action_base_keyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
import io
import os
from os.path import basename
import sys

from .action_base import DynStrActionBase
from .keyboard import Keyboard
from .typeables import typeables

_CONFIG_LOADED = False
UNICODE_KEYBOARD = False
Expand Down Expand Up @@ -70,11 +70,13 @@ def load_configuration():
parser = configparser.ConfigParser()
parser.read(config_path)
if parser.has_option("Text", "hardware_apps"):
HARDWARE_APPS = parser.get("Text", "hardware_apps").lower().split("|")
HARDWARE_APPS = (parser.get("Text", "hardware_apps")
.lower().split("|"))
if parser.has_option("Text", "unicode_keyboard"):
UNICODE_KEYBOARD = parser.getboolean("Text", "unicode_keyboard")
if parser.has_option("Text", "pause_default"):
PAUSE_DEFAULT = parser.getfloat("Text", "pause_default")
BaseKeyboardAction._pause_default = PAUSE_DEFAULT

_CONFIG_LOADED = True

Expand All @@ -83,31 +85,25 @@ class BaseKeyboardAction(DynStrActionBase):
"""
Base keystroke emulation action.
This class isn't meant to be used directly.
"""

_keyboard = Keyboard()
_pause_default = PAUSE_DEFAULT

def __init__(self, spec=None, static=False, use_hardware=False):
# Note: these are only used on Windows.
self._event_cache = {}
self._use_hardware = use_hardware

super(BaseKeyboardAction, self).__init__(spec, static)
# Load the Windows-only config file if necessary.
if not _CONFIG_LOADED and os.name == "nt":
load_configuration()

# Save events for the current layout if on Windows.
if self._events is not None and sys.platform.startswith("win"):
layout = self._keyboard.get_current_layout()
self._event_cache[layout] = self._events
super(BaseKeyboardAction, self).__init__(spec, static)

def require_hardware_events(self):
"""
Return `True` if the current context requires hardware emulation.
"""
# Always use hardware_events for non-Windows platforms.
if not sys.platform.startswith("win") or self._use_hardware:
if self._use_hardware:
return True

# Load the keyboard configuration, if necessary.
Expand All @@ -123,14 +119,30 @@ def require_hardware_events(self):
return ((not UNICODE_KEYBOARD) or
(foreground_executable in HARDWARE_APPS))

def _execute(self, data=None):
# Get updated events on Windows for each new keyboard layout
# encountered.
if sys.platform.startswith("win") and self._static:
layout = self._keyboard.get_current_layout()
events = self._event_cache.get(layout)
if events is None:
self._events = self._parse_spec(self._spec)
self._event_cache[layout] = self._events

return super(BaseKeyboardAction, self)._execute(data)
def _get_typeable(self, key_symbol, use_hardware):
# Use the Typeable object for the symbol, if it exists.
typeable = typeables.get(key_symbol)
if typeable:
# Update the object and return it.
typeable.update(use_hardware)
return typeable

# Otherwise, get a new Typeable for the symbol, if possible.
is_text = not use_hardware
try:
typeable = self._keyboard.get_typeable(key_symbol,
is_text=is_text)
except ValueError:
pass

# If getting a Typeable failed, then, if it is allowed, try
# again with is_text=True. On Windows, this will use Unicode
# events instead.
if not (typeable or use_hardware):
try:
typeable = self._keyboard.get_typeable(key_symbol,
is_text=True)
except ValueError:
pass

return typeable
127 changes: 61 additions & 66 deletions dragonfly/actions/action_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,16 +282,16 @@
"""

import sys
import collections

from .action_base import ActionError
from .action_base_keyboard import BaseKeyboardAction
from .typeables import typeables

#---------------------------------------------------------------------------

class Key(BaseKeyboardAction):

class Key(BaseKeyboardAction):
"""
Keystroke emulation action.
Expand Down Expand Up @@ -331,31 +331,36 @@ class Key(BaseKeyboardAction):
interval_factor = 0.01
interval_default = 0.0

# Keystroke event data type.
EventData = collections.namedtuple(
"EventData", "keyname "
"direction "
"modifiers "
"inner_pause "
"repeat "
"outer_pause "
)

def _parse_spec(self, spec):
# Iterate through the keystrokes specified in spec, parsing
# each individually.
events = []
error_message = None
hardware_events_required = self.require_hardware_events()
for single in spec.split(self._key_separator):
key_events, error_message = self._parse_single(
single, hardware_events_required
)
if error_message:
break

events.extend(key_events)
return events, error_message
event_data = self._parse_single(single)
if not event_data:
continue
events.append(event_data)
return events

def _parse_single(self, spec, hardware_events_required):
def _parse_single(self, spec):
# pylint: disable=R0912,R0914,R0915
# Suppress warnings about too many branches, variables and
# statements.

# Remove leading and trailing whitespace.
spec = spec.strip()
if not spec:
return [], None
return None

# Parse modifier prefix.
index = spec.find(self._modifier_prefix_delimiter)
Expand Down Expand Up @@ -409,38 +414,6 @@ def _parse_single(self, spec, hardware_events_required):
else:
raise ActionError("Invalid key spec: %s" % spec)

# Check if the key name is valid.
error_message = ("Keyboard interface cannot type this character: %r"
% keyname)
code = typeables.get(keyname)
if code is None:
# Delegate to the platform keyboard class. Any invalid keys will
# cause error messages later than normal, but this allows using
# valid key symbols that dragonfly doesn't define.
try:
code = self._keyboard.get_typeable(keyname)
typeables[keyname] = code
except ValueError:
if hardware_events_required:
# Return an error message to display when this action
# is executed.
return [], error_message

# If on Windows and if hardware events are not required,
# then attempt to use the Unicode keyboard instead.
if sys.platform.startswith("win"):
try:
code = self._keyboard.get_typeable(keyname,
is_text=True)
typeables[keyname] = code
except ValueError:
return [], error_message
else:
# Update the Typeable. Return an error message if this fails.
# Note: this only currently does anything on Windows.
if not code.update(hardware_events_required):
return [], error_message

if inner_pause is not None:
s = inner_pause
try:
Expand Down Expand Up @@ -473,6 +446,43 @@ def _parse_single(self, spec, hardware_events_required):
raise ActionError("Invalid repeat value: %r,"
" should be a positive integer." % s)

if direction is not None:
if modifiers:
raise ActionError("Cannot use direction with modifiers.")
if inner_pause is not None:
raise ActionError("Cannot use direction with inner pause.")

return self.EventData(keyname, direction, modifiers, inner_pause,
repeat, outer_pause)

def _execute_events(self, events):
# Calculate keyboard events from events (event data).
use_hardware = self.require_hardware_events()
keyboard_events = []
for event_data in events:
events_single = self._calc_events_single(event_data,
use_hardware)
keyboard_events.extend(events_single)

# Send keyboard events.
self._keyboard.send_keyboard_events(keyboard_events)
return True

def _calc_events_single(self, event_data, use_hardware):
(keyname, direction, modifiers, inner_pause, repeat,
outer_pause) = event_data

# Get a Typeable object for the key, if possible.
typeable = self._get_typeable(event_data.keyname, use_hardware)

# Raise an error message if a Typeable could not be retrieved.
if typeable is None:
error_message = ("Keyboard interface cannot type this "
"character: %r (in %r)" %
(keyname, self._spec))
raise ActionError(error_message)

# Calculate keyboard events using the Typeable and event data.
if direction is None:
if inner_pause is None:
inner_pause = self.interval_default * self.interval_factor
Expand All @@ -483,31 +493,16 @@ def _parse_single(self, spec, hardware_events_required):
for m in modifiers:
events.extend(m.on_events())
for _ in range(repeat - 1):
events.extend(code.events(inner_pause))
events.extend(code.events(outer_pause))
events.extend(typeable.events(inner_pause))
events.extend(typeable.events(outer_pause))
for m in modifiers[-1::-1]:
events.extend(m.off_events())
else:
if modifiers:
raise ActionError("Cannot use direction with modifiers.")
if inner_pause is not None:
raise ActionError("Cannot use direction with inner pause.")
if direction:
events = code.on_events(outer_pause)
events = typeable.on_events(outer_pause)
else:
events = code.off_events(outer_pause)

return events, None

def _execute_events(self, events):
events, error_message = events

# Raise any message about invalid keys.
if error_message:
raise ActionError(error_message)
else:
self._keyboard.send_keyboard_events(events)
return True
events = typeable.off_events(outer_pause)
return events

def __str__(self):
return '[{!r}]'.format(self._spec)
Loading

0 comments on commit 0a0eae5

Please sign in to comment.