fix: VO browse cursor sync - post FocusedUIElementChanged on window
VoiceOver's browse cursor was not following Emacs cursor moves because FocusedUIElementChangedNotification was posted on the buffer element (a custom NSObject subclass), which VoiceOver ignores for browse cursor re-anchoring. Three changes: 1. Post FocusedUIElementChanged on the NSWindow instead of self, causing VoiceOver to call accessibilityFocusedUIElement on the window and re-anchor its browse cursor at the returned buffer element. 2. Post NSAccessibilityLayoutChangedNotification on self.emacsView with NSAccessibilityUIElementsKey pointing to the buffer element, causing VoiceOver to re-examine the element's properties including accessibilitySelectedTextRange. 3. Post SelectedTextChangedNotification from self.emacsView (an NSView) instead of self (NSObject), with NSAccessibilityUIElementsKey, so VoiceOver processes text-change notifications from a view-based element in the hierarchy.
This commit is contained in:
@@ -385,7 +385,7 @@ index 8d44b5f..29b646d 100644
|
|||||||
specpdl_ref count = SPECPDL_INDEX ();
|
specpdl_ref count = SPECPDL_INDEX ();
|
||||||
record_unwind_current_buffer ();
|
record_unwind_current_buffer ();
|
||||||
/* Ensure block_input is always matched by unblock_input even if
|
/* Ensure block_input is always matched by unblock_input even if
|
||||||
@@ -9064,11 +9196,13 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point
|
@@ -9064,14 +9196,22 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point
|
||||||
= @(ns_ax_text_state_change_selection_move);
|
= @(ns_ax_text_state_change_selection_move);
|
||||||
moveInfo[@"AXTextSelectionDirection"] = @(direction);
|
moveInfo[@"AXTextSelectionDirection"] = @(direction);
|
||||||
moveInfo[@"AXTextChangeElement"] = self;
|
moveInfo[@"AXTextChangeElement"] = self;
|
||||||
@@ -403,8 +403,18 @@ index 8d44b5f..29b646d 100644
|
|||||||
+ && direction != ns_ax_text_selection_direction_discontiguous)
|
+ && direction != ns_ax_text_selection_direction_discontiguous)
|
||||||
moveInfo[@"AXTextSelectionGranularity"] = @(granularity);
|
moveInfo[@"AXTextSelectionGranularity"] = @(granularity);
|
||||||
|
|
||||||
|
+ /* Post SelectedTextChanged from the parent EmacsView (an NSView)
|
||||||
|
+ rather than from self (a custom NSObject element). VoiceOver
|
||||||
|
+ processes text-change notifications more reliably from view-based
|
||||||
|
+ elements. Include UIElementsKey so VoiceOver knows which child
|
||||||
|
+ element's selectedTextRange to re-query. */
|
||||||
|
+ moveInfo[NSAccessibilityUIElementsKey] = @[self];
|
||||||
ns_ax_post_notification_with_info (
|
ns_ax_post_notification_with_info (
|
||||||
@@ -9111,12 +9245,17 @@ derive its own speech (it would read the wrong character
|
- self,
|
||||||
|
+ self.emacsView,
|
||||||
|
NSAccessibilitySelectedTextChangedNotification,
|
||||||
|
moveInfo);
|
||||||
|
@@ -9111,12 +9254,17 @@ derive its own speech (it would read the wrong character
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +437,7 @@ index 8d44b5f..29b646d 100644
|
|||||||
if (cachedText
|
if (cachedText
|
||||||
&& granularity == ns_ax_text_selection_granularity_line)
|
&& granularity == ns_ax_text_selection_granularity_line)
|
||||||
{
|
{
|
||||||
@@ -9179,7 +9318,14 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
|
@@ -9179,7 +9327,14 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
|
||||||
ptrdiff_t currentOverlayStart = 0;
|
ptrdiff_t currentOverlayStart = 0;
|
||||||
ptrdiff_t currentOverlayEnd = 0;
|
ptrdiff_t currentOverlayEnd = 0;
|
||||||
|
|
||||||
@@ -442,7 +452,7 @@ index 8d44b5f..29b646d 100644
|
|||||||
record_unwind_current_buffer ();
|
record_unwind_current_buffer ();
|
||||||
if (b != current_buffer)
|
if (b != current_buffer)
|
||||||
set_buffer_internal_1 (b);
|
set_buffer_internal_1 (b);
|
||||||
@@ -9356,12 +9502,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
|
@@ -9356,12 +9511,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
|
||||||
if (!b)
|
if (!b)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -472,7 +482,7 @@ index 8d44b5f..29b646d 100644
|
|||||||
if (modiff != self.cachedModiff)
|
if (modiff != self.cachedModiff)
|
||||||
{
|
{
|
||||||
self.cachedModiff = modiff;
|
self.cachedModiff = modiff;
|
||||||
@@ -9375,6 +9538,7 @@ Text property changes (e.g. face updates from
|
@@ -9375,6 +9547,7 @@ Text property changes (e.g. face updates from
|
||||||
{
|
{
|
||||||
self.cachedCharsModiff = chars_modiff;
|
self.cachedCharsModiff = chars_modiff;
|
||||||
[self postTextChangedNotification:point];
|
[self postTextChangedNotification:point];
|
||||||
@@ -480,7 +490,7 @@ index 8d44b5f..29b646d 100644
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9397,8 +9561,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
@@ -9397,8 +9570,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
||||||
displayed in the minibuffer. In normal editing buffers,
|
displayed in the minibuffer. In normal editing buffers,
|
||||||
font-lock and other modes change BUF_OVERLAY_MODIFF on
|
font-lock and other modes change BUF_OVERLAY_MODIFF on
|
||||||
every redisplay, triggering O(overlays) work per keystroke.
|
every redisplay, triggering O(overlays) work per keystroke.
|
||||||
@@ -498,7 +508,7 @@ index 8d44b5f..29b646d 100644
|
|||||||
goto skip_overlay_scan;
|
goto skip_overlay_scan;
|
||||||
|
|
||||||
int selected_line = -1;
|
int selected_line = -1;
|
||||||
@@ -9444,7 +9615,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
@@ -9444,7 +9624,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
||||||
self.cachedPoint = point;
|
self.cachedPoint = point;
|
||||||
self.cachedMarkActive = markActive;
|
self.cachedMarkActive = markActive;
|
||||||
|
|
||||||
@@ -518,7 +528,7 @@ index 8d44b5f..29b646d 100644
|
|||||||
NSInteger direction = ns_ax_text_selection_direction_discontiguous;
|
NSInteger direction = ns_ax_text_selection_direction_discontiguous;
|
||||||
if (point > oldPoint)
|
if (point > oldPoint)
|
||||||
direction = ns_ax_text_selection_direction_next;
|
direction = ns_ax_text_selection_direction_next;
|
||||||
@@ -9492,6 +9674,38 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
@@ -9492,6 +9683,58 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
|
||||||
granularity = ns_ax_text_selection_granularity_line;
|
granularity = ns_ax_text_selection_granularity_line;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,22 +552,42 @@ index 8d44b5f..29b646d 100644
|
|||||||
+ if (emacsMovedCursor && !isCtrlNP)
|
+ if (emacsMovedCursor && !isCtrlNP)
|
||||||
+ direction = ns_ax_text_selection_direction_discontiguous;
|
+ direction = ns_ax_text_selection_direction_discontiguous;
|
||||||
+
|
+
|
||||||
+ /* Post FocusedUIElementChangedNotification on the buffer element
|
+ /* When Emacs moves the cursor (not VoiceOver-initiated),
|
||||||
+ itself (not the parent view) so VoiceOver re-queries
|
+ VoiceOver's browse cursor must re-anchor to the new
|
||||||
+ accessibilitySelectedTextRange and re-anchors its rotor browse
|
+ insertion point. Posting FocusedUIElementChanged on self
|
||||||
+ cursor to Emacs' current point. Posting on self (already
|
+ (a custom NSObject-based element, not an NSView) is
|
||||||
+ focused) causes VO to silently re-anchor without re-announcing
|
+ insufficient \u2014 VoiceOver only re-anchors its browse cursor
|
||||||
+ the element name. Required for all granularities (char, word,
|
+ when it receives FocusedUIElementChanged from the NSWindow,
|
||||||
+ line). */
|
+ which triggers accessibilityFocusedUIElement to walk the
|
||||||
|
+ hierarchy and re-anchor at the returned element.
|
||||||
|
+
|
||||||
|
+ Post on the window so VoiceOver calls
|
||||||
|
+ accessibilityFocusedUIElement on it, receives our buffer
|
||||||
|
+ element, and re-anchors its rotor browse cursor. Also
|
||||||
|
+ post LayoutChanged with UIElementsKey on the parent view
|
||||||
|
+ so VoiceOver re-examines our element's properties
|
||||||
|
+ (including accessibilitySelectedTextRange). */
|
||||||
+ if (emacsMovedCursor && [self isAccessibilityFocused])
|
+ if (emacsMovedCursor && [self isAccessibilityFocused])
|
||||||
|
+ {
|
||||||
|
+ NSWindow *win = [self.emacsView window];
|
||||||
|
+ if (win)
|
||||||
+ ns_ax_post_notification (
|
+ ns_ax_post_notification (
|
||||||
+ self,
|
+ win,
|
||||||
+ NSAccessibilityFocusedUIElementChangedNotification);
|
+ NSAccessibilityFocusedUIElementChangedNotification);
|
||||||
|
+
|
||||||
|
+ NSDictionary *layoutInfo = @{
|
||||||
|
+ NSAccessibilityUIElementsKey: @[self]
|
||||||
|
+ };
|
||||||
|
+ ns_ax_post_notification_with_info (
|
||||||
|
+ self.emacsView,
|
||||||
|
+ NSAccessibilityLayoutChangedNotification,
|
||||||
|
+ layoutInfo);
|
||||||
|
+ }
|
||||||
+
|
+
|
||||||
/* Post notifications for focused and non-focused elements. */
|
/* Post notifications for focused and non-focused elements. */
|
||||||
if ([self isAccessibilityFocused])
|
if ([self isAccessibilityFocused])
|
||||||
[self postFocusedCursorNotification:point
|
[self postFocusedCursorNotification:point
|
||||||
@@ -9634,6 +9836,17 @@ - (NSRect)accessibilityFrame
|
@@ -9634,6 +9865,17 @@ - (NSRect)accessibilityFrame
|
||||||
if (vis_start >= vis_end)
|
if (vis_start >= vis_end)
|
||||||
return @[];
|
return @[];
|
||||||
|
|
||||||
@@ -575,7 +605,7 @@ index 8d44b5f..29b646d 100644
|
|||||||
/* Symbols are interned once at startup via DEFSYM in syms_of_nsterm;
|
/* Symbols are interned once at startup via DEFSYM in syms_of_nsterm;
|
||||||
reference them directly here (GC-safe, no repeated obarray lookup). */
|
reference them directly here (GC-safe, no repeated obarray lookup). */
|
||||||
|
|
||||||
@@ -9754,6 +9967,7 @@ than O(chars). Fall back to pos+1 as safety net. */
|
@@ -9754,6 +9996,7 @@ than O(chars). Fall back to pos+1 as safety net. */
|
||||||
pos = span_end;
|
pos = span_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,7 +613,7 @@ index 8d44b5f..29b646d 100644
|
|||||||
return [[spans copy] autorelease];
|
return [[spans copy] autorelease];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9935,6 +10149,10 @@ - (void)dealloc
|
@@ -9935,6 +10178,10 @@ - (void)dealloc
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
[accessibilityElements release];
|
[accessibilityElements release];
|
||||||
@@ -594,7 +624,7 @@ index 8d44b5f..29b646d 100644
|
|||||||
[[self menu] release];
|
[[self menu] release];
|
||||||
[super dealloc];
|
[super dealloc];
|
||||||
}
|
}
|
||||||
@@ -11384,6 +11602,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
|
@@ -11384,6 +11631,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
|
||||||
|
|
||||||
windowClosing = NO;
|
windowClosing = NO;
|
||||||
processingCompose = NO;
|
processingCompose = NO;
|
||||||
@@ -604,7 +634,7 @@ index 8d44b5f..29b646d 100644
|
|||||||
scrollbarsNeedingUpdate = 0;
|
scrollbarsNeedingUpdate = 0;
|
||||||
fs_state = FULLSCREEN_NONE;
|
fs_state = FULLSCREEN_NONE;
|
||||||
fs_before_fs = next_maximized = -1;
|
fs_before_fs = next_maximized = -1;
|
||||||
@@ -12692,6 +12913,154 @@ - (id)accessibilityFocusedUIElement
|
@@ -12692,6 +12942,154 @@ - (id)accessibilityFocusedUIElement
|
||||||
The existing elements carry cached state (modiff, point) from the
|
The existing elements carry cached state (modiff, point) from the
|
||||||
previous redisplay cycle. Rebuilding first would create fresh
|
previous redisplay cycle. Rebuilding first would create fresh
|
||||||
elements with current values, making change detection impossible. */
|
elements with current values, making change detection impossible. */
|
||||||
@@ -759,7 +789,7 @@ index 8d44b5f..29b646d 100644
|
|||||||
- (void)postAccessibilityUpdates
|
- (void)postAccessibilityUpdates
|
||||||
{
|
{
|
||||||
NSTRACE ("[EmacsView postAccessibilityUpdates]");
|
NSTRACE ("[EmacsView postAccessibilityUpdates]");
|
||||||
@@ -12702,11 +13069,64 @@ - (void)postAccessibilityUpdates
|
@@ -12702,11 +13098,64 @@ - (void)postAccessibilityUpdates
|
||||||
|
|
||||||
/* Re-entrance guard: VoiceOver callbacks during notification posting
|
/* Re-entrance guard: VoiceOver callbacks during notification posting
|
||||||
can trigger redisplay, which calls ns_update_end, which calls us
|
can trigger redisplay, which calls ns_update_end, which calls us
|
||||||
|
|||||||
Reference in New Issue
Block a user