Remove a couple plugins that were not being used and won't be ported over. If needed, they can be rewritten later.
This commit is contained in:
parent
231d74efa0
commit
815d39fc3f
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -1,7 +0,0 @@
|
||||
cthulhu_python_PYTHON = \
|
||||
__init__.py \
|
||||
CapsLockHack.plugin \
|
||||
CapsLockHack.py
|
||||
|
||||
cthulhu_pythondir=$(pkgpythondir)/plugins/CapsLockHack
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
cthulhu_python_PYTHON = \
|
||||
__init__.py \
|
||||
MouseReview.plugin \
|
||||
MouseReview.py
|
||||
|
||||
cthulhu_pythondir=$(pkgpythondir)/plugins/MouseReview
|
||||
|
@ -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
|
@ -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)
|
@ -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
|
||||
|
@ -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']
|
||||
|
Loading…
x
Reference in New Issue
Block a user