1107 lines
40 KiB
Python
1107 lines
40 KiB
Python
# Cthulhu
|
|
#
|
|
# Copyright 2005-2008 Sun Microsystems Inc.
|
|
# Copyright 2011-2016 Igalia, S.L.
|
|
#
|
|
# This library 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 2.1 of the License, or (at your option) any later version.
|
|
#
|
|
# This library 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 library; if not, write to the
|
|
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
|
# Boston MA 02110-1301 USA.
|
|
|
|
"""Provides support for handling input events."""
|
|
|
|
__id__ = "$Id$"
|
|
__version__ = "$Revision$"
|
|
__date__ = "$Date$"
|
|
__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \
|
|
"Copyright (c) 2011-2016 Igalia, S.L."
|
|
__license__ = "LGPL"
|
|
|
|
import gi
|
|
gi.require_version("Atspi", "2.0")
|
|
from gi.repository import Atspi
|
|
|
|
import math
|
|
import time
|
|
from gi.repository import Gdk
|
|
from gi.repository import GLib
|
|
|
|
from . import debug
|
|
from . import keybindings
|
|
from . import keynames
|
|
from . import messages
|
|
from . import cthulhu
|
|
from . import cthulhu_state
|
|
from . import script_manager
|
|
from . import settings
|
|
from .ax_object import AXObject
|
|
from .ax_utilities import AXUtilities
|
|
|
|
KEYBOARD_EVENT = "keyboard"
|
|
BRAILLE_EVENT = "braille"
|
|
MOUSE_BUTTON_EVENT = "mouse:button"
|
|
|
|
class InputEvent:
|
|
|
|
def __init__(self, eventType):
|
|
"""Creates a new KEYBOARD_EVENT, BRAILLE_EVENT, or MOUSE_BUTTON_EVENT."""
|
|
|
|
self.type = eventType
|
|
self.time = time.time()
|
|
self._clickCount = 0
|
|
|
|
def getClickCount(self):
|
|
"""Return the count of the number of clicks a user has made."""
|
|
|
|
return self._clickCount
|
|
|
|
def setClickCount(self):
|
|
"""Updates the count of the number of clicks a user has made."""
|
|
|
|
pass
|
|
|
|
def _getXkbStickyKeysState():
|
|
from subprocess import check_output
|
|
|
|
try:
|
|
output = check_output(['xkbset', 'q'])
|
|
for line in output.decode('ASCII', errors='ignore').split('\n'):
|
|
if line.startswith('Sticky-Keys = '):
|
|
return line.endswith('On')
|
|
except Exception:
|
|
pass
|
|
return False
|
|
|
|
class KeyboardEvent(InputEvent):
|
|
|
|
stickyKeys = _getXkbStickyKeysState()
|
|
|
|
duplicateCount = 0
|
|
cthulhuModifierPressed = False
|
|
|
|
# Whether last press of the Cthulhu modifier was alone
|
|
lastCthulhuModifierAlone = False
|
|
lastCthulhuModifierAloneTime = None
|
|
# Whether the current press of the Cthulhu modifier is alone
|
|
currentCthulhuModifierAlone = False
|
|
currentCthulhuModifierAloneTime = None
|
|
# When the second cthulhu press happened
|
|
secondCthulhuModifierTime = None
|
|
# Sticky modifiers state, to be applied to the next keyboard event
|
|
cthulhuStickyModifiers = 0
|
|
|
|
TYPE_UNKNOWN = "unknown"
|
|
TYPE_PRINTABLE = "printable"
|
|
TYPE_MODIFIER = "modifier"
|
|
TYPE_LOCKING = "locking"
|
|
TYPE_FUNCTION = "function"
|
|
TYPE_ACTION = "action"
|
|
TYPE_NAVIGATION = "navigation"
|
|
TYPE_DIACRITICAL = "diacritical"
|
|
TYPE_ALPHABETIC = "alphabetic"
|
|
TYPE_NUMERIC = "numeric"
|
|
TYPE_PUNCTUATION = "punctuation"
|
|
TYPE_SPACE = "space"
|
|
|
|
GDK_PUNCTUATION_KEYS = [Gdk.KEY_acute,
|
|
Gdk.KEY_ampersand,
|
|
Gdk.KEY_apostrophe,
|
|
Gdk.KEY_asciicircum,
|
|
Gdk.KEY_asciitilde,
|
|
Gdk.KEY_asterisk,
|
|
Gdk.KEY_at,
|
|
Gdk.KEY_backslash,
|
|
Gdk.KEY_bar,
|
|
Gdk.KEY_braceleft,
|
|
Gdk.KEY_braceright,
|
|
Gdk.KEY_bracketleft,
|
|
Gdk.KEY_bracketright,
|
|
Gdk.KEY_brokenbar,
|
|
Gdk.KEY_cedilla,
|
|
Gdk.KEY_cent,
|
|
Gdk.KEY_colon,
|
|
Gdk.KEY_comma,
|
|
Gdk.KEY_copyright,
|
|
Gdk.KEY_currency,
|
|
Gdk.KEY_degree,
|
|
Gdk.KEY_diaeresis,
|
|
Gdk.KEY_dollar,
|
|
Gdk.KEY_EuroSign,
|
|
Gdk.KEY_equal,
|
|
Gdk.KEY_exclam,
|
|
Gdk.KEY_exclamdown,
|
|
Gdk.KEY_grave,
|
|
Gdk.KEY_greater,
|
|
Gdk.KEY_guillemotleft,
|
|
Gdk.KEY_guillemotright,
|
|
Gdk.KEY_hyphen,
|
|
Gdk.KEY_less,
|
|
Gdk.KEY_macron,
|
|
Gdk.KEY_minus,
|
|
Gdk.KEY_notsign,
|
|
Gdk.KEY_numbersign,
|
|
Gdk.KEY_paragraph,
|
|
Gdk.KEY_parenleft,
|
|
Gdk.KEY_parenright,
|
|
Gdk.KEY_percent,
|
|
Gdk.KEY_period,
|
|
Gdk.KEY_periodcentered,
|
|
Gdk.KEY_plus,
|
|
Gdk.KEY_plusminus,
|
|
Gdk.KEY_question,
|
|
Gdk.KEY_questiondown,
|
|
Gdk.KEY_quotedbl,
|
|
Gdk.KEY_quoteleft,
|
|
Gdk.KEY_quoteright,
|
|
Gdk.KEY_registered,
|
|
Gdk.KEY_section,
|
|
Gdk.KEY_semicolon,
|
|
Gdk.KEY_slash,
|
|
Gdk.KEY_sterling,
|
|
Gdk.KEY_underscore,
|
|
Gdk.KEY_yen]
|
|
|
|
GDK_ACCENTED_LETTER_KEYS = [Gdk.KEY_Aacute,
|
|
Gdk.KEY_aacute,
|
|
Gdk.KEY_Acircumflex,
|
|
Gdk.KEY_acircumflex,
|
|
Gdk.KEY_Adiaeresis,
|
|
Gdk.KEY_adiaeresis,
|
|
Gdk.KEY_Agrave,
|
|
Gdk.KEY_agrave,
|
|
Gdk.KEY_Aring,
|
|
Gdk.KEY_aring,
|
|
Gdk.KEY_Atilde,
|
|
Gdk.KEY_atilde,
|
|
Gdk.KEY_Ccedilla,
|
|
Gdk.KEY_ccedilla,
|
|
Gdk.KEY_Eacute,
|
|
Gdk.KEY_eacute,
|
|
Gdk.KEY_Ecircumflex,
|
|
Gdk.KEY_ecircumflex,
|
|
Gdk.KEY_Ediaeresis,
|
|
Gdk.KEY_ediaeresis,
|
|
Gdk.KEY_Egrave,
|
|
Gdk.KEY_egrave,
|
|
Gdk.KEY_Iacute,
|
|
Gdk.KEY_iacute,
|
|
Gdk.KEY_Icircumflex,
|
|
Gdk.KEY_icircumflex,
|
|
Gdk.KEY_Idiaeresis,
|
|
Gdk.KEY_idiaeresis,
|
|
Gdk.KEY_Igrave,
|
|
Gdk.KEY_igrave,
|
|
Gdk.KEY_Ntilde,
|
|
Gdk.KEY_ntilde,
|
|
Gdk.KEY_Oacute,
|
|
Gdk.KEY_oacute,
|
|
Gdk.KEY_Ocircumflex,
|
|
Gdk.KEY_ocircumflex,
|
|
Gdk.KEY_Odiaeresis,
|
|
Gdk.KEY_odiaeresis,
|
|
Gdk.KEY_Ograve,
|
|
Gdk.KEY_ograve,
|
|
Gdk.KEY_Ooblique,
|
|
Gdk.KEY_ooblique,
|
|
Gdk.KEY_Otilde,
|
|
Gdk.KEY_otilde,
|
|
Gdk.KEY_Uacute,
|
|
Gdk.KEY_uacute,
|
|
Gdk.KEY_Ucircumflex,
|
|
Gdk.KEY_ucircumflex,
|
|
Gdk.KEY_Udiaeresis,
|
|
Gdk.KEY_udiaeresis,
|
|
Gdk.KEY_Ugrave,
|
|
Gdk.KEY_ugrave,
|
|
Gdk.KEY_Yacute,
|
|
Gdk.KEY_yacute]
|
|
|
|
def __init__(self, event):
|
|
"""Creates a new InputEvent of type KEYBOARD_EVENT.
|
|
|
|
Arguments:
|
|
- event: the AT-SPI keyboard event
|
|
"""
|
|
|
|
super().__init__(KEYBOARD_EVENT)
|
|
self.id = event.id
|
|
self.type = event.type
|
|
self.hw_code = event.hw_code
|
|
self.modifiers = event.modifiers & Gdk.ModifierType.MODIFIER_MASK
|
|
if event.modifiers & (1 << Atspi.ModifierType.NUMLOCK):
|
|
self.modifiers |= (1 << Atspi.ModifierType.NUMLOCK)
|
|
self.event_string = event.event_string
|
|
self.keyval_name = Gdk.keyval_name(event.id)
|
|
if self.event_string == "":
|
|
self.event_string = self.keyval_name
|
|
self.timestamp = event.timestamp
|
|
self.is_duplicate = self in [cthulhu_state.lastInputEvent,
|
|
cthulhu_state.lastNonModifierKeyEvent]
|
|
self._script = cthulhu_state.activeScript
|
|
self._app = None
|
|
self._window = cthulhu_state.activeWindow
|
|
self._obj = cthulhu_state.locusOfFocus
|
|
self._handler = None
|
|
self._consumer = None
|
|
self._should_consume = None
|
|
self._consume_reason = None
|
|
self._did_consume = None
|
|
self._result_reason = None
|
|
self._bypassCthulhu = None
|
|
self._is_kp_with_numlock = False
|
|
|
|
# Some implementors don't populate this field at all. More often than not,
|
|
# the event_string and the keyval_name coincide for input events.
|
|
if not self.event_string:
|
|
self.event_string = self.keyval_name
|
|
|
|
# Some implementors do populate the field, but with the keyname rather than
|
|
# the printable character. This messes us up with punctuation and other symbols.
|
|
if len(self.event_string) > 1 \
|
|
and (self.id in KeyboardEvent.GDK_PUNCTUATION_KEYS or \
|
|
self.id in KeyboardEvent.GDK_ACCENTED_LETTER_KEYS):
|
|
self.event_string = chr(self.id)
|
|
|
|
# Some implementors don't include numlock in the modifiers. Unfortunately,
|
|
# trying to heuristically hack around this just by looking at the event
|
|
# is not reliable. Ditto regarding asking Gdk for the numlock state.
|
|
if self.keyval_name.startswith("KP"):
|
|
if event.modifiers & (1 << Atspi.ModifierType.NUMLOCK):
|
|
self._is_kp_with_numlock = True
|
|
|
|
if self._script:
|
|
self._app = self._script.app
|
|
if not self._window:
|
|
cthulhu.setActiveWindow(self._script.utilities.activeWindow())
|
|
self._window = cthulhu_state.activeWindow
|
|
tokens = ["INPUT EVENT: Updated window and active window to", self._window]
|
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
|
|
|
if self._window and self._app != AXObject.get_application(self._window):
|
|
self._script = script_manager.getManager().getScript(
|
|
AXObject.get_application(self._window))
|
|
self._app = self._script.app
|
|
tokens = ["INPUT EVENT: Updated script to", self._script]
|
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
|
|
|
if self.is_duplicate:
|
|
KeyboardEvent.duplicateCount += 1
|
|
else:
|
|
KeyboardEvent.duplicateCount = 0
|
|
|
|
self.keyType = None
|
|
|
|
_isPressed = event.type == Atspi.EventType.KEY_PRESSED_EVENT
|
|
role = AXObject.get_role(self._obj)
|
|
_mayEcho = _isPressed or role == Atspi.Role.TERMINAL
|
|
|
|
if KeyboardEvent.stickyKeys and not self.isCthulhuModifier() \
|
|
and not KeyboardEvent.lastCthulhuModifierAlone:
|
|
doubleEvent = self._getDoubleClickCandidate()
|
|
if doubleEvent and \
|
|
doubleEvent.modifiers & keybindings.CTHULHU_MODIFIER_MASK:
|
|
# this is the second event of a double-click, and sticky Cthulhu
|
|
# affected the first, so copy over the modifiers to the second
|
|
KeyboardEvent.cthulhuStickyModifiers = doubleEvent.modifiers
|
|
|
|
if not self.isCthulhuModifier():
|
|
if KeyboardEvent.cthulhuModifierPressed:
|
|
KeyboardEvent.currentCthulhuModifierAlone = False
|
|
KeyboardEvent.currentCthulhuModifierAloneTime = None
|
|
else:
|
|
KeyboardEvent.lastCthulhuModifierAlone = False
|
|
KeyboardEvent.lastCthulhuModifierAloneTime = None
|
|
|
|
if self.isNavigationKey():
|
|
self.keyType = KeyboardEvent.TYPE_NAVIGATION
|
|
self.shouldEcho = _mayEcho and settings.enableNavigationKeys
|
|
elif self.isActionKey():
|
|
self.keyType = KeyboardEvent.TYPE_ACTION
|
|
self.shouldEcho = _mayEcho and settings.enableActionKeys
|
|
elif self.isModifierKey():
|
|
self.keyType = KeyboardEvent.TYPE_MODIFIER
|
|
self.shouldEcho = _mayEcho and settings.enableModifierKeys
|
|
if self.isCthulhuModifier() and not self.is_duplicate:
|
|
now = time.time()
|
|
if KeyboardEvent.lastCthulhuModifierAlone:
|
|
if _isPressed:
|
|
KeyboardEvent.secondCthulhuModifierTime = now
|
|
if (KeyboardEvent.secondCthulhuModifierTime <
|
|
KeyboardEvent.lastCthulhuModifierAloneTime + 0.5):
|
|
# double-cthulhu, let the real action happen
|
|
self._bypassCthulhu = True
|
|
if not _isPressed:
|
|
KeyboardEvent.lastCthulhuModifierAlone = False
|
|
KeyboardEvent.lastCthulhuModifierAloneTime = False
|
|
else:
|
|
KeyboardEvent.cthulhuModifierPressed = _isPressed
|
|
if _isPressed:
|
|
KeyboardEvent.currentCthulhuModifierAlone = True
|
|
KeyboardEvent.currentCthulhuModifierAloneTime = now
|
|
else:
|
|
KeyboardEvent.lastCthulhuModifierAlone = \
|
|
KeyboardEvent.currentCthulhuModifierAlone
|
|
KeyboardEvent.lastCthulhuModifierAloneTime = \
|
|
KeyboardEvent.currentCthulhuModifierAloneTime
|
|
elif self.isFunctionKey():
|
|
self.keyType = KeyboardEvent.TYPE_FUNCTION
|
|
self.shouldEcho = _mayEcho and settings.enableFunctionKeys
|
|
elif self.isDiacriticalKey():
|
|
self.keyType = KeyboardEvent.TYPE_DIACRITICAL
|
|
self.shouldEcho = _mayEcho and settings.enableDiacriticalKeys
|
|
elif self.isLockingKey():
|
|
self.keyType = KeyboardEvent.TYPE_LOCKING
|
|
self.shouldEcho = settings.presentLockingKeys
|
|
if self.shouldEcho is None:
|
|
self.shouldEcho = not settings.onlySpeakDisplayedText
|
|
self.shouldEcho = self.shouldEcho and _isPressed
|
|
elif self.isAlphabeticKey():
|
|
self.keyType = KeyboardEvent.TYPE_ALPHABETIC
|
|
self.shouldEcho = _mayEcho \
|
|
and (settings.enableAlphabeticKeys or settings.enableEchoByCharacter)
|
|
elif self.isNumericKey():
|
|
self.keyType = KeyboardEvent.TYPE_NUMERIC
|
|
self.shouldEcho = _mayEcho \
|
|
and (settings.enableNumericKeys or settings.enableEchoByCharacter)
|
|
elif self.isPunctuationKey():
|
|
self.keyType = KeyboardEvent.TYPE_PUNCTUATION
|
|
self.shouldEcho = _mayEcho \
|
|
and (settings.enablePunctuationKeys or settings.enableEchoByCharacter)
|
|
elif self.isSpace():
|
|
self.keyType = KeyboardEvent.TYPE_SPACE
|
|
self.shouldEcho = _mayEcho \
|
|
and (settings.enableSpace or settings.enableEchoByCharacter)
|
|
else:
|
|
self.keyType = KeyboardEvent.TYPE_UNKNOWN
|
|
self.shouldEcho = False
|
|
|
|
if not self.isLockingKey():
|
|
self.shouldEcho = self.shouldEcho and settings.enableKeyEcho
|
|
|
|
if not self.isModifierKey():
|
|
self.setClickCount()
|
|
|
|
if cthulhu_state.bypassNextCommand and _isPressed:
|
|
KeyboardEvent.cthulhuModifierPressed = False
|
|
|
|
if KeyboardEvent.cthulhuModifierPressed:
|
|
self.modifiers |= keybindings.CTHULHU_MODIFIER_MASK
|
|
|
|
if KeyboardEvent.stickyKeys:
|
|
# apply all recorded sticky modifiers
|
|
self.modifiers |= KeyboardEvent.cthulhuStickyModifiers
|
|
if self.isModifierKey():
|
|
# add this modifier to the sticky ones
|
|
KeyboardEvent.cthulhuStickyModifiers |= self.modifiers
|
|
else:
|
|
# Non-modifier key, so clear the sticky modifiers. If the user
|
|
# actually double-presses that key, the modifiers of this event
|
|
# will be copied over to the second event, see earlier in this
|
|
# function.
|
|
KeyboardEvent.cthulhuStickyModifiers = 0
|
|
|
|
self._should_consume, self._consume_reason = self.shouldConsume()
|
|
|
|
def _getDoubleClickCandidate(self):
|
|
lastEvent = cthulhu_state.lastNonModifierKeyEvent
|
|
if isinstance(lastEvent, KeyboardEvent) \
|
|
and lastEvent.event_string == self.event_string \
|
|
and self.time - lastEvent.time <= settings.doubleClickTimeout:
|
|
return lastEvent
|
|
return None
|
|
|
|
def setClickCount(self):
|
|
"""Updates the count of the number of clicks a user has made."""
|
|
|
|
doubleEvent = self._getDoubleClickCandidate()
|
|
if not doubleEvent:
|
|
self._clickCount = 1
|
|
return
|
|
|
|
self._clickCount = doubleEvent.getClickCount()
|
|
if self.is_duplicate:
|
|
return
|
|
|
|
if self.type == Atspi.EventType.KEY_RELEASED_EVENT:
|
|
return
|
|
|
|
if self._clickCount < 3:
|
|
self._clickCount += 1
|
|
return
|
|
|
|
self._clickCount = 1
|
|
|
|
def __eq__(self, other):
|
|
if not other:
|
|
return False
|
|
|
|
if self.type == other.type and self.hw_code == other.hw_code:
|
|
return self.timestamp == other.timestamp
|
|
|
|
return False
|
|
|
|
def __str__(self):
|
|
if self._shouldObscure():
|
|
keyid = hw_code = modifiers = event_string = keyval_name = key_type = "*"
|
|
else:
|
|
keyid = self.id
|
|
hw_code = self.hw_code
|
|
modifiers = self.modifiers
|
|
event_string = self.event_string
|
|
keyval_name = self.keyval_name
|
|
key_type = self.keyType
|
|
|
|
return (f"KEYBOARD_EVENT: type={self.type.value_name.upper()}\n") \
|
|
+ f" id={keyid}\n" \
|
|
+ f" hw_code={hw_code}\n" \
|
|
+ f" modifiers={modifiers}\n" \
|
|
+ f" event_string=({event_string})\n" \
|
|
+ f" keyval_name=({keyval_name})\n" \
|
|
+ (" timestamp=%d\n" % self.timestamp) \
|
|
+ f" time={time.time():f}\n" \
|
|
+ f" keyType={key_type}\n" \
|
|
+ f" clickCount={self._clickCount}\n" \
|
|
+ f" shouldEcho={self.shouldEcho}\n"
|
|
|
|
def _shouldObscure(self):
|
|
if not AXUtilities.is_password_text(self._obj):
|
|
return False
|
|
|
|
if not self.isPrintableKey():
|
|
return False
|
|
|
|
if self.modifiers & keybindings.CTRL_MODIFIER_MASK \
|
|
or self.modifiers & keybindings.ALT_MODIFIER_MASK \
|
|
or self.modifiers & keybindings.CTHULHU_MODIFIER_MASK:
|
|
return False
|
|
|
|
return True
|
|
|
|
def _isReleaseForLastNonModifierKeyEvent(self):
|
|
last = cthulhu_state.lastNonModifierKeyEvent
|
|
if not last:
|
|
return False
|
|
|
|
if not last.isPressedKey() or self.isPressedKey():
|
|
return False
|
|
|
|
if self.id == last.id and self.hw_code == last.hw_code:
|
|
return self.modifiers == last.modifiers
|
|
|
|
return False
|
|
|
|
def isReleaseFor(self, other):
|
|
"""Return True if this is the release event for other."""
|
|
|
|
if not other:
|
|
return False
|
|
|
|
if not other.isPressedKey() or self.isPressedKey():
|
|
return False
|
|
|
|
return self.id == other.id \
|
|
and self.hw_code == other.hw_code \
|
|
and self.modifiers == other.modifiers \
|
|
and self.event_string == other.event_string \
|
|
and self.keyval_name == other.keyval_name \
|
|
and self.keyType == other.keyType \
|
|
and self._clickCount == other._clickCount
|
|
|
|
def isNavigationKey(self):
|
|
"""Return True if this is a navigation key."""
|
|
|
|
if self.keyType:
|
|
return self.keyType == KeyboardEvent.TYPE_NAVIGATION
|
|
|
|
return self.event_string in \
|
|
["Left", "Right", "Up", "Down", "Home", "End"]
|
|
|
|
def isActionKey(self):
|
|
"""Return True if this is an action key."""
|
|
|
|
if self.keyType:
|
|
return self.keyType == KeyboardEvent.TYPE_ACTION
|
|
|
|
return self.event_string in \
|
|
["Return", "Escape", "Tab", "BackSpace", "Delete",
|
|
"Page_Up", "Page_Down"]
|
|
|
|
def isAlphabeticKey(self):
|
|
"""Return True if this is an alphabetic key."""
|
|
|
|
if self.keyType:
|
|
return self.keyType == KeyboardEvent.TYPE_ALPHABETIC
|
|
|
|
if not len(self.event_string) == 1:
|
|
return False
|
|
|
|
return self.event_string.isalpha()
|
|
|
|
def isDiacriticalKey(self):
|
|
"""Return True if this is a non-spacing diacritical key."""
|
|
|
|
if self.keyType:
|
|
return self.keyType == KeyboardEvent.TYPE_DIACRITICAL
|
|
|
|
return self.event_string.startswith("dead_")
|
|
|
|
def isFunctionKey(self):
|
|
"""Return True if this is a function key."""
|
|
|
|
if self.keyType:
|
|
return self.keyType == KeyboardEvent.TYPE_FUNCTION
|
|
|
|
return self.event_string in \
|
|
["F1", "F2", "F3", "F4", "F5", "F6",
|
|
"F7", "F8", "F9", "F10", "F11", "F12"]
|
|
|
|
def isLockingKey(self):
|
|
"""Return True if this is a locking key."""
|
|
|
|
if self.keyType:
|
|
return self.keyType in KeyboardEvent.TYPE_LOCKING
|
|
|
|
lockingKeys = ["Caps_Lock", "Shift_Lock", "Num_Lock", "Scroll_Lock"]
|
|
if self.event_string not in lockingKeys:
|
|
return False
|
|
|
|
if not cthulhu_state.bypassNextCommand and not self._bypassCthulhu:
|
|
return self.event_string not in settings.cthulhuModifierKeys
|
|
|
|
return True
|
|
|
|
def isModifierKey(self):
|
|
"""Return True if this is a modifier key."""
|
|
|
|
if self.keyType:
|
|
return self.keyType == KeyboardEvent.TYPE_MODIFIER
|
|
|
|
if self.isCthulhuModifier():
|
|
return True
|
|
|
|
return self.event_string in \
|
|
['Alt_L', 'Alt_R', 'Control_L', 'Control_R',
|
|
'Shift_L', 'Shift_R', 'Meta_L', 'Meta_R',
|
|
'ISO_Level3_Shift']
|
|
|
|
def isNumericKey(self):
|
|
"""Return True if this is a numeric key."""
|
|
|
|
if self.keyType:
|
|
return self.keyType == KeyboardEvent.TYPE_NUMERIC
|
|
|
|
if not len(self.event_string) == 1:
|
|
return False
|
|
|
|
return self.event_string.isnumeric()
|
|
|
|
def isCthulhuModifier(self, checkBypassMode=True):
|
|
"""Return True if this is the Cthulhu modifier key."""
|
|
|
|
if checkBypassMode and cthulhu_state.bypassNextCommand:
|
|
return False
|
|
|
|
if self.event_string in settings.cthulhuModifierKeys:
|
|
return True
|
|
|
|
if self.keyval_name == "KP_0" \
|
|
and "KP_Insert" in settings.cthulhuModifierKeys \
|
|
and self.modifiers & keybindings.SHIFT_MODIFIER_MASK:
|
|
return True
|
|
|
|
return False
|
|
|
|
def isCthulhuModified(self):
|
|
"""Return True if this key is Cthulhu modified."""
|
|
|
|
if cthulhu_state.bypassNextCommand:
|
|
return False
|
|
|
|
return self.modifiers & keybindings.CTHULHU_MODIFIER_MASK
|
|
|
|
def isKeyPadKeyWithNumlockOn(self):
|
|
"""Return True if this is a key pad key with numlock on."""
|
|
|
|
return self._is_kp_with_numlock
|
|
|
|
def isPrintableKey(self):
|
|
"""Return True if this is a printable key."""
|
|
|
|
if self.event_string in ["space", " "]:
|
|
return True
|
|
|
|
if not len(self.event_string) == 1:
|
|
return False
|
|
|
|
return self.event_string.isprintable()
|
|
|
|
def isPressedKey(self):
|
|
"""Returns True if the key is pressed"""
|
|
|
|
return self.type == Atspi.EventType.KEY_PRESSED_EVENT
|
|
|
|
def isPunctuationKey(self):
|
|
"""Return True if this is a punctuation key."""
|
|
|
|
if self.keyType:
|
|
return self.keyType == KeyboardEvent.TYPE_PUNCTUATION
|
|
|
|
if not len(self.event_string) == 1:
|
|
return False
|
|
|
|
if self.isAlphabeticKey() or self.isNumericKey():
|
|
return False
|
|
|
|
return self.event_string.isprintable() and not self.event_string.isspace()
|
|
|
|
def isSpace(self):
|
|
"""Return True if this is the space key."""
|
|
|
|
if self.keyType:
|
|
return self.keyType == KeyboardEvent.TYPE_SPACE
|
|
|
|
return self.event_string in ["space", " "]
|
|
|
|
def isFromApplication(self, app):
|
|
"""Return True if this is associated with the specified app."""
|
|
|
|
return self._app == app
|
|
|
|
def isCharacterEchoable(self):
|
|
"""Returns True if the script will echo this event as part of
|
|
character echo. We do this to not double-echo a given printable
|
|
character."""
|
|
|
|
if not self.isPrintableKey():
|
|
return False
|
|
|
|
script = cthulhu_state.activeScript
|
|
return script and script.utilities.willEchoCharacter(self)
|
|
|
|
def getLockingState(self):
|
|
"""Returns True if the event locked a locking key, False if the
|
|
event unlocked a locking key, and None if we do not know or this
|
|
is not a locking key."""
|
|
|
|
if not self.isLockingKey():
|
|
return None
|
|
|
|
if self.event_string == "Caps_Lock":
|
|
mod = Atspi.ModifierType.SHIFTLOCK
|
|
elif self.event_string == "Shift_Lock":
|
|
mod = Atspi.ModifierType.SHIFT
|
|
elif self.event_string == "Num_Lock":
|
|
mod = Atspi.ModifierType.NUMLOCK
|
|
else:
|
|
return None
|
|
|
|
return not self.modifiers & (1 << mod)
|
|
|
|
def getLockingStateString(self):
|
|
"""Returns the string which reflects the locking state we wish to
|
|
include when presenting a locking key."""
|
|
|
|
locked = self.getLockingState()
|
|
if locked is None:
|
|
return ''
|
|
|
|
if not locked:
|
|
return messages.LOCKING_KEY_STATE_OFF
|
|
|
|
return messages.LOCKING_KEY_STATE_ON
|
|
|
|
def getKeyName(self):
|
|
"""Returns the string to be used for presenting the key to the user."""
|
|
|
|
return keynames.getKeyName(self.event_string)
|
|
|
|
def getObject(self):
|
|
"""Returns the object believed to be associated with this key event."""
|
|
|
|
return self._obj
|
|
|
|
def getHandler(self):
|
|
"""Returns the handler associated with this key event."""
|
|
|
|
return self._handler
|
|
|
|
def _getUserHandler(self):
|
|
# TODO - JD: This should go away once plugin support is in place.
|
|
try:
|
|
bindings = settings.keyBindingsMap.get(self._script.__module__)
|
|
except Exception:
|
|
bindings = None
|
|
if not bindings:
|
|
try:
|
|
bindings = settings.keyBindingsMap.get("default")
|
|
except Exception:
|
|
bindings = None
|
|
|
|
try:
|
|
handler = bindings.getInputHandler(self)
|
|
except Exception:
|
|
handler = None
|
|
|
|
return handler
|
|
|
|
def shouldConsume(self):
|
|
"""Returns True if this event should be consumed."""
|
|
|
|
if not self.timestamp:
|
|
return False, 'No timestamp'
|
|
|
|
if not self._script:
|
|
return False, 'No active script when received'
|
|
|
|
if self.is_duplicate:
|
|
return False, 'Is duplicate'
|
|
|
|
if cthulhu_state.capturingKeys:
|
|
return False, 'Capturing keys'
|
|
|
|
if cthulhu_state.bypassNextCommand:
|
|
return False, 'Bypass next command'
|
|
|
|
self._handler = self._getUserHandler() \
|
|
or self._script.keyBindings.getInputHandler(self)
|
|
|
|
# TODO - JD: Right now we need to always call consumesKeyboardEvent()
|
|
# because that method is updating state, even in instances where there
|
|
# is no handler.
|
|
scriptConsumes = self._script.consumesKeyboardEvent(self)
|
|
|
|
if self._isReleaseForLastNonModifierKeyEvent():
|
|
return scriptConsumes, 'Is release for last non-modifier keyevent'
|
|
|
|
if self._script.learnModePresenter.is_active():
|
|
self._consumer = self._script.learnModePresenter.handle_event
|
|
return True, 'In Learn Mode'
|
|
|
|
if self.isModifierKey():
|
|
if not self.isCthulhuModifier():
|
|
return False, 'Non-Cthulhu modifier not in Learn Mode'
|
|
return True, 'Cthulhu modifier'
|
|
|
|
if not self._handler:
|
|
return False, 'No handler'
|
|
|
|
return scriptConsumes, 'Script indication'
|
|
|
|
def didConsume(self):
|
|
"""Returns True if this event was consumed."""
|
|
|
|
if self._did_consume is not None:
|
|
return self._did_consume
|
|
|
|
return False
|
|
|
|
def isHandledBy(self, method):
|
|
if not self._handler:
|
|
return False
|
|
|
|
return method.__func__ == self._handler.function
|
|
|
|
def _present(self, inputEvent=None):
|
|
if self.isPressedKey():
|
|
self._script.presentationInterrupt()
|
|
|
|
if self._script.learnModePresenter.is_active():
|
|
return False
|
|
|
|
return self._script.presentKeyboardEvent(self)
|
|
|
|
def process(self):
|
|
"""Processes this input event."""
|
|
|
|
startTime = time.time()
|
|
if not self._shouldObscure():
|
|
data = "'%s' (%d)" % (self.event_string, self.hw_code)
|
|
else:
|
|
data = "(obscured)"
|
|
|
|
if self.is_duplicate:
|
|
data = '%s DUPLICATE EVENT #%i' % (data, KeyboardEvent.duplicateCount)
|
|
|
|
msg = f'\nvvvvv PROCESS {self.type.value_name.upper()}: {data} vvvvv'
|
|
debug.printMessage(debug.LEVEL_INFO, msg, False)
|
|
|
|
tokens = ["HOST_APP:", self._app]
|
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
|
|
|
tokens = ["WINDOW:", self._window]
|
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
|
|
|
tokens = ["LOCATION:", self._obj]
|
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
|
|
|
tokens = ["CONSUME:", self._should_consume, self._consume_reason]
|
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
|
|
|
self._did_consume, self._result_reason = self._process()
|
|
|
|
if self._should_consume != self._did_consume:
|
|
tokens = ["CONSUMED:", self._did_consume, self._result_reason]
|
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
|
|
|
if debug.LEVEL_INFO >= debug.debugLevel and cthulhu_state.activeScript:
|
|
attributes = cthulhu_state.activeScript.getTransferableAttributes()
|
|
for key, value in attributes.items():
|
|
msg = f"INPUT EVENT: {key}: {value}"
|
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
|
|
msg = f"TOTAL PROCESSING TIME: {time.time() - startTime:.4f}"
|
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
|
|
msg = f"^^^^^ PROCESS {self.type.value_name.upper()}: {data} ^^^^^\n"
|
|
debug.printMessage(debug.LEVEL_INFO, msg, False)
|
|
|
|
return self._did_consume
|
|
|
|
def _process(self):
|
|
"""Processes this input event."""
|
|
|
|
if self._bypassCthulhu:
|
|
if (self.event_string == "Caps_Lock" \
|
|
or self.event_string == "Shift_Lock") \
|
|
and self.type == Atspi.EventType.KEY_PRESSED_EVENT:
|
|
self._lock_mod()
|
|
self.keyType = KeyboardEvent.TYPE_LOCKING
|
|
self._present()
|
|
return False, 'Bypassed cthulhu modifier'
|
|
|
|
cthulhu_state.lastInputEvent = self
|
|
if not self.isModifierKey():
|
|
cthulhu_state.lastNonModifierKeyEvent = self
|
|
|
|
if not self._script:
|
|
return False, 'No active script'
|
|
|
|
if self.is_duplicate:
|
|
return False, 'Is duplicate'
|
|
|
|
self._present()
|
|
|
|
if not self.isPressedKey():
|
|
return self._should_consume, 'Consumed based on handler'
|
|
|
|
if cthulhu_state.capturingKeys:
|
|
return False, 'Capturing keys'
|
|
|
|
if self.isCthulhuModifier():
|
|
return True, 'Cthulhu modifier'
|
|
|
|
if cthulhu_state.bypassNextCommand:
|
|
if not self.isModifierKey():
|
|
cthulhu_state.bypassNextCommand = False
|
|
self._script.addKeyGrabs()
|
|
return False, 'Bypass next command'
|
|
|
|
if not self._should_consume:
|
|
return False, 'Should not consume'
|
|
|
|
if not (self._consumer or self._handler):
|
|
return False, 'No consumer or handler'
|
|
|
|
if self._consumer or self._handler.function:
|
|
GLib.timeout_add(1, self._consume)
|
|
return True, 'Will be consumed'
|
|
|
|
return False, 'Unaddressed case'
|
|
|
|
def _lock_mod(self):
|
|
def lock_mod(modifiers, modifier):
|
|
def lockit():
|
|
try:
|
|
if modifiers & modifier:
|
|
lock = Atspi.KeySynthType.UNLOCKMODIFIERS
|
|
debug.printMessage(debug.LEVEL_INFO, "Unlocking capslock", True)
|
|
else:
|
|
lock = Atspi.KeySynthType.LOCKMODIFIERS
|
|
debug.printMessage(debug.LEVEL_INFO, "Locking capslock", True)
|
|
Atspi.generate_keyboard_event(modifier, "", lock)
|
|
debug.printMessage(debug.LEVEL_INFO, "Done with capslock", True)
|
|
except Exception:
|
|
debug.printMessage(debug.LEVEL_INFO, "Could not trigger capslock, " \
|
|
"at-spi2-core >= 2.32 is needed for triggering capslock", True)
|
|
pass
|
|
return lockit
|
|
if self.event_string == "Caps_Lock":
|
|
modifier = 1 << Atspi.ModifierType.SHIFTLOCK
|
|
elif self.event_string == "Shift_Lock":
|
|
modifier = 1 << Atspi.ModifierType.SHIFT
|
|
else:
|
|
tokens = ["Unknown locking key", self.event_string]
|
|
debug.printTokens(debug.LEVEL_WARNING, tokens, True)
|
|
return
|
|
debug.printMessage(debug.LEVEL_INFO, "Scheduling capslock", True)
|
|
GLib.timeout_add(1, lock_mod(self.modifiers, modifier))
|
|
|
|
def _consume(self):
|
|
startTime = time.time()
|
|
data = "'%s' (%d)" % (self.event_string, self.hw_code)
|
|
msg = f'vvvvv CONSUME {self.type.value_name.upper()}: {data} vvvvv'
|
|
debug.printMessage(debug.LEVEL_INFO, msg, False)
|
|
|
|
if self._consumer:
|
|
msg = f'INFO: Consumer is {self._consumer.__name__}'
|
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
self._consumer(self)
|
|
elif self._handler.function:
|
|
msg = f'INFO: Handler is {self._handler.description}'
|
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
self._handler.function(self._script, self)
|
|
else:
|
|
msg = 'INFO: No handler or consumer'
|
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
|
|
msg = f'TOTAL PROCESSING TIME: {time.time() - startTime:.4f}'
|
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
|
|
msg = f'^^^^^ CONSUME {self.type.value_name.upper()}: {data} ^^^^^'
|
|
debug.printMessage(debug.LEVEL_INFO, msg, False)
|
|
|
|
return False
|
|
|
|
class BrailleEvent(InputEvent):
|
|
|
|
def __init__(self, event):
|
|
"""Creates a new InputEvent of type BRAILLE_EVENT.
|
|
|
|
Arguments:
|
|
- event: the integer BrlTTY command for this event.
|
|
"""
|
|
super().__init__(BRAILLE_EVENT)
|
|
self.event = event
|
|
|
|
class MouseButtonEvent(InputEvent):
|
|
|
|
try:
|
|
display = Gdk.Display.get_default()
|
|
seat = Gdk.Display.get_default_seat(display)
|
|
_pointer = seat.get_pointer()
|
|
except Exception:
|
|
_pointer = None
|
|
|
|
def __init__(self, event):
|
|
"""Creates a new InputEvent of type MOUSE_BUTTON_EVENT."""
|
|
|
|
super().__init__(MOUSE_BUTTON_EVENT)
|
|
self.x = event.detail1
|
|
self.y = event.detail2
|
|
self.pressed = event.type.endswith('p')
|
|
self.button = event.type[len("mouse:button:"):-1]
|
|
self._script = cthulhu_state.activeScript
|
|
self.window = cthulhu_state.activeWindow
|
|
self.obj = None
|
|
|
|
if self.pressed:
|
|
self._validateCoordinates()
|
|
|
|
if not self._script:
|
|
return
|
|
|
|
if not self._script.utilities.canBeActiveWindow(self.window):
|
|
self.window = self._script.utilities.activeWindow()
|
|
|
|
if not self.window:
|
|
return
|
|
|
|
self.obj = self._script.utilities.descendantAtPoint(
|
|
self.window, self.x, self.y, event.any_data)
|
|
|
|
def _validateCoordinates(self):
|
|
if not self._pointer:
|
|
return
|
|
|
|
screen, x, y = self._pointer.get_position()
|
|
if math.sqrt((self.x - x)**2 + (self.y - y)**2) < 25:
|
|
return
|
|
|
|
msg = (
|
|
f"WARNING: Event coordinates ({self.x}, {self.y}) may be bogus. "
|
|
f"Updating to ({x}, {y})"
|
|
)
|
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
self.x, self.y = x, y
|
|
|
|
def setClickCount(self):
|
|
"""Updates the count of the number of clicks a user has made."""
|
|
|
|
if not self.pressed:
|
|
return
|
|
|
|
lastInputEvent = cthulhu_state.lastInputEvent
|
|
if not isinstance(lastInputEvent, MouseButtonEvent):
|
|
self._clickCount = 1
|
|
return
|
|
|
|
if self.time - lastInputEvent.time < settings.doubleClickTimeout \
|
|
and lastInputEvent.button == self.button:
|
|
if self._clickCount < 2:
|
|
self._clickCount += 1
|
|
return
|
|
|
|
self._clickCount = 1
|
|
|
|
|
|
class InputEventHandler:
|
|
|
|
def __init__(self, function, description, learnModeEnabled=True):
|
|
"""Creates a new InputEventHandler instance. All bindings
|
|
(e.g., key bindings and braille bindings) will be handled
|
|
by an instance of an InputEventHandler.
|
|
|
|
Arguments:
|
|
- function: the function to call with an InputEvent instance as its
|
|
sole argument. The function is expected to return True
|
|
if it consumes the event; otherwise it should return
|
|
False
|
|
- description: a localized string describing what this InputEvent
|
|
does
|
|
- learnModeEnabled: if True, the description will be spoken and
|
|
brailled if learn mode is enabled. If False,
|
|
the function will be called no matter what.
|
|
"""
|
|
|
|
self.function = function
|
|
self.description = description
|
|
self.learnModeEnabled = learnModeEnabled
|
|
|
|
def __eq__(self, other):
|
|
"""Compares one input handler to another."""
|
|
|
|
if not other:
|
|
return False
|
|
|
|
return (self.function == other.function)
|
|
|
|
def processInputEvent(self, script, inputEvent):
|
|
"""Processes an input event.
|
|
|
|
This function is expected to return True if it consumes the
|
|
event; otherwise it is expected to return False.
|
|
|
|
Arguments:
|
|
- script: the script (if any) associated with this event
|
|
- inputEvent: the input event to pass to the function bound
|
|
to this InputEventHandler instance.
|
|
"""
|
|
|
|
consumed = False
|
|
try:
|
|
consumed = self.function(script, inputEvent)
|
|
except Exception:
|
|
debug.printException(debug.LEVEL_SEVERE)
|
|
|
|
return consumed
|