Skip to content

Commit efe3ebe

Browse files
authored
Update magnifier point at start (#19780)
fixes #19746 Summary of the issue: Text caret follow would fail in some text editor (python console of nvda or notepad), if it would be the last character Description of user facing changes: text caret will now focus properly end character Description of developer facing changes: Description of development approach: Update _get_pointAtStart now falls back to the previous character's position when the caret is at end-of-text and both _getBoundingRectFromOffset and _getPointFromOffset fail for the current offset.
1 parent 24e7978 commit efe3ebe

File tree

2 files changed

+53
-2
lines changed

2 files changed

+53
-2
lines changed

source/_magnifier/utils/focusManager.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@
88
Handles all focus tracking logic and coordinate calculations.
99
"""
1010

11+
from logHandler import log
1112
import api
1213
import winUser
1314
import mouseHandler
15+
import time
16+
import locationHelper
17+
import textInfos
18+
from textInfos.offsets import OffsetsTextInfo
1419
from .types import Coordinates, FocusType
1520

1621

@@ -20,6 +25,8 @@ class FocusManager:
2025
Tracks mouse, system focus, and navigator object positions.
2126
"""
2227

28+
_SYSTEM_FOCUS_STICKINESS_SECONDS: float = 0.12
29+
2330
def __init__(self):
2431
"""Initialize the focus manager."""
2532
self._lastFocusedObject: FocusType | None = None
@@ -28,6 +35,7 @@ def __init__(self):
2835
self._lastNavigatorObjectPosition = Coordinates(0, 0)
2936
self._lastValidSystemFocusPosition = Coordinates(0, 0)
3037
self._lastValidNavigatorObjectPosition = Coordinates(0, 0)
38+
self._lastSystemFocusChangeTime: float = 0.0
3139

3240
def getCurrentFocusCoordinates(self) -> Coordinates:
3341
"""
@@ -36,6 +44,8 @@ def getCurrentFocusCoordinates(self) -> Coordinates:
3644
3745
:return: The (x, y) coordinates of the current focus
3846
"""
47+
now = time.monotonic()
48+
3949
# Get all three positions
4050
systemFocusPosition = self._getSystemFocusPosition()
4151
navigatorObjectPosition = self._getNavigatorObjectPosition()
@@ -52,6 +62,7 @@ def getCurrentFocusCoordinates(self) -> Coordinates:
5262
# Update last positions
5363
if systemFocusChanged:
5464
self._lastSystemFocusPosition = systemFocusPosition
65+
self._lastSystemFocusChangeTime = now
5566
if navigatorObjectChanged:
5667
self._lastNavigatorObjectPosition = navigatorObjectPosition
5768
if mouseChanged:
@@ -69,6 +80,14 @@ def getCurrentFocusCoordinates(self) -> Coordinates:
6980

7081
# Priority 3: Navigator object – but only when it represents a genuinely independent movement.
7182
if navigatorObjectChanged:
83+
# If system focus just changed and we were already tracking it, keep system focus
84+
# briefly to avoid visible oscillation while editing text.
85+
if (
86+
self._lastFocusedObject == FocusType.SYSTEM_FOCUS
87+
and now - self._lastSystemFocusChangeTime <= self._SYSTEM_FOCUS_STICKINESS_SECONDS
88+
):
89+
return systemFocusPosition
90+
7291
# If both navigator and system focus changed but ended up at the same
7392
# coordinates, treat this as ordinary system-focus navigation.
7493
# This avoids marking normal focus/caret movement as NAVIGATOR when
@@ -118,7 +137,7 @@ def _getSystemFocusPosition(self) -> Coordinates:
118137
try:
119138
# Get caret position (works for both browse mode and regular focus)
120139
caretPosition = api.getCaretPosition()
121-
point = caretPosition.pointAtStart
140+
point = self._getPointAtStart(caretPosition)
122141
coords = Coordinates(point.x, point.y)
123142
# Store as last valid position if not (0, 0)
124143
if coords != Coordinates(0, 0):
@@ -151,13 +170,43 @@ def _getReviewPosition(self) -> Coordinates | None:
151170
reviewPosition = api.getReviewPosition()
152171
if reviewPosition:
153172
try:
154-
point = reviewPosition.pointAtStart
173+
point = self._getPointAtStart(reviewPosition)
155174
return Coordinates(point.x, point.y)
156175
except (NotImplementedError, LookupError, AttributeError):
157176
# Review position may not support pointAtStart
158177
pass
159178
return None
160179

180+
def _getPointAtStart(self, textInfo: textInfos.TextInfo) -> locationHelper.Point:
181+
"""
182+
Get a point for the start of a text range with a local end-of-text fallback.
183+
184+
When a collapsed TextInfo is positioned at an exclusive end offset, use the
185+
right edge of the previous character if available. This keeps the workaround
186+
local to the magnifier instead of changing TextInfo behavior globally.
187+
"""
188+
try:
189+
return textInfo.pointAtStart
190+
except (NotImplementedError, LookupError, AttributeError) as e:
191+
log.debug(f"pointAtStart failed for {textInfo!r}: {e}", exc_info=True)
192+
originalExc = e
193+
194+
# Only apply the fallback for TextInfos exposing the offset-based internals
195+
# we need. Otherwise, preserve the original failure.
196+
if not (isinstance(textInfo, OffsetsTextInfo) and textInfo.isCollapsed and textInfo._startOffset > 0):
197+
raise originalExc
198+
199+
prevOffset = textInfo._startOffset - 1
200+
try:
201+
return textInfo._getBoundingRectFromOffset(prevOffset).topRight
202+
except (NotImplementedError, LookupError, AttributeError) as e:
203+
log.debug(f"_getBoundingRectFromOffset failed: {e}", exc_info=True)
204+
try:
205+
return textInfo._getPointFromOffset(prevOffset)
206+
except (NotImplementedError, LookupError, AttributeError) as e:
207+
log.debug(f"_getPointFromOffset failed: {e}", exc_info=True)
208+
raise originalExc
209+
161210
def _getNavigatorObjectLocation(self) -> Coordinates | None:
162211
"""
163212
Get the navigator object location from its bounding rectangle.

tests/unit/test_magnifier/test_focusManager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ def testGetCurrentFocusCoordinates(self):
198198
self.focusManager._lastNavigatorObjectPosition = Coordinates(0, 0)
199199
self.focusManager._lastSystemFocusPosition = Coordinates(0, 0)
200200
self.focusManager._lastMousePosition = Coordinates(0, 0)
201+
self.focusManager._lastFocusedObject = None
202+
self.focusManager._lastSystemFocusChangeTime = 0.0
201203

202204
# Set lastFocusedObject if specified
203205
if param.lastFocusedObject is not None:

0 commit comments

Comments
 (0)