diff --git a/configure.ac b/configure.ac index c26fc97..b3356e3 100644 --- a/configure.ac +++ b/configure.ac @@ -131,10 +131,8 @@ src/cthulhu/plugins/PluginManager/Makefile src/cthulhu/plugins/Clipboard/Makefile src/cthulhu/plugins/DisplayVersion/Makefile src/cthulhu/plugins/hello_world/Makefile -src/cthulhu/plugins/CapsLockHack/Makefile src/cthulhu/plugins/self_voice/Makefile src/cthulhu/plugins/Time/Makefile -src/cthulhu/plugins/MouseReview/Makefile src/cthulhu/plugins/SimplePluginSystem/Makefile src/cthulhu/backends/Makefile src/cthulhu/cthulhu_bin.py diff --git a/src/cthulhu/plugins/CapsLockHack/CapsLockHack.plugin b/src/cthulhu/plugins/CapsLockHack/CapsLockHack.plugin deleted file mode 100644 index 6279400..0000000 --- a/src/cthulhu/plugins/CapsLockHack/CapsLockHack.plugin +++ /dev/null @@ -1,6 +0,0 @@ -[Plugin] -Module=CapsLockHack -Loader=python3 -Name=Caps Lock Hack -Description=Fix Capslock sometimes switch on / off when its used as modifier -Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/CapsLockHack/CapsLockHack.py b/src/cthulhu/plugins/CapsLockHack/CapsLockHack.py deleted file mode 100644 index a616ac2..0000000 --- a/src/cthulhu/plugins/CapsLockHack/CapsLockHack.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2024 Stormux -# Copyright (c) 2010-2012 The Orca Team -# Copyright (c) 2012 Igalia, S.L. -# Copyright (c) 2005-2010 Sun Microsystems Inc. -# -# 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. -# -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca - -from cthulhu import plugin - -import gi -gi.require_version('Peas', '1.0') -from gi.repository import GObject -from gi.repository import Peas -from threading import Thread, Lock -import subprocess, time, re, os - -class CapsLockHack(GObject.Object, Peas.Activatable, plugin.Plugin): - __gtype_name__ = 'CapsLockHack' - - object = GObject.Property(type=GObject.Object) - def __init__(self): - plugin.Plugin.__init__(self) - self.lock = Lock() - self.active = False - self.workerThread = Thread(target=self.worker) - def do_activate(self): - API = self.object - """Enable or disable use of the caps lock key as an Cthulhu modifier key.""" - self.interpretCapsLineProg = re.compile( - r'^\s*interpret\s+Caps[_+]Lock[_+]AnyOfOrNone\s*\(all\)\s*{\s*$', re.I) - self.normalCapsLineProg = re.compile( - r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Lock\s*\)\s*;\s*$', re.I) - self.interpretShiftLineProg = re.compile( - r'^\s*interpret\s+Shift[_+]Lock[_+]AnyOf\s*\(\s*Shift\s*\+\s*Lock\s*\)\s*{\s*$', re.I) - self.normalShiftLineProg = re.compile( - r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Shift\s*\)\s*;\s*$', re.I) - self.disabledModLineProg = re.compile( - r'^\s*action\s*=\s*NoAction\s*\(\s*\)\s*;\s*$', re.I) - self.normalCapsLine = ' action= LockMods(modifiers=Lock);' - self.normalShiftLine = ' action= LockMods(modifiers=Shift);' - self.disabledModLine = ' action= NoAction();' - self.activateWorker() - def do_deactivate(self): - API = self.object - self.deactivateWorker() - def do_update_state(self): - API = self.object - def deactivateWorker(self): - with self.lock: - self.active = False - self.workerThread.join() - def activateWorker(self): - with self.lock: - self.active = True - self.workerThread.start() - def isActive(self): - with self.lock: - return self.active - def worker(self): - """Makes an Cthulhu-specific Xmodmap so that the keys behave as we - need them to do. This is especially the case for the Cthulhu modifier. - """ - API = self.object - capsLockCleared = False - settings = API.app.getDynamicApiManager().getAPI('Settings') - time.sleep(3) - while self.isActive(): - if "Caps_Lock" in settings.cthulhuModifierKeys \ - or "Shift_Lock" in settings.cthulhuModifierKeys: - self.setCapsLockAsCthulhuModifier(True) - capsLockCleared = True - elif capsLockCleared: - self.setCapsLockAsCthulhuModifier(False) - capsLockCleared = False - time.sleep(1) - - def setCapsLockAsCthulhuModifier(self, enable): - originalXmodmap = None - lines = None - try: - originalXmodmap = subprocess.check_output(['xkbcomp', os.environ['DISPLAY'], '-']) - lines = originalXmodmap.decode('UTF-8').split('\n') - except: - return - foundCapsInterpretSection = False - foundShiftInterpretSection = False - modified = False - for i, line in enumerate(lines): - if not foundCapsInterpretSection and not foundShiftInterpretSection: - if self.interpretCapsLineProg.match(line): - foundCapsInterpretSection = True - elif self.interpretShiftLineProg.match(line): - foundShiftInterpretSection = True - elif foundCapsInterpretSection: - if enable: - if self.normalCapsLineProg.match(line): - lines[i] = self.disabledModLine - modified = True - else: - if self.disabledModLineProg.match(line): - lines[i] = self.normalCapsLine - modified = True - if line.find('}'): - foundCapsInterpretSection = False - else: # foundShiftInterpretSection - if enable: - if self.normalShiftLineProg.match(line): - lines[i] = self.disabledModLine - modified = True - else: - if self.disabledModLineProg.match(line): - lines[i] = self.normalShiftLine - modified = True - if line.find('}'): - foundShiftInterpretSection = False - if modified: - newXmodMap = bytes('\n'.join(lines), 'UTF-8') - self.setXmodmap(newXmodMap) - def setXmodmap(self, xkbmap): - """Set the keyboard map using xkbcomp.""" - try: - p = subprocess.Popen(['xkbcomp', '-w0', '-', os.environ['DISPLAY']], - stdin=subprocess.PIPE, stdout=None, stderr=None) - p.communicate(xkbmap) - except: - pass diff --git a/src/cthulhu/plugins/CapsLockHack/Makefile.am b/src/cthulhu/plugins/CapsLockHack/Makefile.am deleted file mode 100644 index c687cc7..0000000 --- a/src/cthulhu/plugins/CapsLockHack/Makefile.am +++ /dev/null @@ -1,7 +0,0 @@ -cthulhu_python_PYTHON = \ - __init__.py \ - CapsLockHack.plugin \ - CapsLockHack.py - -cthulhu_pythondir=$(pkgpythondir)/plugins/CapsLockHack - diff --git a/src/cthulhu/plugins/CapsLockHack/__init__.py b/src/cthulhu/plugins/CapsLockHack/__init__.py deleted file mode 100644 index 782103c..0000000 --- a/src/cthulhu/plugins/CapsLockHack/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2024 Stormux -# Copyright (c) 2010-2012 The Orca Team -# Copyright (c) 2012 Igalia, S.L. -# Copyright (c) 2005-2010 Sun Microsystems Inc. -# -# 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. -# -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca - diff --git a/src/cthulhu/plugins/Makefile.am b/src/cthulhu/plugins/Makefile.am index 042fc2b..b90e9d5 100644 --- a/src/cthulhu/plugins/Makefile.am +++ b/src/cthulhu/plugins/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = Clipboard DisplayVersion hello_world self_voice Time MouseReview ByeCthulhu HelloCthulhu PluginManager CapsLockHack SimplePluginSystem +SUBDIRS = Clipboard DisplayVersion hello_world self_voice Time ByeCthulhu HelloCthulhu PluginManager SimplePluginSystem cthulhu_pythondir=$(pkgpythondir)/plugins diff --git a/src/cthulhu/plugins/MouseReview/Makefile.am b/src/cthulhu/plugins/MouseReview/Makefile.am deleted file mode 100644 index e7d9089..0000000 --- a/src/cthulhu/plugins/MouseReview/Makefile.am +++ /dev/null @@ -1,7 +0,0 @@ -cthulhu_python_PYTHON = \ - __init__.py \ - MouseReview.plugin \ - MouseReview.py - -cthulhu_pythondir=$(pkgpythondir)/plugins/MouseReview - diff --git a/src/cthulhu/plugins/MouseReview/MouseReview.plugin b/src/cthulhu/plugins/MouseReview/MouseReview.plugin deleted file mode 100644 index 986dfad..0000000 --- a/src/cthulhu/plugins/MouseReview/MouseReview.plugin +++ /dev/null @@ -1,6 +0,0 @@ -[Plugin] -Module=MouseReview -Loader=python3 -Name=Mouse Review -Description=Review whats below the mouse coursor -Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/MouseReview/MouseReview.py b/src/cthulhu/plugins/MouseReview/MouseReview.py deleted file mode 100644 index ad409c2..0000000 --- a/src/cthulhu/plugins/MouseReview/MouseReview.py +++ /dev/null @@ -1,759 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2024 Stormux -# Copyright (c) 2010-2012 The Orca Team -# Copyright (c) 2012 Igalia, S.L. -# Copyright (c) 2005-2010 Sun Microsystems Inc. -# -# 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. -# -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca - -"""Mouse review mode.""" - -__id__ = "$Id$" -__version__ = "$Revision$" -__date__ = "$Date$" -__copyright__ = "Copyright (c) 2008 Eitan Isaacson" \ - "Copyright (c) 2016 Igalia, S.L." -__license__ = "LGPL" - -from cthulhu import plugin - -import gi, math, time -gi.require_version('Peas', '1.0') -from gi.repository import GObject -from gi.repository import Peas - -gi.require_version("Atspi", "2.0") -from gi.repository import Atspi - -from gi.repository import Gdk -try: - gi.require_version("Wnck", "3.0") - from gi.repository import Wnck - _mouseReviewCapable = True -except Exception: - _mouseReviewCapable = False - -# compatibility layer, see MouseReview.do_activate -debug = None -event_manager = None -cthulhu = None -cthulhu_state = None -script_manager = None -settings_manager = None -speech = None -messages = None -cmdnames = None -emitRegionChanged = None -_scriptManager = None -_settingsManager = None -AXObject = None -AXUtilities = None -keybindings = None -input_event = None - -class MouseReview(GObject.Object, Peas.Activatable, plugin.Plugin): - #__gtype_name__ = 'MouseReview' - - object = GObject.Property(type=GObject.Object) - def __init__(self): - plugin.Plugin.__init__(self) - def do_activate(self): - API = self.object - global _mouseReviewCapable - if not _mouseReviewCapable: - return - global debug - global event_manager - global cthulhu_state - global script_manager - global settings_manager - global speech - global _scriptManager - global _settingsManager - global emitRegionChanged - global messages - global cmdnames - global AXObject - global AXUtilities - global keybindings - global input_event - debug= API.app.getDynamicApiManager().getAPI('Debug') - event_manager = API.app.getDynamicApiManager().getAPI('EventManager') - messages = API.app.getDynamicApiManager().getAPI('Messages') - cmdnames = API.app.getDynamicApiManager().getAPI('Cmdnames') - cthulhu_state = API.app.getDynamicApiManager().getAPI('CthulhuState') - script_manager = API.app.getDynamicApiManager().getAPI('ScriptManager') - settings_manager = API.app.getDynamicApiManager().getAPI('SettingsManager') - speech = API.app.getDynamicApiManager().getAPI('Speech') - emitRegionChanged = API.app.getDynamicApiManager().getAPI('EmitRegionChanged') - _scriptManager = script_manager.getManager() - _settingsManager = settings_manager.getManager() - AXObject = API.app.getDynamicApiManager().getAPI('AXObject') - AXUtilities = API.app.getDynamicApiManager().getAPI('AXUtilities') - keybindings = API.app.getDynamicApiManager().getAPI('Keybindings') - input_event = API.app.getDynamicApiManager().getAPI('InputEvent') - mouse_review = MouseReviewer() - self.registerAPI('MouseReview', mouse_review) - self.Initialize(API.app) - self.connectSignal("setup-inputeventhandlers-completed", self.setupCompatBinding) - self.connectSignal("load-setting-completed", self.Initialize) - - def do_deactivate(self): - API = self.object - global _mouseReviewCapable - if not _mouseReviewCapable: - return - mouse_review = API.app.getDynamicApiManager().getAPI('MouseReview') - - mouse_review.deactivate() - def do_update_state(self): - API = self.object - def setupCompatBinding(self, app): - API = self.object - mouse_review = API.app.getDynamicApiManager().getAPI('MouseReview') - cmdnames = API.app.getDynamicApiManager().getAPI('Cmdnames') - inputEventHandlers = API.app.getDynamicApiManager().getAPI('inputEventHandlers') - inputEventHandlers['toggleMouseReviewHandler'] = API.app.getAPIHelper().createInputEventHandler(mouse_review.toggle, cmdnames.MOUSE_REVIEW_TOGGLE) - def Initialize(self, app): - mouse_review = app.getDynamicApiManager().getAPI('MouseReview') - settings_manager = app.getDynamicApiManager().getAPI('SettingsManager') - _settingsManager = settings_manager.getManager() - if _settingsManager.getSetting('enableMouseReview'): - mouse_review.activate() - else: - mouse_review.deactivate() - -class _StringContext: - """The textual information associated with an _ItemContext.""" - - def __init__(self, obj, script=None, string="", start=0, end=0): - """Initialize the _StringContext. - - Arguments: - - string: The human-consumable string - - obj: The accessible object associated with this string - - start: The start offset with respect to entire text, if one exists - - end: The end offset with respect to the entire text, if one exists - - script: The script associated with the accessible object - """ - - self._obj = obj - self._script = script - self._string = string - self._start = start - self._end = end - self._boundingBox = 0, 0, 0, 0 - if script: - self._boundingBox = script.utilities.getTextBoundingBox(obj, start, end) - - def __eq__(self, other): - return other is not None \ - and self._obj == other._obj \ - and self._string == other._string \ - and self._start == other._start \ - and self._end == other._end - - def isSubstringOf(self, other): - """Returns True if this is a substring of other.""" - - if other is None: - return False - - if not (self._obj and other._obj): - return False - - thisBox = self.getBoundingBox() - if thisBox == (0, 0, 0, 0): - return False - - otherBox = other.getBoundingBox() - if otherBox == (0, 0, 0, 0): - return False - - # We get various and sundry results for the bounding box if the implementor - # included newline characters as part of the word or line at offset. Try to - # detect this and adjust the bounding boxes before getting the intersection. - if thisBox[3] != otherBox[3] and self._obj == other._obj: - thisNewLineCount = self._string.count("\n") - if thisNewLineCount and thisBox[3] / thisNewLineCount == otherBox[3]: - thisBox = *thisBox[0:3], otherBox[3] - - if self._script.utilities.intersection(thisBox, otherBox) != thisBox: - return False - - if not (self._string and self._string.strip() in other._string): - return False - - msg = f"MOUSE REVIEW: '{self._string}' is substring of '{other._string}'" - debug.println(debug.LEVEL_INFO, msg, True) - return True - - def getBoundingBox(self): - """Returns the bounding box associated with this context's range.""" - - return self._boundingBox - - def getString(self): - """Returns the string associated with this context.""" - - return self._string - - def present(self): - """Presents this context to the user.""" - - if not self._script: - msg = "MOUSE REVIEW: Not presenting due to lack of script" - debug.println(debug.LEVEL_INFO, msg, True) - return False - - if not self._string: - msg = "MOUSE REVIEW: Not presenting due to lack of string" - debug.println(debug.LEVEL_INFO, msg, True) - return False - - voice = self._script.speechGenerator.voice(obj=self._obj, string=self._string) - string = self._script.utilities.adjustForRepeats(self._string) - # TODO - #cthulhu.emitRegionChanged(self._obj, self._start, self._end, cthulhu.MOUSE_REVIEW) - emitRegionChanged(self._obj, self._start, self._end, "mouse-review") - - - self._script.speakMessage(string, voice=voice, interrupt=False) - self._script.displayBrailleMessage(self._string, -1) - return True - - -class _ItemContext: - """Holds all the information of the item at a specified point.""" - - def __init__(self, x=0, y=0, obj=None, boundary=None, frame=None, script=None): - """Initialize the _ItemContext. - - Arguments: - - x: The X coordinate - - y: The Y coordinate - - obj: The accessible object of interest at that coordinate - - boundary: The accessible-text boundary type - - frame: The containing accessible object (often a top-level window) - - script: The script associated with the accessible object - """ - - self._x = x - self._y = y - self._obj = obj - self._boundary = boundary - self._frame = frame - self._script = script - self._string = self._getStringContext() - self._time = time.time() - self._boundingBox = 0, 0, 0, 0 - if script: - self._boundingBox = script.utilities.getBoundingBox(obj) - - def __eq__(self, other): - return other is not None \ - and self._frame == other._frame \ - and self._obj == other._obj \ - and self._string == other._string - - def _treatAsDuplicate(self, prior): - if self._obj != prior._obj or self._frame != prior._frame: - msg = "MOUSE REVIEW: Not a duplicate: different objects" - debug.println(debug.LEVEL_INFO, msg, True) - return False - - if self.getString() and prior.getString() and not self._isSubstringOf(prior): - msg = "MOUSE REVIEW: Not a duplicate: not a substring of" - debug.println(debug.LEVEL_INFO, msg, True) - return False - - if self._x == prior._x and self._y == prior._y: - msg = "MOUSE REVIEW: Treating as duplicate: mouse didn't move" - debug.println(debug.LEVEL_INFO, msg, True) - return True - - interval = self._time - prior._time - if interval > 0.5: - msg = f"MOUSE REVIEW: Not a duplicate: was {interval:.2f}s ago" - debug.println(debug.LEVEL_INFO, msg, True) - return False - - msg = "MOUSE REVIEW: Treating as duplicate" - debug.println(debug.LEVEL_INFO, msg, True) - return True - - def _treatAsSingleObject(self): - if not AXObject.supports_text(self._obj): - return True - - if not self._obj.queryText().characterCount: - return True - - return False - - def _getStringContext(self): - """Returns the _StringContext associated with the specified point.""" - - if not (self._script and self._obj): - return _StringContext(self._obj) - - if self._treatAsSingleObject(): - return _StringContext(self._obj, self._script) - - string, start, end = self._script.utilities.textAtPoint( - self._obj, self._x, self._y, boundary=self._boundary) - if string: - string = self._script.utilities.expandEOCs(self._obj, start, end) - - return _StringContext(self._obj, self._script, string, start, end) - - def _getContainer(self): - roles = [Atspi.Role.DIALOG, - Atspi.Role.FRAME, - Atspi.Role.LAYERED_PANE, - Atspi.Role.MENU, - Atspi.Role.PAGE_TAB, - Atspi.Role.TOOL_BAR, - Atspi.Role.WINDOW] - return AXObject.find_ancestor(self._obj, lambda x: AXObject.get_role(x) in roles) - - def _isSubstringOf(self, other): - """Returns True if this is a substring of other.""" - - return self._string.isSubstringOf(other._string) - - def getObject(self): - """Returns the accessible object associated with this context.""" - - return self._obj - - def getBoundingBox(self): - """Returns the bounding box associated with this context.""" - - x, y, width, height = self._string.getBoundingBox() - if not (width or height): - return self._boundingBox - - return x, y, width, height - - def getString(self): - """Returns the string associated with this context.""" - - return self._string.getString() - - def getTime(self): - """Returns the time associated with this context.""" - - return self._time - - def _isInlineChild(self, prior): - if not self._obj or not prior._obj: - return False - - if AXObject.get_parent(prior._obj) != self._obj: - return False - - if self._treatAsSingleObject(): - return False - - return AXUtilities.is_link(prior._obj) - - def present(self, prior): - """Presents this context to the user.""" - - if self == prior or self._treatAsDuplicate(prior): - msg = "MOUSE REVIEW: Not presenting due to no change" - debug.println(debug.LEVEL_INFO, msg, True) - return False - - interrupt = self._obj and self._obj != prior._obj \ - or math.sqrt((self._x - prior._x)**2 + (self._y - prior._y)**2) > 25 - - if interrupt: - self._script.presentationInterrupt() - - if self._frame and self._frame != prior._frame: - self._script.presentObject(self._frame, - alreadyFocused=True, - inMouseReview=True, - interrupt=True) - - if self._script.utilities.containsOnlyEOCs(self._obj): - msg = "MOUSE REVIEW: Not presenting object which contains only EOCs" - debug.println(debug.LEVEL_INFO, msg, True) - return False - - if self._obj and self._obj != prior._obj and not self._isInlineChild(prior): - priorObj = prior._obj or self._getContainer() - # TODO - #cthulhu.emitRegionChanged(self._obj, mode=cthulhu.MOUSE_REVIEW) - emitRegionChanged(self._obj, mode="mouse-review") - - self._script.presentObject(self._obj, priorObj=priorObj, inMouseReview=True) - if self._string.getString() == AXObject.get_name(self._obj): - return True - if not self._script.utilities.isEditableTextArea(self._obj): - return True - if AXUtilities.is_table_cell(self._obj) \ - and self._string.getString() == self._script.utilities.displayedText(self._obj): - return True - - if self._string != prior._string and self._string.present(): - return True - - return True - - -class MouseReviewer: - """Main class for the mouse-review feature.""" - - def __init__(self): - self._active = _settingsManager.getSetting("enableMouseReview") - self._currentMouseOver = _ItemContext() - self._pointer = None - self._workspace = None - self._windows = [] - self._all_windows = [] - self._handlerIds = {} - self._eventListener = Atspi.EventListener.new(self._listener) - self.inMouseEvent = False - self._handlers = self._setup_handlers() - self._bindings = self._setup_bindings() - - if not _mouseReviewCapable: - msg = "MOUSE REVIEW ERROR: Wnck is not available" - debug.println(debug.LEVEL_INFO, msg, True) - return - - display = Gdk.Display.get_default() - try: - seat = Gdk.Display.get_default_seat(display) - self._pointer = seat.get_pointer() - except AttributeError: - msg = "MOUSE REVIEW ERROR: Gtk+ 3.20 is not available" - debug.println(debug.LEVEL_INFO, msg, True) - return - except Exception: - msg = "MOUSE REVIEW ERROR: Exception getting pointer for default seat." - debug.println(debug.LEVEL_INFO, msg, True) - return - - if not self._pointer: - msg = "MOUSE REVIEW ERROR: No pointer for default seat." - debug.println(debug.LEVEL_INFO, msg, True) - return - - if not self._active: - return - - self.activate() - - def get_bindings(self): - """Returns the mouse-review keybindings.""" - - return self._bindings - - def get_handlers(self): - """Returns the mouse-review handlers.""" - - return self._handlers - - def _setup_handlers(self): - """Sets up and returns the mouse-review input event handlers.""" - - handlers = {} - - handlers["toggleMouseReviewHandler"] = \ - input_event.InputEventHandler( - self.toggle, - cmdnames.MOUSE_REVIEW_TOGGLE) - - return handlers - - def _setup_bindings(self): - """Sets up and returns the mouse-review key bindings.""" - - bindings = keybindings.KeyBindings() - - bindings.add( - keybindings.KeyBinding( - "", - keybindings.defaultModifierMask, - keybindings.NO_MODIFIER_MASK, - self._handlers.get("toggleMouseReviewHandler"))) - - return bindings - - def activate(self): - """Activates mouse review.""" - - if not _mouseReviewCapable: - msg = "MOUSE REVIEW ERROR: Wnck is not available" - debug.println(debug.LEVEL_INFO, msg, True) - return - - # Set up the initial object as the one with the focus to avoid - # presenting irrelevant info the first time. - obj = cthulhu_state.locusOfFocus - script = None - frame = None - if obj: - script = _scriptManager.getScript(AXObject.get_application(obj), obj) - if script: - frame = script.utilities.topLevelObject(obj) - self._currentMouseOver = _ItemContext(obj=obj, frame=frame, script=script) - - self._eventListener.register("mouse:abs") - screen = Wnck.Screen.get_default() - if screen: - # On first startup windows and workspace are likely to be None, - # but the signals we connect to will get emitted when proper values - # become available; but in case we got disabled and re-enabled we - # have to get the initial values manually. - stacked = screen.get_windows_stacked() - if stacked: - stacked.reverse() - self._all_windows = stacked - self._workspace = screen.get_active_workspace() - if self._workspace: - self._update_workspace_windows() - - i = screen.connect("window-stacking-changed", self._on_stacking_changed) - self._handlerIds[i] = screen - i = screen.connect("active-workspace-changed", self._on_workspace_changed) - self._handlerIds[i] = screen - - self._active = True - - def deactivate(self): - """Deactivates mouse review.""" - - self._eventListener.deregister("mouse:abs") - for key, value in self._handlerIds.items(): - value.disconnect(key) - self._handlerIds = {} - self._workspace = None - self._windows = [] - self._all_windows = [] - - self._active = False - - def getCurrentItem(self): - """Returns the accessible object being reviewed.""" - - if not _mouseReviewCapable: - return None - - if not self._active: - return None - - obj = self._currentMouseOver.getObject() - - if time.time() - self._currentMouseOver.getTime() > 0.1: - msg = f"MOUSE REVIEW: Treating {obj} as stale" - debug.println(debug.LEVEL_INFO, msg, True) - return None - - return obj - - def toggle(self, script=None, event=None): - """Toggle mouse reviewing on or off.""" - - if not _mouseReviewCapable: - return - - self._active = not self._active - _settingsManager.setSetting("enableMouseReview", self._active) - - if not self._active: - self.deactivate() - msg = messages.MOUSE_REVIEW_DISABLED - else: - self.activate() - msg = messages.MOUSE_REVIEW_ENABLED - - if cthulhu_state.activeScript: - cthulhu_state.activeScript.presentMessage(msg) - - def _update_workspace_windows(self): - self._windows = [w for w in self._all_windows - if w.is_on_workspace(self._workspace)] - - def _on_stacking_changed(self, screen): - """Callback for Wnck's window-stacking-changed signal.""" - - stacked = screen.get_windows_stacked() - stacked.reverse() - self._all_windows = stacked - if self._workspace: - self._update_workspace_windows() - - def _on_workspace_changed(self, screen, prev_ws=None): - """Callback for Wnck's active-workspace-changed signal.""" - - self._workspace = screen.get_active_workspace() - self._update_workspace_windows() - - def _contains_point(self, obj, x, y, coordType=None): - if coordType is None: - coordType = Atspi.CoordType.SCREEN - - try: - return obj.queryComponent().contains(x, y, coordType) - except Exception: - return False - - def _has_bounds(self, obj, bounds, coordType=None): - """Returns True if the bounding box of obj is bounds.""" - - if coordType is None: - coordType = Atspi.CoordType.SCREEN - - try: - extents = obj.queryComponent().getExtents(coordType) - except Exception: - return False - - return list(extents) == list(bounds) - - def _accessible_window_at_point(self, pX, pY): - """Returns the accessible window at the specified coordinates.""" - - window = None - for w in self._windows: - if w.is_minimized(): - continue - - x, y, width, height = w.get_geometry() - if x <= pX <= x + width and y <= pY <= y + height: - window = w - break - - if not window: - return None - - windowApp = window.get_application() - if not windowApp: - return None - - app = AXUtilities.get_application_with_pid(windowApp.get_pid()) - if not app: - return None - - candidates = [o for o in AXObject.iter_children( - app, lambda x: self._contains_point(x, pX, pY))] - if len(candidates) == 1: - return candidates[0] - - name = window.get_name() - matches = [o for o in candidates if AXObject.get_name(o) == name] - if len(matches) == 1: - return matches[0] - - bbox = window.get_client_window_geometry() - matches = [o for o in candidates if self._has_bounds(o, bbox)] - if len(matches) == 1: - return matches[0] - - return None - - def _on_mouse_moved(self, event): - """Callback for mouse:abs events.""" - - screen, pX, pY = self._pointer.get_position() - window = self._accessible_window_at_point(pX, pY) - msg = "MOUSE REVIEW: Window at (%i, %i) is %s" % (pX, pY, window) - debug.println(debug.LEVEL_INFO, msg, True) - if not window: - return - - script = _scriptManager.getScript(AXObject.get_application(window)) - if not script: - return - - if script.utilities.isDead(cthulhu_state.locusOfFocus): - menu = None - elif AXUtilities.is_menu(cthulhu_state.locusOfFocus): - menu = cthulhu_state.locusOfFocus - else: - menu = AXObject.find_ancestor(cthulhu_state.locusOfFocus, AXUtilities.is_menu) - - screen, nowX, nowY = self._pointer.get_position() - if (pX, pY) != (nowX, nowY): - msg = "MOUSE REVIEW: Pointer moved again: (%i, %i)" % (nowX, nowY) - debug.println(debug.LEVEL_INFO, msg, True) - return - - obj = script.utilities.descendantAtPoint(menu, pX, pY) \ - or script.utilities.descendantAtPoint(window, pX, pY) - msg = "MOUSE REVIEW: Object at (%i, %i) is %s" % (pX, pY, obj) - debug.println(debug.LEVEL_INFO, msg, True) - - script = _scriptManager.getScript(AXObject.get_application(window), obj) - if menu and obj and not AXObject.find_ancestor(obj, AXUtilities.is_menu): - if script.utilities.intersectingRegion(obj, menu) != (0, 0, 0, 0): - msg = f"MOUSE REVIEW: {obj} believed to be under {menu}" - debug.println(debug.LEVEL_INFO, msg, True) - return - - objDocument = script.utilities.getTopLevelDocumentForObject(obj) - if objDocument and script.utilities.inDocumentContent(): - document = script.utilities.activeDocument() - if document != objDocument: - msg = f"MOUSE REVIEW: {obj} is not in active document {document}" - debug.println(debug.LEVEL_INFO, msg, True) - return - - screen, nowX, nowY = self._pointer.get_position() - if (pX, pY) != (nowX, nowY): - msg = "MOUSE REVIEW: Pointer moved again: (%i, %i)" % (nowX, nowY) - debug.println(debug.LEVEL_INFO, msg, True) - return - - boundary = None - x, y, width, height = self._currentMouseOver.getBoundingBox() - if y <= pY <= y + height and self._currentMouseOver.getString(): - boundary = Atspi.TextBoundaryType.WORD_START - elif obj == self._currentMouseOver.getObject(): - boundary = Atspi.TextBoundaryType.LINE_START - elif AXUtilities.is_selectable(obj): - boundary = Atspi.TextBoundaryType.LINE_START - elif script.utilities.isMultiParagraphObject(obj): - boundary = Atspi.TextBoundaryType.LINE_START - - new = _ItemContext(pX, pY, obj, boundary, window, script) - if new.present(self._currentMouseOver): - self._currentMouseOver = new - - def _listener(self, event): - """Generic listener, mainly to output debugging info.""" - - startTime = time.time() - msg = f"\nvvvvv PROCESS OBJECT EVENT {event.type} vvvvv" - debug.println(debug.LEVEL_INFO, msg, False) - - if event.type.startswith("mouse:abs"): - self.inMouseEvent = True - self._on_mouse_moved(event) - self.inMouseEvent = False - - msg = f"TOTAL PROCESSING TIME: {time.time() - startTime:.4f}\n" - msg += f"^^^^^ PROCESS OBJECT EVENT {event.type} ^^^^^\n" - debug.println(debug.LEVEL_INFO, msg, False) diff --git a/src/cthulhu/plugins/MouseReview/__init__.py b/src/cthulhu/plugins/MouseReview/__init__.py deleted file mode 100644 index 782103c..0000000 --- a/src/cthulhu/plugins/MouseReview/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2024 Stormux -# Copyright (c) 2010-2012 The Orca Team -# Copyright (c) 2012 Igalia, S.L. -# Copyright (c) 2005-2010 Sun Microsystems Inc. -# -# 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. -# -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca - diff --git a/src/cthulhu/settings.py b/src/cthulhu/settings.py index 787c5cd..694123d 100644 --- a/src/cthulhu/settings.py +++ b/src/cthulhu/settings.py @@ -413,4 +413,4 @@ presentChatRoomLast = False presentLiveRegionFromInactiveTab = False # Plugins -activePlugins = ['Clipboard', 'DisplayVersion', 'MouseReview', 'ByeCthulhu', 'Time', 'HelloCthulhu', 'hello_world', 'self_voice', 'PluginManager', 'SimplePluginSystem'] +activePlugins = ['Clipboard', 'DisplayVersion', 'ByeCthulhu', 'Time', 'HelloCthulhu', 'hello_world', 'self_voice', 'PluginManager', 'SimplePluginSystem']