[orca] Use new AT-SPI device API for keyboard monitoring when available



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]