Add guarded AT-SPI pointer mouse review backend
This commit is contained in:
+180
-55
@@ -32,30 +32,31 @@ __copyright__ = "Copyright (c) 2008 Eitan Isaacson" \
|
|||||||
"Copyright (c) 2016 Igalia, S.L."
|
"Copyright (c) 2016 Igalia, S.L."
|
||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
gi.require_version("Atspi", "2.0")
|
gi.require_version("Atspi", "2.0")
|
||||||
from gi.repository import Atspi
|
from gi.repository import Atspi, GLib
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from gi.repository import Gdk
|
from gi.repository import Gdk
|
||||||
try:
|
from .wnck_support import load_wnck
|
||||||
gi.require_version("Wnck", "3.0")
|
|
||||||
from gi.repository import Wnck
|
Wnck = load_wnck()
|
||||||
_mouseReviewCapable = True
|
|
||||||
except Exception:
|
|
||||||
_mouseReviewCapable = False
|
|
||||||
|
|
||||||
from . import cmdnames
|
from . import cmdnames
|
||||||
from . import debug
|
from . import debug
|
||||||
from . import keybindings
|
from . import keybindings
|
||||||
from . import input_event
|
from . import input_event
|
||||||
|
from . import input_event_manager
|
||||||
from . import messages
|
from . import messages
|
||||||
from . import cthulhu
|
from . import cthulhu
|
||||||
from . import cthulhu_state
|
from . import cthulhu_state
|
||||||
from . import script_manager
|
from . import script_manager
|
||||||
from . import settings_manager
|
from . import settings_manager
|
||||||
|
from .ax_component import AXComponent
|
||||||
from .ax_object import AXObject
|
from .ax_object import AXObject
|
||||||
from .ax_text import AXText
|
from .ax_text import AXText
|
||||||
from .ax_utilities import AXUtilities
|
from .ax_utilities import AXUtilities
|
||||||
@@ -82,8 +83,9 @@ class _StringContext:
|
|||||||
self._start = start
|
self._start = start
|
||||||
self._end = end
|
self._end = end
|
||||||
self._boundingBox = 0, 0, 0, 0
|
self._boundingBox = 0, 0, 0, 0
|
||||||
if script:
|
if AXObject.supports_text(obj):
|
||||||
self._boundingBox = script.utilities.getTextBoundingBox(obj, start, end)
|
rect = AXText.get_range_rect(obj, start, end)
|
||||||
|
self._boundingBox = rect.x, rect.y, rect.width, rect.height
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return other is not None \
|
return other is not None \
|
||||||
@@ -183,8 +185,9 @@ class _ItemContext:
|
|||||||
self._string = self._getStringContext()
|
self._string = self._getStringContext()
|
||||||
self._time = time.time()
|
self._time = time.time()
|
||||||
self._boundingBox = 0, 0, 0, 0
|
self._boundingBox = 0, 0, 0, 0
|
||||||
if script:
|
if AXObject.supports_component(obj):
|
||||||
self._boundingBox = script.utilities.getBoundingBox(obj)
|
rect = AXComponent.get_rect(obj)
|
||||||
|
self._boundingBox = rect.x, rect.y, rect.width, rect.height
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return other is not None \
|
return other is not None \
|
||||||
@@ -237,7 +240,12 @@ class _ItemContext:
|
|||||||
return _StringContext(self._obj, self._script)
|
return _StringContext(self._obj, self._script)
|
||||||
|
|
||||||
string, start, end = self._script.utilities.textAtPoint(
|
string, start, end = self._script.utilities.textAtPoint(
|
||||||
self._obj, self._x, self._y, boundary=self._boundary)
|
self._obj,
|
||||||
|
self._x,
|
||||||
|
self._y,
|
||||||
|
coordType=Atspi.CoordType.WINDOW,
|
||||||
|
boundary=self._boundary,
|
||||||
|
)
|
||||||
if string:
|
if string:
|
||||||
string = self._script.utilities.expandEOCs(self._obj, start, end)
|
string = self._script.utilities.expandEOCs(self._obj, start, end)
|
||||||
|
|
||||||
@@ -351,14 +359,45 @@ class MouseReviewer:
|
|||||||
self._handlerIds = {}
|
self._handlerIds = {}
|
||||||
self._eventListener = Atspi.EventListener.new(self._listener)
|
self._eventListener = Atspi.EventListener.new(self._listener)
|
||||||
self.inMouseEvent = False
|
self.inMouseEvent = False
|
||||||
|
self._eventQueue = deque()
|
||||||
|
self._mouseReviewCapable = False
|
||||||
|
self._useAtspi = False
|
||||||
self._handlers = self._setup_handlers()
|
self._handlers = self._setup_handlers()
|
||||||
self._bindings = self._setup_bindings()
|
self._bindings = self._setup_bindings()
|
||||||
|
|
||||||
if not _mouseReviewCapable:
|
atspiVersion = Atspi.get_version()
|
||||||
msg = "MOUSE REVIEW ERROR: Wnck is not available"
|
capabilityEnum = getattr(Atspi, "DeviceCapability", None)
|
||||||
|
pointerMonitor = getattr(capabilityEnum, "POINTER_MONITOR", 0) if capabilityEnum else 0
|
||||||
|
atspiSupported = pointerMonitor and (
|
||||||
|
atspiVersion[0] > 2
|
||||||
|
or atspiVersion[1] >= 60
|
||||||
|
or (atspiVersion[0] == 2 and atspiVersion[1] == 59 and atspiVersion[2] >= 90)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if atspiSupported:
|
||||||
|
manager = input_event_manager.get_manager()
|
||||||
|
manager.activate_device()
|
||||||
|
if manager.enable_pointer_monitoring():
|
||||||
|
self._useAtspi = True
|
||||||
|
self._mouseReviewCapable = True
|
||||||
|
else:
|
||||||
|
self._mouseReviewCapable = Wnck is not None
|
||||||
|
else:
|
||||||
|
self._mouseReviewCapable = Wnck is not None
|
||||||
|
except Exception:
|
||||||
|
self._mouseReviewCapable = False
|
||||||
|
|
||||||
|
if not self._mouseReviewCapable:
|
||||||
|
msg = (
|
||||||
|
"MOUSE REVIEW ERROR: Not supported by AT-SPI device"
|
||||||
|
if atspiSupported
|
||||||
|
else "MOUSE REVIEW ERROR: Wnck or at-spi2-core >= 2.60 required"
|
||||||
|
)
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not self._useAtspi:
|
||||||
display = Gdk.Display.get_default()
|
display = Gdk.Display.get_default()
|
||||||
try:
|
try:
|
||||||
seat = Gdk.Display.get_default_seat(display)
|
seat = Gdk.Display.get_default_seat(display)
|
||||||
@@ -366,15 +405,18 @@ class MouseReviewer:
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
msg = "MOUSE REVIEW ERROR: Gtk+ 3.20 is not available"
|
msg = "MOUSE REVIEW ERROR: Gtk+ 3.20 is not available"
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
self._mouseReviewCapable = False
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
msg = "MOUSE REVIEW ERROR: Exception getting pointer for default seat."
|
msg = "MOUSE REVIEW ERROR: Exception getting pointer for default seat."
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
self._mouseReviewCapable = False
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self._pointer:
|
if not self._pointer:
|
||||||
msg = "MOUSE REVIEW ERROR: No pointer for default seat."
|
msg = "MOUSE REVIEW ERROR: No pointer for default seat."
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
self._mouseReviewCapable = False
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self._active:
|
if not self._active:
|
||||||
@@ -421,9 +463,14 @@ class MouseReviewer:
|
|||||||
def activate(self):
|
def activate(self):
|
||||||
"""Activates mouse review."""
|
"""Activates mouse review."""
|
||||||
|
|
||||||
if not _mouseReviewCapable:
|
if not self._mouseReviewCapable:
|
||||||
msg = "MOUSE REVIEW ERROR: Wnck is not available"
|
msg = (
|
||||||
|
"MOUSE REVIEW ERROR: Not supported by AT-SPI device"
|
||||||
|
if self._useAtspi
|
||||||
|
else "MOUSE REVIEW ERROR: Wnck or at-spi2-core >= 2.60 required"
|
||||||
|
)
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
self._active = False
|
||||||
return
|
return
|
||||||
|
|
||||||
# Set up the initial object as the one with the focus to avoid
|
# Set up the initial object as the one with the focus to avoid
|
||||||
@@ -437,6 +484,11 @@ class MouseReviewer:
|
|||||||
frame = script.utilities.topLevelObject(obj)
|
frame = script.utilities.topLevelObject(obj)
|
||||||
self._currentMouseOver = _ItemContext(obj=obj, frame=frame, script=script)
|
self._currentMouseOver = _ItemContext(obj=obj, frame=frame, script=script)
|
||||||
|
|
||||||
|
if self._useAtspi:
|
||||||
|
input_event_manager.get_manager().start_pointer_watcher(self._on_pointer_moved)
|
||||||
|
self._active = True
|
||||||
|
return
|
||||||
|
|
||||||
self._eventListener.register("mouse:abs")
|
self._eventListener.register("mouse:abs")
|
||||||
screen = Wnck.Screen.get_default()
|
screen = Wnck.Screen.get_default()
|
||||||
if screen:
|
if screen:
|
||||||
@@ -462,7 +514,15 @@ class MouseReviewer:
|
|||||||
def deactivate(self):
|
def deactivate(self):
|
||||||
"""Deactivates mouse review."""
|
"""Deactivates mouse review."""
|
||||||
|
|
||||||
|
if self._useAtspi:
|
||||||
|
input_event_manager.get_manager().stop_pointer_watcher()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
self._eventListener.deregister("mouse:abs")
|
self._eventListener.deregister("mouse:abs")
|
||||||
|
except GLib.GError as error:
|
||||||
|
msg = f"MOUSE REVIEW: Exception deregistering 'mouse:abs' listener: {error}"
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
for key, value in self._handlerIds.items():
|
for key, value in self._handlerIds.items():
|
||||||
value.disconnect(key)
|
value.disconnect(key)
|
||||||
self._handlerIds = {}
|
self._handlerIds = {}
|
||||||
@@ -470,12 +530,13 @@ class MouseReviewer:
|
|||||||
self._windows = []
|
self._windows = []
|
||||||
self._all_windows = []
|
self._all_windows = []
|
||||||
|
|
||||||
|
self._eventQueue.clear()
|
||||||
self._active = False
|
self._active = False
|
||||||
|
|
||||||
def getCurrentItem(self):
|
def getCurrentItem(self):
|
||||||
"""Returns the accessible object being reviewed."""
|
"""Returns the accessible object being reviewed."""
|
||||||
|
|
||||||
if not _mouseReviewCapable:
|
if not self._mouseReviewCapable:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not self._active:
|
if not self._active:
|
||||||
@@ -493,7 +554,7 @@ class MouseReviewer:
|
|||||||
def toggle(self, script=None, event=None):
|
def toggle(self, script=None, event=None):
|
||||||
"""Toggle mouse reviewing on or off."""
|
"""Toggle mouse reviewing on or off."""
|
||||||
|
|
||||||
if not _mouseReviewCapable:
|
if not self._mouseReviewCapable:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._active = not self._active
|
self._active = not self._active
|
||||||
@@ -570,52 +631,65 @@ class MouseReviewer:
|
|||||||
|
|
||||||
return [extents.x, extents.y, extents.width, extents.height] == list(bounds)
|
return [extents.x, extents.y, extents.width, extents.height] == list(bounds)
|
||||||
|
|
||||||
def _accessible_window_at_point(self, pX, pY):
|
def _accessible_window_at_point_deprecated(self, pX, pY):
|
||||||
"""Returns the accessible window at the specified coordinates."""
|
"""Returns the accessible window and local coordinates for screen coordinates."""
|
||||||
|
|
||||||
window = None
|
window = None
|
||||||
for w in self._windows:
|
for w in self._windows:
|
||||||
if w.is_minimized():
|
if w.is_minimized():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
x, y, width, height = w.get_geometry()
|
x, y, width, height = w.get_client_window_geometry()
|
||||||
if x <= pX <= x + width and y <= pY <= y + height:
|
if x <= pX <= x + width and y <= pY <= y + height:
|
||||||
window = w
|
window = w
|
||||||
break
|
break
|
||||||
|
|
||||||
if not window:
|
if not window:
|
||||||
return None
|
return None, -1, -1
|
||||||
|
|
||||||
windowApp = window.get_application()
|
windowApp = window.get_application()
|
||||||
if not windowApp:
|
app = AXUtilities.get_application_with_pid(windowApp.get_pid()) if windowApp else None
|
||||||
return None
|
|
||||||
|
|
||||||
app = AXUtilities.get_application_with_pid(windowApp.get_pid())
|
|
||||||
if not app:
|
if not app:
|
||||||
return None
|
return None, -1, -1
|
||||||
|
|
||||||
|
windowX = pX - x
|
||||||
|
windowY = pY - y
|
||||||
|
|
||||||
candidates = [o for o in AXObject.iter_children(
|
candidates = [o for o in AXObject.iter_children(
|
||||||
app, lambda x: self._contains_point(x, pX, pY))]
|
app, lambda obj: self._contains_point(obj, windowX, windowY, Atspi.CoordType.WINDOW))]
|
||||||
if len(candidates) == 1:
|
if len(candidates) == 1:
|
||||||
return candidates[0]
|
return candidates[0], windowX, windowY
|
||||||
|
|
||||||
name = window.get_name()
|
name = window.get_name()
|
||||||
matches = [o for o in candidates if AXObject.get_name(o) == name]
|
matches = [o for o in candidates if AXObject.get_name(o) == name]
|
||||||
if len(matches) == 1:
|
if len(matches) == 1:
|
||||||
return matches[0]
|
return matches[0], windowX, windowY
|
||||||
|
|
||||||
bbox = window.get_client_window_geometry()
|
matches = [o for o in candidates if AXUtilities.is_active(o)]
|
||||||
matches = [o for o in candidates if self._has_bounds(o, bbox)]
|
|
||||||
if len(matches) == 1:
|
if len(matches) == 1:
|
||||||
return matches[0]
|
return matches[0], windowX, windowY
|
||||||
|
|
||||||
return None
|
return None, -1, -1
|
||||||
|
|
||||||
def _on_mouse_moved(self, event):
|
def _accessible_window_at_point(self, app, pX, pY):
|
||||||
"""Callback for mouse:abs events."""
|
"""Returns the accessible window and local coordinates for pointer-moved events."""
|
||||||
|
|
||||||
screen, pX, pY = self._pointer.get_position()
|
def getTuple(obj, x, y):
|
||||||
window = self._accessible_window_at_point(pX, pY)
|
rect = AXComponent.get_rect(obj)
|
||||||
|
return obj, x - rect.x, y - rect.y
|
||||||
|
|
||||||
|
candidates = [o for o in AXObject.iter_children(
|
||||||
|
app, lambda obj: self._contains_point(obj, pX, pY, Atspi.CoordType.WINDOW))]
|
||||||
|
if len(candidates) == 1:
|
||||||
|
return getTuple(candidates[0], pX, pY)
|
||||||
|
|
||||||
|
matches = [o for o in candidates if AXUtilities.is_active(o)]
|
||||||
|
if len(matches) == 1:
|
||||||
|
return getTuple(matches[0], pX, pY)
|
||||||
|
|
||||||
|
return None, -1, -1
|
||||||
|
|
||||||
|
def _mouse_moved_common(self, window, pX, pY):
|
||||||
tokens = [f"MOUSE REVIEW: Window at ({pX}, {pY}) is", window]
|
tokens = [f"MOUSE REVIEW: Window at ({pX}, {pY}) is", window]
|
||||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
if not window:
|
if not window:
|
||||||
@@ -632,15 +706,15 @@ class MouseReviewer:
|
|||||||
else:
|
else:
|
||||||
menu = AXObject.find_ancestor(cthulhu_state.locusOfFocus, AXUtilities.is_menu)
|
menu = AXObject.find_ancestor(cthulhu_state.locusOfFocus, AXUtilities.is_menu)
|
||||||
|
|
||||||
screen, nowX, nowY = self._pointer.get_position()
|
obj = None
|
||||||
if (pX, pY) != (nowX, nowY):
|
if menu:
|
||||||
msg = f"MOUSE REVIEW: Pointer moved again: ({nowX}, {nowY})"
|
obj = script.utilities.descendantAtPoint(menu, pX, pY, Atspi.CoordType.WINDOW)
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
tokens = ["MOUSE REVIEW: Object in", menu, f"at ({pX}, {pY}) is", obj]
|
||||||
return
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
|
||||||
obj = script.utilities.descendantAtPoint(menu, pX, pY) \
|
if obj is None:
|
||||||
or script.utilities.descendantAtPoint(window, pX, pY)
|
obj = script.utilities.descendantAtPoint(window, pX, pY, Atspi.CoordType.WINDOW)
|
||||||
tokens = [f"MOUSE REVIEW: Object at ({pX}, {pY}) is", obj]
|
tokens = ["MOUSE REVIEW: Object in", window, f"at ({pX}, {pY}) is", obj]
|
||||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
|
||||||
script = self.app.getScriptManager().get_script(AXObject.get_application(window), obj)
|
script = self.app.getScriptManager().get_script(AXObject.get_application(window), obj)
|
||||||
@@ -658,9 +732,8 @@ class MouseReviewer:
|
|||||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
return
|
return
|
||||||
|
|
||||||
screen, nowX, nowY = self._pointer.get_position()
|
if len(self._eventQueue):
|
||||||
if (pX, pY) != (nowX, nowY):
|
msg = "MOUSE REVIEW: Mouse moved again."
|
||||||
msg = f"MOUSE REVIEW: Pointer moved again: ({nowX}, {nowY})"
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -679,22 +752,76 @@ class MouseReviewer:
|
|||||||
if new.present(self._currentMouseOver):
|
if new.present(self._currentMouseOver):
|
||||||
self._currentMouseOver = new
|
self._currentMouseOver = new
|
||||||
|
|
||||||
def _listener(self, event):
|
def _on_mouse_moved_deprecated(self, event):
|
||||||
"""Generic listener, mainly to output debugging info."""
|
"""Callback for mouse:abs events."""
|
||||||
|
|
||||||
|
pX, pY = event.detail1, event.detail2
|
||||||
|
window, windowX, windowY = self._accessible_window_at_point_deprecated(pX, pY)
|
||||||
|
self._mouse_moved_common(window, windowX, windowY)
|
||||||
|
|
||||||
|
def _on_mouse_moved(self, obj, pX, pY):
|
||||||
|
"""Callback for pointer-moved events."""
|
||||||
|
|
||||||
|
if AXObject.get_role(obj) == Atspi.Role.APPLICATION:
|
||||||
|
window, windowX, windowY = self._accessible_window_at_point(obj, pX, pY)
|
||||||
|
self._mouse_moved_common(window, windowX, windowY)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._mouse_moved_common(obj, pX, pY)
|
||||||
|
|
||||||
|
def _process_event_deprecated(self):
|
||||||
|
if not self._eventQueue:
|
||||||
|
return
|
||||||
|
|
||||||
|
event = self._eventQueue.popleft()
|
||||||
|
if len(self._eventQueue):
|
||||||
|
return
|
||||||
|
|
||||||
startTime = time.time()
|
startTime = time.time()
|
||||||
tokens = ["\nvvvvv PROCESS OBJECT EVENT", event.type, "vvvvv"]
|
tokens = ["\nvvvvv PROCESS OBJECT EVENT", event.type, "vvvvv"]
|
||||||
debug.printTokens(debug.LEVEL_INFO, tokens, False)
|
debug.printTokens(debug.LEVEL_INFO, tokens, False)
|
||||||
|
|
||||||
if event.type.startswith("mouse:abs"):
|
|
||||||
self.inMouseEvent = True
|
self.inMouseEvent = True
|
||||||
self._on_mouse_moved(event)
|
self._on_mouse_moved_deprecated(event)
|
||||||
self.inMouseEvent = False
|
self.inMouseEvent = False
|
||||||
|
|
||||||
msg = f"TOTAL PROCESSING TIME: {time.time() - startTime:.4f}\n"
|
msg = f"TOTAL PROCESSING TIME: {time.time() - startTime:.4f}\n"
|
||||||
msg += f"^^^^^ PROCESS OBJECT EVENT {event.type} ^^^^^\n"
|
msg += f"^^^^^ PROCESS OBJECT EVENT {event.type} ^^^^^\n"
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, False)
|
debug.printMessage(debug.LEVEL_INFO, msg, False)
|
||||||
|
|
||||||
|
def _process_pointer_event(self):
|
||||||
|
if not self._eventQueue:
|
||||||
|
return
|
||||||
|
|
||||||
|
obj, x, y = self._eventQueue.popleft()
|
||||||
|
if len(self._eventQueue):
|
||||||
|
return
|
||||||
|
|
||||||
|
startTime = time.time()
|
||||||
|
tokens = ["\nvvvvv PROCESS POINTER-MOVED EVENT", "vvvvv"]
|
||||||
|
debug.printTokens(debug.LEVEL_INFO, tokens, False)
|
||||||
|
|
||||||
|
self.inMouseEvent = True
|
||||||
|
self._on_mouse_moved(obj, x, y)
|
||||||
|
self.inMouseEvent = False
|
||||||
|
|
||||||
|
msg = f"TOTAL PROCESSING TIME: {time.time() - startTime:.4f}\n"
|
||||||
|
msg += "^^^^^ PROCESS POINTER-MOVED EVENT ^^^^^\n"
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, msg, False)
|
||||||
|
|
||||||
|
def _listener(self, event):
|
||||||
|
"""Generic listener, mainly to output debugging info."""
|
||||||
|
|
||||||
|
if event.type.startswith("mouse:abs"):
|
||||||
|
self._eventQueue.append(event)
|
||||||
|
GLib.timeout_add(50, self._process_event_deprecated)
|
||||||
|
|
||||||
|
def _on_pointer_moved(self, _device, obj, x, y):
|
||||||
|
"""Listener for pointer-moved events from devices."""
|
||||||
|
|
||||||
|
self._eventQueue.append([obj, x, y])
|
||||||
|
GLib.timeout_add(50, self._process_pointer_event)
|
||||||
|
|
||||||
_reviewer = None
|
_reviewer = None
|
||||||
def getReviewer():
|
def getReviewer():
|
||||||
"""Returns the Mouse Reviewer"""
|
"""Returns the Mouse Reviewer"""
|
||||||
@@ -704,5 +831,3 @@ def getReviewer():
|
|||||||
from . import cthulhu
|
from . import cthulhu
|
||||||
_reviewer = MouseReviewer(cthulhu.cthulhuApp)
|
_reviewer = MouseReviewer(cthulhu.cthulhuApp)
|
||||||
return _reviewer
|
return _reviewer
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from unittest import mock
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||||
|
|
||||||
from cthulhu import input_event_manager
|
from cthulhu import input_event_manager
|
||||||
|
from cthulhu import mouse_review
|
||||||
|
|
||||||
|
|
||||||
class FakeDevice:
|
class FakeDevice:
|
||||||
@@ -131,5 +132,93 @@ class InputEventManagerPointerMonitorTests(unittest.TestCase):
|
|||||||
self.assertEqual(device.disconnect_calls, [17])
|
self.assertEqual(device.disconnect_calls, [17])
|
||||||
|
|
||||||
|
|
||||||
|
class MouseReviewBackendSelectionTests(unittest.TestCase):
|
||||||
|
@staticmethod
|
||||||
|
def _make_app(enabled=False):
|
||||||
|
app = mock.Mock()
|
||||||
|
settingsManager = mock.Mock()
|
||||||
|
settingsManager.getSetting.return_value = enabled
|
||||||
|
app.getSettingsManager.return_value = settingsManager
|
||||||
|
app.getScriptManager.return_value = mock.Mock()
|
||||||
|
return app
|
||||||
|
|
||||||
|
def test_prefers_atspi_backend_when_version_and_capability_are_available(self):
|
||||||
|
listener = mock.Mock()
|
||||||
|
deviceManager = mock.Mock()
|
||||||
|
deviceManager.enable_pointer_monitoring.return_value = True
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(mouse_review.Atspi.EventListener, "new", return_value=listener),
|
||||||
|
mock.patch.object(mouse_review.Atspi, "get_version", return_value=(2, 60, 0)),
|
||||||
|
mock.patch.object(
|
||||||
|
mouse_review.Atspi,
|
||||||
|
"DeviceCapability",
|
||||||
|
new=types.SimpleNamespace(POINTER_MONITOR=8),
|
||||||
|
create=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(mouse_review.input_event_manager, "get_manager", return_value=deviceManager),
|
||||||
|
mock.patch.object(mouse_review, "Wnck", None),
|
||||||
|
):
|
||||||
|
reviewer = mouse_review.MouseReviewer(self._make_app())
|
||||||
|
|
||||||
|
self.assertTrue(reviewer._useAtspi)
|
||||||
|
self.assertTrue(reviewer._mouseReviewCapable)
|
||||||
|
deviceManager.activate_device.assert_called_once_with()
|
||||||
|
deviceManager.enable_pointer_monitoring.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_activate_uses_pointer_watcher_for_atspi_backend(self):
|
||||||
|
listener = mock.Mock()
|
||||||
|
deviceManager = mock.Mock()
|
||||||
|
deviceManager.enable_pointer_monitoring.return_value = True
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(mouse_review.Atspi.EventListener, "new", return_value=listener),
|
||||||
|
mock.patch.object(mouse_review.Atspi, "get_version", return_value=(2, 60, 0)),
|
||||||
|
mock.patch.object(
|
||||||
|
mouse_review.Atspi,
|
||||||
|
"DeviceCapability",
|
||||||
|
new=types.SimpleNamespace(POINTER_MONITOR=8),
|
||||||
|
create=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(mouse_review.input_event_manager, "get_manager", return_value=deviceManager),
|
||||||
|
mock.patch.object(mouse_review.cthulhu_state, "locusOfFocus", None),
|
||||||
|
mock.patch.object(mouse_review, "Wnck", None),
|
||||||
|
):
|
||||||
|
reviewer = mouse_review.MouseReviewer(self._make_app(enabled=False))
|
||||||
|
reviewer.activate()
|
||||||
|
reviewer.deactivate()
|
||||||
|
|
||||||
|
deviceManager.start_pointer_watcher.assert_called_once_with(reviewer._on_pointer_moved)
|
||||||
|
deviceManager.stop_pointer_watcher.assert_called_once_with()
|
||||||
|
listener.register.assert_not_called()
|
||||||
|
|
||||||
|
def test_keeps_legacy_backend_when_atspi_pointer_monitor_is_unavailable(self):
|
||||||
|
listener = mock.Mock()
|
||||||
|
pointer = mock.Mock()
|
||||||
|
seat = mock.Mock()
|
||||||
|
seat.get_pointer.return_value = pointer
|
||||||
|
screen = mock.Mock()
|
||||||
|
screen.get_windows_stacked.return_value = []
|
||||||
|
screen.get_active_workspace.return_value = None
|
||||||
|
fakeWnck = mock.Mock()
|
||||||
|
fakeWnck.Screen.get_default.return_value = screen
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(mouse_review.Atspi.EventListener, "new", return_value=listener),
|
||||||
|
mock.patch.object(mouse_review.Atspi, "get_version", return_value=(2, 58, 4)),
|
||||||
|
mock.patch.object(mouse_review.input_event_manager, "get_manager"),
|
||||||
|
mock.patch.object(mouse_review.cthulhu_state, "locusOfFocus", None),
|
||||||
|
mock.patch.object(mouse_review.Gdk.Display, "get_default", return_value=mock.Mock()),
|
||||||
|
mock.patch.object(mouse_review.Gdk.Display, "get_default_seat", return_value=seat),
|
||||||
|
mock.patch.object(mouse_review, "Wnck", fakeWnck),
|
||||||
|
):
|
||||||
|
reviewer = mouse_review.MouseReviewer(self._make_app(enabled=False))
|
||||||
|
reviewer.activate()
|
||||||
|
|
||||||
|
self.assertFalse(reviewer._useAtspi)
|
||||||
|
self.assertTrue(reviewer._mouseReviewCapable)
|
||||||
|
listener.register.assert_called_once_with("mouse:abs")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user