Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions source/_magnifier/utils/focusManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import api
import winUser
import mouseHandler
import time
import locationHelper
from .types import Coordinates, FocusType


Expand All @@ -20,6 +22,8 @@ class FocusManager:
Tracks mouse, system focus, and navigator object positions.
"""

_SYSTEM_FOCUS_STICKINESS_SECONDS: float = 0.12

def __init__(self):
"""Initialize the focus manager."""
self._lastFocusedObject: FocusType | None = None
Expand All @@ -28,6 +32,7 @@ def __init__(self):
self._lastNavigatorObjectPosition = Coordinates(0, 0)
self._lastValidSystemFocusPosition = Coordinates(0, 0)
self._lastValidNavigatorObjectPosition = Coordinates(0, 0)
self._lastSystemFocusChangeTime: float = 0.0

def getCurrentFocusCoordinates(self) -> Coordinates:
"""
Expand All @@ -36,6 +41,8 @@ def getCurrentFocusCoordinates(self) -> Coordinates:

:return: The (x, y) coordinates of the current focus
"""
now = time.monotonic()

# Get all three positions
systemFocusPosition = self._getSystemFocusPosition()
navigatorObjectPosition = self._getNavigatorObjectPosition()
Expand All @@ -52,6 +59,7 @@ def getCurrentFocusCoordinates(self) -> Coordinates:
# Update last positions
if systemFocusChanged:
self._lastSystemFocusPosition = systemFocusPosition
self._lastSystemFocusChangeTime = now
if navigatorObjectChanged:
self._lastNavigatorObjectPosition = navigatorObjectPosition
if mouseChanged:
Expand All @@ -69,6 +77,14 @@ def getCurrentFocusCoordinates(self) -> Coordinates:

# Priority 3: Navigator object – but only when it represents a genuinely independent movement.
if navigatorObjectChanged:
# If system focus just changed and we were already tracking it, keep system focus
# briefly to avoid visible oscillation while editing text.
if (
self._lastFocusedObject == FocusType.SYSTEM_FOCUS
and now - self._lastSystemFocusChangeTime <= self._SYSTEM_FOCUS_STICKINESS_SECONDS
):
return systemFocusPosition

# If both navigator and system focus changed but ended up at the same
# coordinates, treat this as ordinary system-focus navigation.
# This avoids marking normal focus/caret movement as NAVIGATOR when
Expand Down Expand Up @@ -118,7 +134,7 @@ def _getSystemFocusPosition(self) -> Coordinates:
try:
# Get caret position (works for both browse mode and regular focus)
caretPosition = api.getCaretPosition()
point = caretPosition.pointAtStart
point = self._getPointAtStart(caretPosition)
coords = Coordinates(point.x, point.y)
# Store as last valid position if not (0, 0)
if coords != Coordinates(0, 0):
Expand Down Expand Up @@ -151,13 +167,46 @@ def _getReviewPosition(self) -> Coordinates | None:
reviewPosition = api.getReviewPosition()
if reviewPosition:
try:
point = reviewPosition.pointAtStart
point = self._getPointAtStart(reviewPosition)
return Coordinates(point.x, point.y)
except (NotImplementedError, LookupError, AttributeError):
# Review position may not support pointAtStart
pass
return None

def _getPointAtStart(self, textInfo) -> locationHelper.Point:
Comment thread
Boumtchack marked this conversation as resolved.
Outdated
"""
Get a point for the start of a text range with a local end-of-text fallback.

When a collapsed TextInfo is positioned at an exclusive end offset, use the
right edge of the previous character if available. This keeps the workaround
local to the magnifier instead of changing TextInfo behavior globally.
"""
try:
return textInfo.pointAtStart
except (NotImplementedError, LookupError, AttributeError):
pass

# Only apply the fallback for TextInfos exposing the offset-based internals
# we need. Otherwise, preserve the original failure.
if not (
getattr(textInfo, "isCollapsed", False)
and getattr(textInfo, "_startOffset", 0) > 0
and hasattr(textInfo, "_getBoundingRectFromOffset")
):
Comment thread
Boumtchack marked this conversation as resolved.
Outdated
raise LookupError
Comment thread
Boumtchack marked this conversation as resolved.
Outdated

prevOffset = textInfo._startOffset - 1
try:
return textInfo._getBoundingRectFromOffset(prevOffset).topRight
except (NotImplementedError, LookupError, AttributeError):
if hasattr(textInfo, "_getPointFromOffset"):
Comment thread
Boumtchack marked this conversation as resolved.
Outdated
try:
return textInfo._getPointFromOffset(prevOffset)
except (NotImplementedError, LookupError, AttributeError):
pass
raise LookupError
Comment thread
Boumtchack marked this conversation as resolved.
Outdated

def _getNavigatorObjectLocation(self) -> Coordinates | None:
"""
Get the navigator object location from its bounding rectangle.
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/test_magnifier/test_focusManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ def testGetCurrentFocusCoordinates(self):
self.focusManager._lastNavigatorObjectPosition = Coordinates(0, 0)
self.focusManager._lastSystemFocusPosition = Coordinates(0, 0)
self.focusManager._lastMousePosition = Coordinates(0, 0)
self.focusManager._lastFocusedObject = None
self.focusManager._lastSystemFocusChangeTime = 0.0

# Set lastFocusedObject if specified
if param.lastFocusedObject is not None:
Expand Down
Loading