patches: fix review B1/W1-5 — unwind protection, dealloc leak, DEFSYM nav, lineRange, buffer validation, select-window
B1: setAccessibilitySelectedTextRange: — add record_unwind_protect_void(unblock_input)
before block_input to prevent permanently blocked input if Fset_marker signals.
W1: EmacsAccessibilityInteractiveSpan — add -dealloc releasing spanLabel/spanValue
(MRC copy properties leaked on every span rebuild cycle).
W2: ns_ax_event_is_line_nav_key — replace 8x intern_c_string with DEFSYM'd symbols
(Qns_ax_next_line etc.) to avoid per-cursor-move obarray lookups.
W3: accessibilityRangeForLine: — rewrite from O(n chars) characterAtIndex loop to
O(lines) lineRangeForRange, matching accessibilityLineForIndex: pattern.
W4: accessibilityChildrenInNavigationOrder — validate buffer before calling
ns_ax_scan_interactive_spans to prevent Lisp signals in dispatch_sync context.
W5: EmacsAccessibilityBuffer setAccessibilityFocused: — add Fselect_window so
VoiceOver focus actually switches the Emacs selected window, with proper
unwind protection for block_input.
This commit is contained in:
@@ -88,7 +88,7 @@ Lisp calls, read only immutable NSString and scalar cache).
|
|||||||
etc/NEWS | 11 +
|
etc/NEWS | 11 +
|
||||||
src/nsterm.h | 108 ++
|
src/nsterm.h | 108 ++
|
||||||
src/nsterm.m | 2870 +++++++++++++++++++++++++++++++++++++++++++++++---
|
src/nsterm.m | 2870 +++++++++++++++++++++++++++++++++++++++++++++++---
|
||||||
3 files changed, 2922 insertions(+), 149 deletions(-)
|
3 files changed, 2960 insertions(+), 149 deletions(-)
|
||||||
|
|
||||||
diff --git a/etc/NEWS b/etc/NEWS
|
diff --git a/etc/NEWS b/etc/NEWS
|
||||||
index 7367e3cc..0e4480ad 100644
|
index 7367e3cc..0e4480ad 100644
|
||||||
@@ -228,7 +228,7 @@ index 7c1ee4cf..542e7d59 100644
|
|||||||
#endif
|
#endif
|
||||||
BOOL font_panel_active;
|
BOOL font_panel_active;
|
||||||
NSFont *font_panel_result;
|
NSFont *font_panel_result;
|
||||||
@@ -528,6 +629,13 @@ enum ns_return_frame_mode
|
@@ -528,6 +626,13 @@ enum ns_return_frame_mode
|
||||||
- (void)windowWillExitFullScreen;
|
- (void)windowWillExitFullScreen;
|
||||||
- (void)windowDidExitFullScreen;
|
- (void)windowDidExitFullScreen;
|
||||||
- (void)windowDidBecomeKey;
|
- (void)windowDidBecomeKey;
|
||||||
@@ -309,7 +309,7 @@ index 932d209f..ea2de6f2 100644
|
|||||||
ns_focus (f, NULL, 0);
|
ns_focus (f, NULL, 0);
|
||||||
|
|
||||||
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
|
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
|
||||||
@@ -6849,218 +6891,2471 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
|
@@ -6849,218 +6891,2498 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
|
|
||||||
@@ -788,24 +788,26 @@ index 932d209f..ea2de6f2 100644
|
|||||||
|
|
||||||
- result = font_panel_result;
|
- result = font_panel_result;
|
||||||
- font_panel_result = nil;
|
- font_panel_result = nil;
|
||||||
+ /* 1. Check Vthis_command for known navigation command symbols. */
|
+ /* 1. Check Vthis_command for known navigation command symbols.
|
||||||
|
+ All symbols are registered via DEFSYM in syms_of_nsterm to avoid
|
||||||
|
+ per-call obarray lookups in this hot path (runs every cursor move). */
|
||||||
+ if (SYMBOLP (Vthis_command) && !NILP (Vthis_command))
|
+ if (SYMBOLP (Vthis_command) && !NILP (Vthis_command))
|
||||||
+ {
|
+ {
|
||||||
+ Lisp_Object cmd = Vthis_command;
|
+ Lisp_Object cmd = Vthis_command;
|
||||||
+ /* Forward line commands. */
|
+ /* Forward line commands. */
|
||||||
+ if (EQ (cmd, intern_c_string ("next-line"))
|
+ if (EQ (cmd, Qns_ax_next_line)
|
||||||
+ || EQ (cmd, intern_c_string ("dired-next-line"))
|
+ || EQ (cmd, Qns_ax_dired_next_line)
|
||||||
+ || EQ (cmd, intern_c_string ("evil-next-line"))
|
+ || EQ (cmd, Qns_ax_evil_next_line)
|
||||||
+ || EQ (cmd, intern_c_string ("evil-next-visual-line")))
|
+ || EQ (cmd, Qns_ax_evil_next_visual_line))
|
||||||
+ {
|
+ {
|
||||||
+ if (which) *which = 1;
|
+ if (which) *which = 1;
|
||||||
+ return true;
|
+ return true;
|
||||||
+ }
|
+ }
|
||||||
+ /* Backward line commands. */
|
+ /* Backward line commands. */
|
||||||
+ if (EQ (cmd, intern_c_string ("previous-line"))
|
+ if (EQ (cmd, Qns_ax_previous_line)
|
||||||
+ || EQ (cmd, intern_c_string ("dired-previous-line"))
|
+ || EQ (cmd, Qns_ax_dired_previous_line)
|
||||||
+ || EQ (cmd, intern_c_string ("evil-previous-line"))
|
+ || EQ (cmd, Qns_ax_evil_previous_line)
|
||||||
+ || EQ (cmd, intern_c_string ("evil-previous-visual-line")))
|
+ || EQ (cmd, Qns_ax_evil_previous_visual_line))
|
||||||
+ {
|
+ {
|
||||||
+ if (which) *which = -1;
|
+ if (which) *which = -1;
|
||||||
+ return true;
|
+ return true;
|
||||||
@@ -1129,6 +1131,13 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+
|
+
|
||||||
+@implementation EmacsAccessibilityInteractiveSpan
|
+@implementation EmacsAccessibilityInteractiveSpan
|
||||||
+
|
+
|
||||||
|
+- (void)dealloc
|
||||||
|
+{
|
||||||
|
+ [spanLabel release];
|
||||||
|
+ [spanValue release];
|
||||||
|
+ [super dealloc];
|
||||||
|
+}
|
||||||
|
+
|
||||||
+- (BOOL) isAccessibilityElement { return YES; }
|
+- (BOOL) isAccessibilityElement { return YES; }
|
||||||
+
|
+
|
||||||
+- (NSAccessibilityRole) accessibilityRole
|
+- (NSAccessibilityRole) accessibilityRole
|
||||||
@@ -1225,6 +1234,18 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ }
|
+ }
|
||||||
+
|
+
|
||||||
+ struct window *w = [self validWindow];
|
+ struct window *w = [self validWindow];
|
||||||
|
+ if (!w)
|
||||||
|
+ return cachedInteractiveSpans ? cachedInteractiveSpans : @[];
|
||||||
|
+
|
||||||
|
+ /* Validate buffer before scanning. The Lisp calls inside
|
||||||
|
+ ns_ax_scan_interactive_spans (Ftext_properties_at, Fplist_get,
|
||||||
|
+ Fnext_single_property_change) do not signal on valid buffers
|
||||||
|
+ with valid positions. Verify those preconditions here so we
|
||||||
|
+ never enter the scan with invalid state, which could longjmp
|
||||||
|
+ out of a dispatch_sync block and deadlock the AX thread. */
|
||||||
|
+ if (!BUFFERP (w->contents) || !XBUFFER (w->contents))
|
||||||
|
+ return cachedInteractiveSpans ? cachedInteractiveSpans : @[];
|
||||||
|
+
|
||||||
+ NSArray *spans = ns_ax_scan_interactive_spans (w, self);
|
+ NSArray *spans = ns_ax_scan_interactive_spans (w, self);
|
||||||
+
|
+
|
||||||
+ if (!cachedInteractiveSpans)
|
+ if (!cachedInteractiveSpans)
|
||||||
@@ -1744,6 +1765,10 @@ index 932d209f..ea2de6f2 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
|
||||||
|
+ Fset_marker or another Lisp call signals (longjmp). */
|
||||||
|
+ record_unwind_protect_void (unblock_input);
|
||||||
|
+ block_input ();
|
||||||
+
|
+
|
||||||
+ /* Convert accessibility index to buffer charpos via mapping. */
|
+ /* Convert accessibility index to buffer charpos via mapping. */
|
||||||
+ ptrdiff_t charpos = [self charposForAccessibilityIndex:range.location];
|
+ ptrdiff_t charpos = [self charposForAccessibilityIndex:range.location];
|
||||||
@@ -1754,8 +1779,6 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ if (charpos > BUF_ZV (b))
|
+ if (charpos > BUF_ZV (b))
|
||||||
+ charpos = BUF_ZV (b);
|
+ charpos = BUF_ZV (b);
|
||||||
+
|
+
|
||||||
+ block_input ();
|
|
||||||
+
|
|
||||||
+ /* Move point directly in the buffer. */
|
+ /* Move point directly in the buffer. */
|
||||||
+ if (b != current_buffer)
|
+ if (b != current_buffer)
|
||||||
+ set_buffer_internal_1 (b);
|
+ set_buffer_internal_1 (b);
|
||||||
@@ -1778,8 +1801,6 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+
|
+
|
||||||
+ unbind_to (count, Qnil);
|
+ unbind_to (count, Qnil);
|
||||||
+
|
+
|
||||||
+ unblock_input ();
|
|
||||||
+
|
|
||||||
+ /* Update cached state so the next notification cycle doesn't
|
+ /* Update cached state so the next notification cycle doesn't
|
||||||
+ re-announce this movement. */
|
+ re-announce this movement. */
|
||||||
+ self.cachedPoint = charpos;
|
+ self.cachedPoint = charpos;
|
||||||
@@ -1809,14 +1830,22 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ if (!view || !view->emacsframe)
|
+ if (!view || !view->emacsframe)
|
||||||
+ return;
|
+ return;
|
||||||
+
|
+
|
||||||
|
+ /* Use specpdl unwind protection for block_input safety. */
|
||||||
|
+ specpdl_ref count = SPECPDL_INDEX ();
|
||||||
|
+ record_unwind_protect_void (unblock_input);
|
||||||
+ block_input ();
|
+ block_input ();
|
||||||
+
|
+
|
||||||
|
+ /* Select the Emacs window so keyboard focus follows VoiceOver. */
|
||||||
|
+ struct frame *f = view->emacsframe;
|
||||||
|
+ if (w != XWINDOW (f->selected_window))
|
||||||
|
+ Fselect_window (self.lispWindow, Qnil);
|
||||||
|
+
|
||||||
+ /* Raise the frame's NS window to ensure keyboard focus. */
|
+ /* Raise the frame's NS window to ensure keyboard focus. */
|
||||||
+ NSWindow *nswin = [view window];
|
+ NSWindow *nswin = [view window];
|
||||||
+ if (nswin && ![nswin isKeyWindow])
|
+ if (nswin && ![nswin isKeyWindow])
|
||||||
+ [nswin makeKeyAndOrderFront:nil];
|
+ [nswin makeKeyAndOrderFront:nil];
|
||||||
+
|
+
|
||||||
+ unblock_input ();
|
+ unbind_to (count, Qnil);
|
||||||
+
|
+
|
||||||
+ /* Post SelectedTextChanged so VoiceOver reads the current line
|
+ /* Post SelectedTextChanged so VoiceOver reads the current line
|
||||||
+ upon entering text interaction mode.
|
+ upon entering text interaction mode.
|
||||||
@@ -1945,32 +1974,30 @@ index 932d209f..ea2de6f2 100644
|
|||||||
+ return NSMakeRange (NSNotFound, 0);
|
+ return NSMakeRange (NSNotFound, 0);
|
||||||
+
|
+
|
||||||
+ NSUInteger len = [cachedText length];
|
+ NSUInteger len = [cachedText length];
|
||||||
+ NSInteger cur_line = 0;
|
+ if (len == 0)
|
||||||
|
+ return (line == 0) ? NSMakeRange (0, 0)
|
||||||
|
+ : NSMakeRange (NSNotFound, 0);
|
||||||
+
|
+
|
||||||
+ for (NSUInteger i = 0; i <= len; i++)
|
+ /* Skip to the requested line using lineRangeForRange â O(lines)
|
||||||
+ {
|
+ not O(chars), consistent with accessibilityLineForIndex:. */
|
||||||
+ if (cur_line == line)
|
+ NSInteger cur_line = 0;
|
||||||
+ {
|
+ NSUInteger scan = 0;
|
||||||
+ /* Find end of this line. */
|
+ while (cur_line < line && scan < len)
|
||||||
+ NSUInteger line_end = i;
|
|
||||||
+ while (line_end < len
|
|
||||||
+ && [cachedText characterAtIndex:line_end] != '\n')
|
|
||||||
+ line_end++;
|
|
||||||
+ /* Include the trailing newline so empty lines have length 1. */
|
|
||||||
+ if (line_end < len
|
|
||||||
+ && [cachedText characterAtIndex:line_end] == '\n')
|
|
||||||
+ line_end++;
|
|
||||||
+ return NSMakeRange (i, line_end - i);
|
|
||||||
+ }
|
|
||||||
+ if (i < len && [cachedText characterAtIndex:i] == '\n')
|
|
||||||
+ {
|
+ {
|
||||||
|
+ NSRange lr = [cachedText lineRangeForRange:NSMakeRange (scan, 0)];
|
||||||
|
+ NSUInteger next = NSMaxRange (lr);
|
||||||
|
+ if (next <= scan) break; /* safety */
|
||||||
+ cur_line++;
|
+ cur_line++;
|
||||||
|
+ scan = next;
|
||||||
+ }
|
+ }
|
||||||
+ }
|
+ if (cur_line != line)
|
||||||
+ /* Phantom final line after the last newline. */
|
|
||||||
+ if (cur_line == line)
|
|
||||||
+ return NSMakeRange (len, 0);
|
|
||||||
+ return NSMakeRange (NSNotFound, 0);
|
+ return NSMakeRange (NSNotFound, 0);
|
||||||
|
+
|
||||||
|
+ /* Return the range of the target line. */
|
||||||
|
+ if (scan >= len)
|
||||||
|
+ return NSMakeRange (len, 0); /* phantom line after final newline */
|
||||||
|
+ NSRange lr = [cachedText lineRangeForRange:NSMakeRange (scan, 0)];
|
||||||
|
+ return lr;
|
||||||
+}
|
+}
|
||||||
+
|
+
|
||||||
+- (NSRange)accessibilityRangeForIndex:(NSInteger)index
|
+- (NSRange)accessibilityRangeForIndex:(NSInteger)index
|
||||||
@@ -2929,7 +2956,7 @@ index 932d209f..ea2de6f2 100644
|
|||||||
int code;
|
int code;
|
||||||
unsigned fnKeysym = 0;
|
unsigned fnKeysym = 0;
|
||||||
static NSMutableArray *nsEvArray;
|
static NSMutableArray *nsEvArray;
|
||||||
@@ -8237,6 +10532,31 @@ - (void)windowDidBecomeKey /* for direct calls */
|
@@ -8237,6 +10559,31 @@ - (void)windowDidBecomeKey /* for direct calls */
|
||||||
XSETFRAME (event.frame_or_window, emacsframe);
|
XSETFRAME (event.frame_or_window, emacsframe);
|
||||||
kbd_buffer_store_event (&event);
|
kbd_buffer_store_event (&event);
|
||||||
ns_send_appdefined (-1); // Kick main loop
|
ns_send_appdefined (-1); // Kick main loop
|
||||||
@@ -2961,7 +2988,7 @@ index 932d209f..ea2de6f2 100644
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -9474,6 +11737,332 @@ - (int) fullscreenState
|
@@ -9474,6 +11821,332 @@ - (int) fullscreenState
|
||||||
return fs_state;
|
return fs_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3294,10 +3321,21 @@ index 932d209f..ea2de6f2 100644
|
|||||||
@end /* EmacsView */
|
@end /* EmacsView */
|
||||||
|
|
||||||
|
|
||||||
@@ -11303,7 +13892,18 @@ Convert an X font name (XLFD) to an NS font name.
|
@@ -11303,7 +13976,29 @@ Convert an X font name (XLFD) to an NS font name.
|
||||||
DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic");
|
DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic");
|
||||||
DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion");
|
DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion");
|
||||||
|
|
||||||
|
+ /* Accessibility: line navigation command symbols for
|
||||||
|
+ ns_ax_event_is_line_nav_key (hot path, avoid intern per call). */
|
||||||
|
+ DEFSYM (Qns_ax_next_line, "next-line");
|
||||||
|
+ DEFSYM (Qns_ax_previous_line, "previous-line");
|
||||||
|
+ DEFSYM (Qns_ax_dired_next_line, "dired-next-line");
|
||||||
|
+ DEFSYM (Qns_ax_dired_previous_line, "dired-previous-line");
|
||||||
|
+ DEFSYM (Qns_ax_evil_next_line, "evil-next-line");
|
||||||
|
+ DEFSYM (Qns_ax_evil_previous_line, "evil-previous-line");
|
||||||
|
+ DEFSYM (Qns_ax_evil_next_visual_line, "evil-next-visual-line");
|
||||||
|
+ DEFSYM (Qns_ax_evil_previous_visual_line, "evil-previous-visual-line");
|
||||||
|
+
|
||||||
+ /* Accessibility span scanning symbols. */
|
+ /* Accessibility span scanning symbols. */
|
||||||
+ DEFSYM (Qns_ax_widget, "widget");
|
+ DEFSYM (Qns_ax_widget, "widget");
|
||||||
+ DEFSYM (Qns_ax_button, "button");
|
+ DEFSYM (Qns_ax_button, "button");
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ EMACS NS VOICEOVER ACCESSIBILITY PATCH
|
|||||||
========================================
|
========================================
|
||||||
patch: 0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch
|
patch: 0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch
|
||||||
author: Martin Sukany <martin@sukany.cz>
|
author: Martin Sukany <martin@sukany.cz>
|
||||||
files: src/nsterm.h (+108 lines)
|
files: src/nsterm.h (+105 lines)
|
||||||
src/nsterm.m (+2638 ins, -140 del, +2498 net)
|
src/nsterm.m (+2844 ins, -149 del, +2695 net)
|
||||||
|
|
||||||
|
|
||||||
OVERVIEW
|
OVERVIEW
|
||||||
@@ -425,12 +425,15 @@ ZOOM INTEGRATION
|
|||||||
KEY DESIGN DECISIONS
|
KEY DESIGN DECISIONS
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
1. DEFSYM instead of intern for property symbols.
|
1. DEFSYM instead of intern for all frequently-used symbols.
|
||||||
DEFSYM registers symbols at startup (syms_of_nsterm) and stores
|
DEFSYM registers symbols at startup (syms_of_nsterm) and stores
|
||||||
them in C globals (e.g. Qcompletion__string). Using intern() at
|
them in C globals (e.g. Qns_ax_completion__string, Qns_ax_next_line).
|
||||||
every AX scan would perform an obarray lookup on each redisplay
|
This covers both property scanning symbols and line navigation
|
||||||
cycle. DEFSYM symbols are also always reachable by the GC via
|
command symbols used in ns_ax_event_is_line_nav_key (hot path:
|
||||||
staticpro, eliminating any risk of premature collection.
|
runs on every cursor movement). Using intern() would perform
|
||||||
|
obarray lookups on each redisplay cycle. DEFSYM symbols are
|
||||||
|
also always reachable by the GC via staticpro, eliminating any
|
||||||
|
risk of premature collection.
|
||||||
|
|
||||||
2. AnnouncementRequested for character moves, not SelectedTextChanged.
|
2. AnnouncementRequested for character moves, not SelectedTextChanged.
|
||||||
VoiceOver derives the speech character from SelectedTextChanged by
|
VoiceOver derives the speech character from SelectedTextChanged by
|
||||||
@@ -503,11 +506,12 @@ KNOWN LIMITATIONS
|
|||||||
covers the common case, but overlay-only changes with a stationary
|
covers the common case, but overlay-only changes with a stationary
|
||||||
point would be missed. A future fix would compare overlay_modiff.
|
point would be missed. A future fix would compare overlay_modiff.
|
||||||
|
|
||||||
- Interactive span scan uses property-change jumps to skip
|
- Interactive span scan uses Fnext_single_property_change across
|
||||||
non-interactive regions, but still visits every property boundary. For
|
multiple properties to skip non-interactive regions in bulk, but
|
||||||
large visible buffers this scan runs on every redisplay cycle
|
still visits every property-change boundary. For buffers with
|
||||||
whenever interactiveSpansDirty is set. An optimization would use
|
many overlapping text properties (e.g. heavily fontified source
|
||||||
next_single_property_change to skip non-interactive regions in bulk.
|
code), the number of boundaries can be significant. The scan
|
||||||
|
runs on every redisplay cycle when interactiveSpansDirty is set.
|
||||||
|
|
||||||
- Mode line text is extracted from CHAR_GLYPH rows only. Image
|
- Mode line text is extracted from CHAR_GLYPH rows only. Image
|
||||||
glyphs, stretch glyphs, and composed glyphs are silently skipped.
|
glyphs, stretch glyphs, and composed glyphs are silently skipped.
|
||||||
|
|||||||
Reference in New Issue
Block a user