88Handles all focus tracking logic and coordinate calculations.
99"""
1010
11+ from logHandler import log
1112import api
1213import winUser
1314import mouseHandler
15+ import time
16+ import locationHelper
17+ import textInfos
18+ from textInfos .offsets import OffsetsTextInfo
1419from .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.
0 commit comments