[orca] Use new AT-SPI device API for keyboard monitoring when available
- From: Joanmarie Diggs <joanied src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [orca] Use new AT-SPI device API for keyboard monitoring when available
- Date: Wed, 11 Aug 2021 09:46:41 +0000 (UTC)
commit 163d4162ddc2950950327370481c74ea6625120e
Author: Mike Gorse <mgorse suse com>
Date: Wed Aug 11 09:46:40 2021 +0000
Use new AT-SPI device API for keyboard monitoring when available
src/orca/event_manager.py | 72 ++++++++++++++++++++++++++++++++++++++++--
src/orca/input_event.py | 3 ++
src/orca/keybindings.py | 41 ++++++++++++++++++++++++
src/orca/orca.py | 19 +++++++++++
src/orca/orca_state.py | 9 ++++++
src/orca/script_utilities.py | 10 ++++++
src/orca/scripts/default.py | 61 +++++++++++++++++++++++++++++++++++
src/orca/scripts/web/script.py | 29 ++++++++++++++++-
8 files changed, 241 insertions(+), 3 deletions(-)
---
diff --git a/src/orca/event_manager.py b/src/orca/event_manager.py
index e887c83e7..27e26f3dc 100644
--- a/src/orca/event_manager.py
+++ b/src/orca/event_manager.py
@@ -25,6 +25,9 @@ __copyright__ = "Copyright (c) 2011. Orca Team."
__license__ = "LGPL"
from gi.repository import GLib
+import gi
+gi.require_version('Atspi', '2.0')
+from gi.repository import Atspi
import pyatspi
import queue
import threading
@@ -62,16 +65,48 @@ class EventManager:
'object:state-changed:defunct',
'object:property-change:accessible-parent']
self._parentsOfDefunctDescendants = []
+
+ orca_state.device = None
+ self.newKeyHandlingActive = False
+ self.legacyKeyHandlingActive = False
+ self.forceLegacyKeyHandling = False
+
debug.println(debug.LEVEL_INFO, 'Event manager initialized', True)
def activate(self):
"""Called when this event manager is activated."""
debug.println(debug.LEVEL_INFO, 'EVENT MANAGER: Activating', True)
- self.registerKeystrokeListener(self._processKeyboardEvent)
+ self.setKeyHandling(False)
+
self._active = True
debug.println(debug.LEVEL_INFO, 'EVENT MANAGER: Activated', True)
+ def activateNewKeyHandling(self):
+ if not self.newKeyHandlingActive:
+ try:
+ orca_state.device = Atspi.Device.new()
+ except:
+ self.forceLegacyKeyHandling = True
+ activateLegacyKeyHandling(self)
+ return
+ orca_state.device.event_count = 0
+ orca_state.device.key_watcher = orca_state.device.add_key_watcher(self._processNewKeyboardEvent)
+ self.newKeyHandlingActive = True
+
+ def activateLegacyKeyHandling(self):
+ if not self.legacyKeyHandlingActive:
+ self.registerKeystrokeListener(self._processKeyboardEvent)
+ self.legacyKeyHandlingActive = True
+
+ def setKeyHandling(self, new):
+ if new and not self.forceLegacyKeyHandling:
+ self.deactivateLegacyKeyHandling()
+ self.activateNewKeyHandling()
+ else:
+ self.deactivateNewKeyHandling()
+ self.activateLegacyKeyHandling()
+
def deactivate(self):
"""Called when this event manager is deactivated."""
@@ -80,9 +115,19 @@ class EventManager:
for eventType in self._scriptListenerCounts.keys():
self.registry.deregisterEventListener(self._enqueue, eventType)
self._scriptListenerCounts = {}
- self.deregisterKeystrokeListener(self._processKeyboardEvent)
+ self.deactivateLegacyKeyHandling()
debug.println(debug.LEVEL_INFO, 'EVENT MANAGER: Deactivated', True)
+ def deactivateNewKeyHandling(self):
+ if self.newKeyHandlingActive:
+ orca_state.device = None
+ self.newKeyHandlingActive = False;
+
+ def deactivateLegacyKeyHandling(self):
+ if self.legacyKeyHandlingActive:
+ self.deregisterKeystrokeListener(self._processKeyboardEvent)
+ self.legacyKeyHandlingActive = False;
+
def ignoreEventTypes(self, eventTypeList):
for eventType in eventTypeList:
if not eventType in self._ignoredEvents:
@@ -954,6 +999,29 @@ class EventManager:
msg = 'EVENT MANAGER: %s: %s' % (key, value)
debug.println(debug.LEVEL_INFO, msg, True)
+ def _processNewKeyboardEvent(self, device, pressed, keycode, keysym, state, text):
+ event = Atspi.DeviceEvent()
+ if pressed:
+ event.type = pyatspi.KEY_PRESSED_EVENT
+ else:
+ event.type = pyatspi.KEY_RELEASED_EVENT
+ event.hw_code = keycode
+ event.id = keysym
+ event.modifiers = state
+ event.event_string = text
+ if event.event_string is None:
+ event.event_string = ""
+ event.timestamp = device.event_count
+ device.event_count = device.event_count + 1
+
+ if not pressed and text == "Num_Lock" and "KP_Insert" in settings.orcaModifierKeys and
orca_state.activeSWcript is not None:
+ orca_state.activeScript.refreshKeyGrabs()
+
+ if pressed:
+ orca_state.openingDialog = (text == "space" and (state & ~(1 << pyatspi.MODIFIER_NUMLOCK)))
+
+ self._processKeyboardEvent(event)
+
def _processKeyboardEvent(self, event):
keyboardEvent = input_event.KeyboardEvent(event)
if not keyboardEvent.is_duplicate:
diff --git a/src/orca/input_event.py b/src/orca/input_event.py
index d230b4888..24cc9abca 100644
--- a/src/orca/input_event.py
+++ b/src/orca/input_event.py
@@ -236,6 +236,8 @@ class KeyboardEvent(InputEvent):
self.modifiers |= (1 << pyatspi.MODIFIER_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 [orca_state.lastInputEvent,
orca_state.lastNonModifierKeyEvent]
@@ -919,6 +921,7 @@ class KeyboardEvent(InputEvent):
if orca_state.bypassNextCommand:
if not self.isModifierKey():
orca_state.bypassNextCommand = False
+ self._script.addKeyGrabs()
return False, 'Bypass next command'
if not self._should_consume:
diff --git a/src/orca/keybindings.py b/src/orca/keybindings.py
index cf5eb287d..186f061b8 100644
--- a/src/orca/keybindings.py
+++ b/src/orca/keybindings.py
@@ -28,11 +28,16 @@ __license__ = "LGPL"
from gi.repository import Gdk
+import gi
+gi.require_version('Atspi', '2.0')
+from gi.repository import Atspi
+
import functools
import pyatspi
from . import debug
from . import settings
+from . import orca_state
from .orca_i18n import _
@@ -265,6 +270,42 @@ class KeyBinding:
return string.strip()
+ def keyDefs(self):
+ """ return a list of Atspi key definitions for the given binding.
+ This may return more than one binding if the Orca modifier is bound
+ to more than one key.
+ If AT-SPI is older than 2.40, then this function will not work and
+ will return an empty set.
+ """
+ ret = []
+ if not self.keycode:
+ self.keycode = getKeycode(self.keysymstring)
+
+ if self.modifiers & ORCA_MODIFIER_MASK:
+ device = orca_state.device
+ if device is None:
+ return ret
+ modList = []
+ otherMods = self.modifiers & ~ORCA_MODIFIER_MASK
+ numLockMod = device.get_modifier(getKeycode("Num_Lock"))
+ lockedMods = device.get_locked_modifiers()
+ numLockOn = lockedMods & numLockMod
+ for key in settings.orcaModifierKeys:
+ keycode = getKeycode(key)
+ if keycode == 0 and key == "Shift_Lock":
+ keycode = getKeycode("Caps_Lock")
+ mod = device.map_modifier(keycode)
+ if key != "KP_Insert" or not numLockOn:
+ modList.append(mod | otherMods)
+ else:
+ modList = [self.modifiers]
+ for mod in modList:
+ kd = Atspi.KeyDefinition()
+ kd.keycode = self.keycode
+ kd.modifiers = mod
+ ret.append(kd)
+ return ret
+
class KeyBindings:
"""Structure that maintains a set of KeyBinding instances.
"""
diff --git a/src/orca/orca.py b/src/orca/orca.py
index 2fe0a0bf2..2eee3eec9 100644
--- a/src/orca/orca.py
+++ b/src/orca/orca.py
@@ -379,6 +379,11 @@ def _restoreXmodmap(keyList=[]):
stdin=subprocess.PIPE, stdout=None, stderr=None)
p.communicate(_originalXmodmap)
+def setKeyHandling(new):
+ """Toggle use of the new vs. legacy key handling mode.
+ """
+ _eventManager.setKeyHandling(new)
+
def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False):
"""Loads (and reloads) the user settings module, reinitializing
things such as speech if necessary.
@@ -541,6 +546,20 @@ def helpForOrca(script=None, inputEvent=None, page=""):
Gtk.get_current_event_time())
return True
+def addKeyGrab(binding):
+ """ Add a key grab for the given key binding. """
+ ret = []
+ for kd in binding.keyDefs():
+ ret.append(orca_state.device.add_key_grab(kd, None))
+ return ret
+
+def removeKeyGrab(id):
+ """ Remove the key grab for the given key binding. """
+ orca_state.device.remove_key_grab(id)
+
+def mapModifier(keycode):
+ return orca_state.device.map_modifier(keycode)
+
def quitOrca(script=None, inputEvent=None):
"""Quit Orca. Check if the user wants to confirm this action.
If so, show the confirmation GUI otherwise just shutdown.
diff --git a/src/orca/orca_state.py b/src/orca/orca_state.py
index 8fa1e8865..f6e61c526 100644
--- a/src/orca/orca_state.py
+++ b/src/orca/orca_state.py
@@ -79,3 +79,12 @@ learnModeEnabled = False
orcaOS = None
listNotificationsModeEnabled = False
+
+# Set to True if the last key opened the preferences dialog
+#
+openingDialog = False
+
+# The AT-SPI device (needed for key grabs). Will be set to None if AT-SPI
+# is too old to support the new device API.
+#
+device = None
diff --git a/src/orca/script_utilities.py b/src/orca/script_utilities.py
index f391c5add..fea0e7437 100644
--- a/src/orca/script_utilities.py
+++ b/src/orca/script_utilities.py
@@ -5723,8 +5723,18 @@ class Utilities:
debug.println(debug.LEVEL_INFO, msg, True)
return False
+ if self.isKeyGrabEvent(event):
+ msg = "INFO: Last key was consumed. Probably a bogus event from a key grab"
+ debug.println(debug.LEVEL_INFO, msg, True)
+ return False
+
return True
+ def isKeyGrabEvent(self, event):
+ """ Returns True if this event appears to be a side-effect of an
+ X11 key grab. """
+ return orca_state.lastInputEvent.didConsume() and not orca_state.openingDialog
+
def presentFocusChangeReason(self):
if self.handleUndoLocusOfFocusChange():
return True
diff --git a/src/orca/scripts/default.py b/src/orca/scripts/default.py
index 7156565bf..b891f9df4 100644
--- a/src/orca/scripts/default.py
+++ b/src/orca/scripts/default.py
@@ -34,6 +34,9 @@ import pyatspi
import re
import time
+import gi
+gi.require_version('Atspi', '2.0')
+from gi.repository import Atspi
import orca.braille as braille
import orca.cmdnames as cmdnames
import orca.debug as debug
@@ -122,6 +125,7 @@ class Script(script.Script):
self._inSayAll = False
self._sayAllIsInterrupted = False
self._sayAllContexts = []
+ self.grab_ids = []
if app:
app.setCacheMask(pyatspi.cache.DEFAULT ^ pyatspi.cache.NAME ^ pyatspi.cache.DESCRIPTION)
@@ -741,6 +745,39 @@ class Script(script.Script):
self._sayAllIsInterrupted = False
self.pointOfReference = {}
+ self.removeKeyGrabs()
+
+ def getEnabledKeyBindings(self):
+ """ Returns the key bindings that are currently active. """
+ return self.getKeyBindings().getBoundBindings()
+
+ def addKeyGrabs(self):
+ """ Sets up the key grabs currently needed by this script. """
+ if orca_state.device is None:
+ return
+ msg = "INFO: adding key grabs"
+ debug.println(debug.LEVEL_INFO, msg, True)
+ bound = self.getEnabledKeyBindings()
+ for b in bound:
+ for id in orca.addKeyGrab(b):
+ self.grab_ids.append(id)
+
+ def removeKeyGrabs(self):
+ """ Removes this script's AT-SPI key grabs. """
+ msg = "INFO: removing key grabs"
+ debug.println(debug.LEVEL_INFO, msg, True)
+ for id in self.grab_ids:
+ orca.removeKeyGrab(id)
+ self.grab_ids = []
+
+ def refreshKeyGrabs(self):
+ """ Refreshes the enabled key grabs for this script. """
+ # TODO: Should probably avoid removing key grabs and re-adding them.
+ # Otherwise, a key could conceivably leak through while the script is
+ # in the process of updating the bindings.
+ self.removeKeyGrabs()
+ self.addKeyGrabs()
+
def registerEventListeners(self):
super().registerEventListeners()
self.utilities.connectToClipboard()
@@ -873,6 +910,15 @@ class Script(script.Script):
speech.updatePunctuationLevel()
speech.updateCapitalizationStyle()
+ # Gtk 4 requrns "GTK", while older versions return "gtk"
+ # TODO: move this to a toolkit-specific script
+ if self.app is not None and self.app.toolkitName == "GTK" and self.app.toolkitVersion > "4":
+ orca.setKeyHandling(True)
+ else:
+ orca.setKeyHandling(False)
+
+ self.addKeyGrabs()
+
msg = 'DEFAULT: Script for %s activated' % self.app
debug.println(debug.LEVEL_INFO, msg, True)
@@ -924,6 +970,7 @@ class Script(script.Script):
self.presentMessage(messages.BYPASS_MODE_ENABLED)
orca_state.bypassNextCommand = True
+ self.removeKeyGrabs()
return True
def enterLearnMode(self, inputEvent=None):
@@ -940,6 +987,8 @@ class Script(script.Script):
self.speakMessage(messages.LEARN_MODE_START_SPEECH)
self.displayBrailleMessage(messages.LEARN_MODE_START_BRAILLE)
orca_state.learnModeEnabled = True
+ if orca_state.device is not None:
+ Atspi.Device.grab_keyboard(orca_state.device)
return True
def exitLearnMode(self, inputEvent=None):
@@ -957,6 +1006,8 @@ class Script(script.Script):
self.presentMessage(messages.LEARN_MODE_STOP)
orca_state.learnModeEnabled = False
+ if orca_state.device is not None:
+ Atspi.Device.ungrab_keyboard(orca_state.device)
return True
def showHelp(self, inputEvent=None):
@@ -2903,6 +2954,11 @@ class Script(script.Script):
self.windowActivateTime = time.time()
orca_state.activeWindow = event.source
+ if self.utilities.isKeyGrabEvent(event):
+ msg = "DEFAULT: Ignoring event. Likely from key grab."
+ debug.println(debug.LEVEL_INFO, msg, True)
+ return
+
try:
childCount = event.source.childCount
childRole = event.source[0].getRole()
@@ -2942,6 +2998,11 @@ class Script(script.Script):
debug.println(debug.LEVEL_INFO, msg, True)
return
+ if self.utilities.isKeyGrabEvent(event):
+ msg = "DEFAULT: Ignoring event. Likely from key grab."
+ debug.println(debug.LEVEL_INFO, msg, True)
+ return
+
self.presentationInterrupt()
self.clearBraille()
diff --git a/src/orca/scripts/web/script.py b/src/orca/scripts/web/script.py
index 204f0c489..a64dc7b14 100644
--- a/src/orca/scripts/web/script.py
+++ b/src/orca/scripts/web/script.py
@@ -127,6 +127,7 @@ class Script(default.Script):
self._preMouseOverContext = None, -1
self._inMouseOverObject = False
self.utilities.clearCachedObjects()
+ self.removeKeyGrabs()
def getAppKeyBindings(self):
"""Returns the application-specific keybindings for this script."""
@@ -570,6 +571,24 @@ class Script(default.Script):
return super().consumesKeyboardEvent(keyboardEvent)
+ def getEnabledKeyBindings(self):
+ all = super().getEnabledKeyBindings()
+ ret = []
+ for b in all:
+ if b.handler and self.caretNavigation.handles_navigation(b.handler):
+ if self.useCaretNavigationModel(None):
+ ret.append(b)
+ elif b.handler and b.handler.function in self.structuralNavigation.functions:
+ if self.useStructuralNavigationModel():
+ ret.append(b)
+ elif b.handler and b.handler.function in self.liveRegionManager.functions:
+ # This is temporary.
+ if self.useStructuralNavigationModel():
+ ret.append(b)
+ else:
+ ret.append(b)
+ return ret
+
def consumesBrailleEvent(self, brailleEvent):
"""Returns True if the script will consume this braille event."""
@@ -1120,7 +1139,7 @@ class Script(default.Script):
if not self.utilities.inDocumentContent():
return False
- if keyboardEvent.modifiers & keybindings.SHIFT_MODIFIER_MASK:
+ if keyboardEvent and keyboardEvent.modifiers & keybindings.SHIFT_MODIFIER_MASK:
return False
return True
@@ -1217,6 +1236,7 @@ class Script(default.Script):
self._inFocusMode = False
self._focusModeIsSticky = False
self._browseModeIsSticky = True
+ self.refreshKeyGrabs()
def enableStickyFocusMode(self, inputEvent, forceMessage=False):
if not self._focusModeIsSticky or forceMessage:
@@ -1225,6 +1245,7 @@ class Script(default.Script):
self._inFocusMode = True
self._focusModeIsSticky = True
self._browseModeIsSticky = False
+ self.refreshKeyGrabs()
def toggleLayoutMode(self, inputEvent):
layoutMode = not _settingsManager.getSetting('layoutMode')
@@ -1258,6 +1279,7 @@ class Script(default.Script):
self._inFocusMode = not self._inFocusMode
self._focusModeIsSticky = False
self._browseModeIsSticky = False
+ self.refreshKeyGrabs()
def locusOfFocusChanged(self, event, oldFocus, newFocus):
"""Handles changes of focus of interest to the script."""
@@ -1276,6 +1298,7 @@ class Script(default.Script):
self._madeFindAnnouncement = False
self._inFocusMode = False
debug.println(debug.LEVEL_INFO, msg, True)
+ self.refreshKeyGrabs()
return False
if self.flatReviewContext:
@@ -1368,6 +1391,9 @@ class Script(default.Script):
and self.useFocusMode(newFocus, oldFocus) != self._inFocusMode:
self.togglePresentationMode(None, document)
+ if not self.utilities.inDocumentContent(oldFocus):
+ self.refreshKeyGrabs()
+
return True
def onActiveChanged(self, event):
@@ -2495,6 +2521,7 @@ class Script(default.Script):
self._lastCommandWasStructNav = False
self._lastCommandWasMouseButton = False
self._lastMouseButtonContext = None, -1
+ self.removeKeyGrabs()
return False
def getTransferableAttributes(self):
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]