diff --git a/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch b/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch index c6ee38a..dcfa7b5 100644 --- a/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch +++ b/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch @@ -29,7 +29,7 @@ diff --git a/src/nsterm.m b/src/nsterm.m index 3e1ac74..d3015e2 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -8758,6 +8758,552 @@ - (NSRect)accessibilityFrame +@@ -8758,6 +8758,605 @@ - (NSRect)accessibilityFrame @end @@ -154,6 +154,60 @@ index 3e1ac74..d3015e2 100644 + } + } + ++ /* For word moves: explicit announcement of word AT new point. ++ VO auto-speech from SelectedTextChanged with direction=next ++ and granularity=word reads the word that was traversed (the ++ source word), not the word arrived at. This explicit ++ announcement reads the destination word instead, matching ++ user expectation ("w" jumps to next word and reads it). */ ++ BOOL isWordMove ++ = (!markActive && !oldMarkActive ++ && granularity ++ == ns_ax_text_selection_granularity_word); ++ if (isWordMove && cachedText) ++ { ++ NSCharacterSet *ws ++ = [NSCharacterSet whitespaceAndNewlineCharacterSet]; ++ NSUInteger point_idx ++ = [self accessibilityIndexForCharpos:point]; ++ NSUInteger tlen = [cachedText length]; ++ if (point_idx < tlen ++ && ![ws characterIsMember: ++ [cachedText characterAtIndex:point_idx]]) ++ { ++ /* Find word boundaries around point. */ ++ NSUInteger wstart = point_idx; ++ while (wstart > 0 ++ && ![ws characterIsMember: ++ [cachedText characterAtIndex:wstart - 1]]) ++ wstart--; ++ NSUInteger wend = point_idx; ++ while (wend < tlen ++ && ![ws characterIsMember: ++ [cachedText characterAtIndex:wend]]) ++ wend++; ++ if (wend > wstart) ++ { ++ NSString *word ++ = [cachedText substringWithRange: ++ NSMakeRange (wstart, wend - wstart)]; ++ word = [word stringByTrimmingCharactersInSet: ws]; ++ if ([word length] > 0) ++ { ++ NSDictionary *annInfo = @{ ++ NSAccessibilityAnnouncementKey: word, ++ NSAccessibilityPriorityKey: ++ @(NSAccessibilityPriorityHigh) ++ }; ++ ns_ax_post_notification_with_info ( ++ NSApp, ++ NSAccessibilityAnnouncementRequestedNotification, ++ annInfo); ++ } ++ } ++ } ++ } ++ + /* For focused line moves: always announce line text explicitly. + SelectedTextChanged with granularity=line works for arrow keys, + but C-n/C-p need the explicit announcement (VoiceOver processes @@ -317,7 +371,6 @@ index 3e1ac74..d3015e2 100644 + } + + unbind_to (count2, Qnil); -+ unblock_input (); + + /* Final fallback: read current line at point. */ + if (!announceText) diff --git a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch index 7d4cb32..9740a6a 100644 --- a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch +++ b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch @@ -435,9 +435,9 @@ index 8d44b5f..29b646d 100644 specpdl_ref count2 = SPECPDL_INDEX (); + /* Register unblock_input as an unwind action so that if any Lisp + call below signals (triggering a longjmp through unbind_to), -+ block_input is always paired with an unblock_input. The explicit -+ unblock_input() at the end of the function is still needed for -+ the normal (non-signal) path. */ ++ block_input is always paired with an unblock_input. The ++ unbind_to call at the end of the function unwinds this. ++ record_unwind_protect_void plus unbind_to is idempotent. */ + record_unwind_protect_void (unblock_input); record_unwind_current_buffer (); if (b != current_buffer) @@ -518,7 +518,7 @@ index 8d44b5f..29b646d 100644 NSInteger direction = ns_ax_text_selection_direction_discontiguous; if (point > oldPoint) direction = ns_ax_text_selection_direction_next; -@@ -9488,6 +9670,26 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9488,6 +9670,41 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property granularity = ns_ax_text_selection_granularity_line; } @@ -541,6 +541,21 @@ index 8d44b5f..29b646d 100644 + handles them correctly. */ + if (emacsMovedCursor && !isCtrlNP) + direction = ns_ax_text_selection_direction_discontiguous; ++ ++ /* Post FocusedUIElementChanged when Emacs moved the cursor ++ across a line boundary so VoiceOver re-anchors its browse ++ cursor at the new accessibilitySelectedTextRange. ++ SelectedTextChanged with discontiguous direction alone is ++ not sufficient; VoiceOver requires this notification to ++ re-query the focused element and update its internal ++ browse position. */ ++ if (emacsMovedCursor ++ && granularity ++ == ns_ax_text_selection_granularity_line ++ && [self isAccessibilityFocused]) ++ ns_ax_post_notification ( ++ self, ++ NSAccessibilityFocusedUIElementChangedNotification); + /* Post notifications for focused and non-focused elements. */ if ([self isAccessibilityFocused]) @@ -592,7 +607,7 @@ index 8d44b5f..29b646d 100644 scrollbarsNeedingUpdate = 0; fs_state = FULLSCREEN_NONE; fs_before_fs = next_maximized = -1; -@@ -12688,6 +12909,152 @@ - (id)accessibilityFocusedUIElement +@@ -12688,6 +12909,154 @@ - (id)accessibilityFocusedUIElement The existing elements carry cached state (modiff, point) from the previous redisplay cycle. Rebuilding first would create fresh elements with current values, making change detection impossible. */ @@ -631,7 +646,9 @@ index 8d44b5f..29b646d 100644 + set_buffer_internal_1 is preferred over set_buffer_internal in + a redisplay context: it skips point-motion hooks that could + trigger further redisplay or modify buffer state unexpectedly. */ ++ block_input (); + specpdl_ref count = SPECPDL_INDEX (); ++ record_unwind_protect_void (unblock_input); + record_unwind_current_buffer (); + set_buffer_internal_1 (eb); + Lisp_Object ls = Fbuffer_string ();