diff --git a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch index 03ea34f..98f6bea 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -1,22 +1,31 @@ -From 515d3e0cfc42c1c9f907359b1dff379206d940a7 Mon Sep 17 00:00:00 2001 +From 3e49c04ee6996a95ead094cae9dff5e7b81a2702 Mon Sep 17 00:00:00 2001 From: Martin Sukany -Date: Fri, 27 Feb 2026 12:59:28 +0100 -Subject: [PATCH] ns: implement VoiceOver accessibility (restore line - announcement fallback) +Date: Fri, 27 Feb 2026 13:34:22 +0100 +Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line + nav, completions, interactive spans) -C-n/C-p need explicit AnnouncementRequested for line text; VoiceOver -processes these differently from arrow keys. Restore full-line -fallback announcement for all line moves (not just completion-string). -SelectedTextChanged(granularity=line) + AnnouncementRequested(line text) -does NOT cause double-speech for lines (same text = VoiceOver deduplicates -or announcement replaces; unlike character moves where the texts differ). +* src/nsterm.h: Add EmacsAccessibilityElement, EmacsAccessibilityBuffer, +EmacsAccessibilityModeLine, EmacsAccessibilityInteractiveSpan classes; +ns_ax_visible_run struct; new EmacsView ivars for AX tree. + +* src/nsterm.m: Full VoiceOver/Zoom support: +- AXBoundsForRange for macOS Zoom cursor tracking (UAZoomChangeFocus) +- EmacsAccessibilityBuffer per window (AXTextArea/AXTextField for minibuffer) +- Hybrid notification strategy: SelectedTextChanged for interruption + + braille; AnnouncementRequested for speech (char AT point, line text) +- Word granularity detection for M-f/M-b (same-line multi-char moves) +- Interactive span Tab navigation (buttons, links, completions, org-links) +- 4-fallback completion announcement chain for non-focused buffers +- Thread-safe: AX getters use cached data built on main thread +- GC-safe: all symbols registered via DEFSYM in syms_of_nsterm +- Invisible text skipping via TEXT_PROP_MEANS_INVISIBLE --- - src/nsterm.h | 109 ++ - src/nsterm.m | 2756 +++++++++++++++++++++++++++++++++++++++++++++++--- - 2 files changed, 2716 insertions(+), 149 deletions(-) + src/nsterm.h | 108 ++ + src/nsterm.m | 2700 +++++++++++++++++++++++++++++++++++++++++++++++--- + 2 files changed, 2668 insertions(+), 140 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h -index 7c1ee4c..6c95673 100644 +index 7c1ee4c..6455547 100644 --- a/src/nsterm.h +++ b/src/nsterm.h @@ -453,6 +453,100 @@ enum ns_return_frame_mode @@ -120,7 +129,7 @@ index 7c1ee4c..6c95673 100644 /* ========================================================================== The main Emacs view -@@ -471,6 +565,14 @@ enum ns_return_frame_mode +@@ -471,6 +565,13 @@ enum ns_return_frame_mode #ifdef NS_IMPL_COCOA char *old_title; BOOL maximizing_resize; @@ -129,13 +138,12 @@ index 7c1ee4c..6c95673 100644 + Lisp_Object lastRootWindow; + BOOL accessibilityTreeValid; + BOOL accessibilityUpdating; -+ @public ++ @public /* Accessed by ns_draw_phys_cursor (C function). */ + NSRect lastAccessibilityCursorRect; -+ @protected #endif BOOL font_panel_active; NSFont *font_panel_result; -@@ -528,6 +630,13 @@ enum ns_return_frame_mode +@@ -528,6 +629,13 @@ enum ns_return_frame_mode - (void)windowWillExitFullScreen; - (void)windowDidExitFullScreen; - (void)windowDidBecomeKey; @@ -150,7 +158,7 @@ index 7c1ee4c..6c95673 100644 diff --git a/src/nsterm.m b/src/nsterm.m -index 932d209..4288e68 100644 +index 932d209..7c50716 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) @@ -211,7 +219,7 @@ index 932d209..4288e68 100644 ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -@@ -6849,219 +6886,2309 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg +@@ -6849,207 +6886,2239 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg /* ========================================================================== @@ -230,69 +238,71 @@ index 932d209..4288e68 100644 - [self updateFramePosition]; -} +/* Maximum characters exposed via accessibilityValue. */ ++/* Cap accessibility text at 100,000 UTF-16 units (~200 KB). VoiceOver ++ performance degrades beyond this; buffers larger than ~50,000 lines ++ are truncated for accessibility purposes. */ +#define NS_AX_TEXT_CAP 100000 -/* Needed to inform when window closed from lisp. */ -- (void) setWindowClosing: (BOOL)closing --{ -- NSTRACE ("[EmacsView setWindowClosing:%d]", closing); +/* Build accessibility text for window W, skipping invisible text. + Populates *OUT_START with the buffer start charpos. + Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS + with the count. Caller must free *OUT_RUNS with xfree(). */ - -- windowClosing = closing; --} ++ +static NSString * +ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, + ns_ax_visible_run **out_runs, NSUInteger *out_nruns) -+{ + { +- NSTRACE ("[EmacsView setWindowClosing:%d]", closing); + *out_runs = NULL; + *out_nruns = 0; +- windowClosing = closing; +-} + if (!w || !WINDOW_LEAF_P (w)) + { + *out_start = 0; + return @""; + } ++ struct buffer *b = XBUFFER (w->contents); ++ if (!b) ++ { ++ *out_start = 0; ++ return @""; ++ } + -- (void)dealloc -{ - NSTRACE ("[EmacsView dealloc]"); -+ struct buffer *b = XBUFFER (w->contents); -+ if (!b) -+ { -+ *out_start = 0; -+ return @""; -+ } ++ ptrdiff_t begv = BUF_BEGV (b); ++ ptrdiff_t zv = BUF_ZV (b); - /* Clear the view resize notification. */ - [[NSNotificationCenter defaultCenter] - removeObserver:self - name:NSViewFrameDidChangeNotification - object:nil]; -+ ptrdiff_t begv = BUF_BEGV (b); -+ ptrdiff_t zv = BUF_ZV (b); ++ *out_start = begv; - if (fs_state == FULLSCREEN_BOTH) - [nonfs_window release]; -+ *out_start = begv; ++ if (zv <= begv) ++ return @""; -#if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MIN_REQUIRED >= 101400 - /* Release layer and menu */ - EmacsLayer *layer = (EmacsLayer *)[self layer]; - [layer release]; -#endif -+ if (zv <= begv) -+ return @""; - -- [[self menu] release]; -- [super dealloc]; --} + struct buffer *oldb = current_buffer; + if (b != current_buffer) + set_buffer_internal_1 (b); +- [[self menu] release]; +- [super dealloc]; +-} + /* First pass: count visible runs to allocate the mapping array. */ + NSUInteger run_capacity = 64; + ns_ax_visible_run *runs = xmalloc (run_capacity @@ -300,19 +310,14 @@ index 932d209..4288e68 100644 + NSUInteger nruns = 0; + NSUInteger ax_offset = 0; ++ NSMutableString *result = [NSMutableString string]; ++ ptrdiff_t pos = begv; + -/* Called on font panel selection. */ -- (void) changeFont: (id) sender -{ - struct font *font = FRAME_OUTPUT_DATA (emacsframe)->font; - NSFont *nsfont; -+ NSMutableString *result = [NSMutableString string]; -+ ptrdiff_t pos = begv; - --#ifdef NS_IMPL_GNUSTEP -- nsfont = ((struct nsfont_info *) font)->nsfont; --#else -- nsfont = (NSFont *) macfont_get_nsctfont (font); --#endif + while (pos < zv) + { + /* Check invisible property (text properties + overlays). @@ -331,15 +336,18 @@ index 932d209..4288e68 100644 + continue; + } -- if (!font_panel_active) -- return; +-#ifdef NS_IMPL_GNUSTEP +- nsfont = ((struct nsfont_info *) font)->nsfont; +-#else +- nsfont = (NSFont *) macfont_get_nsctfont (font); +-#endif + /* Find end of this visible run: where invisible property changes. */ + Lisp_Object next = Fnext_single_char_property_change ( + make_fixnum (pos), Qinvisible, Qnil, make_fixnum (zv)); + ptrdiff_t run_end = FIXNUMP (next) ? XFIXNUM (next) : zv; -- if (font_panel_result) -- [font_panel_result release]; +- if (!font_panel_active) +- return; + /* Cap total text at NS_AX_TEXT_CAP. */ + ptrdiff_t run_len = run_end - pos; + if (ax_offset + (NSUInteger) run_len > NS_AX_TEXT_CAP) @@ -371,53 +379,45 @@ index 932d209..4288e68 100644 + runs[nruns].ax_length = ns_len; + nruns++; -- font_panel_result = (NSFont *) [sender convertFont: nsfont]; +- if (font_panel_result) +- [font_panel_result release]; + ax_offset += ns_len; + pos = run_end; + } -- if (font_panel_result) -- [font_panel_result retain]; +- font_panel_result = (NSFont *) [sender convertFont: nsfont]; + if (b != oldb) + set_buffer_internal_1 (oldb); +- if (font_panel_result) +- [font_panel_result retain]; ++ *out_runs = runs; ++ *out_nruns = nruns; ++ return result; ++} + -#ifndef NS_IMPL_COCOA - font_panel_active = NO; - [NSApp stop: self]; -#endif -+ *out_runs = runs; -+ *out_nruns = nruns; -+ return result; - } - --#ifdef NS_IMPL_COCOA --- (void) noteUserSelectedFont + +/* ---- Helper: extract mode line text from glyph rows ---- */ + +static NSString * +ns_ax_mode_line_text (struct window *w) - { -- font_panel_active = NO; ++{ + if (!w || !w->current_matrix) + return @""; - -- /* If no font was previously selected, use the currently selected -- font. */ ++ + struct glyph_matrix *matrix = w->current_matrix; + NSMutableString *text = [NSMutableString string]; - -- if (!font_panel_result && FRAME_FONT (emacsframe)) ++ + for (int i = 0; i < matrix->nrows; i++) - { -- font_panel_result -- = macfont_get_nsctfont (FRAME_FONT (emacsframe)); ++ { + struct glyph_row *row = matrix->rows + i; + if (!row->enabled_p || !row->mode_line_p) + continue; - -- if (font_panel_result) -- [font_panel_result retain]; ++ + struct glyph *g = row->glyphs[TEXT_AREA]; + struct glyph *end = g + row->used[TEXT_AREA]; + for (; g < end; g++) @@ -429,13 +429,12 @@ index 932d209..4288e68 100644 + length:1]]; + } + } - } -- -- [NSApp stop: self]; ++ } + return text; } --- (void) noteUserCancelledSelection +-#ifdef NS_IMPL_COCOA +-- (void) noteUserSelectedFont + +/* ---- Helper: screen rect for a character range via glyph matrix ---- */ + @@ -447,54 +446,32 @@ index 932d209..4288e68 100644 + if (!w || !w->current_matrix || !view) + return NSZeroRect; -- if (font_panel_result) -- [font_panel_result release]; -- font_panel_result = nil; +- /* If no font was previously selected, use the currently selected +- font. */ + /* Convert range indices back to buffer charpos. */ + ptrdiff_t cp_start = text_start + (ptrdiff_t) range.location; + ptrdiff_t cp_end = cp_start + (ptrdiff_t) range.length; -- [NSApp stop: self]; --} --#endif +- if (!font_panel_result && FRAME_FONT (emacsframe)) + struct glyph_matrix *matrix = w->current_matrix; + NSRect result = NSZeroRect; + BOOL found = NO; - --- (Lisp_Object) showFontPanel --{ -- id fm = [NSFontManager sharedFontManager]; -- struct font *font = FRAME_OUTPUT_DATA (emacsframe)->font; -- NSFont *nsfont, *result; -- struct timespec timeout; --#ifdef NS_IMPL_COCOA -- NSView *buttons; -- BOOL canceled; --#endif ++ + for (int i = 0; i < matrix->nrows; i++) -+ { + { +- font_panel_result +- = macfont_get_nsctfont (FRAME_FONT (emacsframe)); + struct glyph_row *row = matrix->rows + i; + if (!row->enabled_p || row->mode_line_p) + continue; + if (!row->displays_text_p && !row->ends_at_zv_p) + continue; --#ifdef NS_IMPL_GNUSTEP -- nsfont = ((struct nsfont_info *) font)->nsfont; --#else -- nsfont = (NSFont *) macfont_get_nsctfont (font); --#endif +- if (font_panel_result) +- [font_panel_result retain]; + ptrdiff_t row_start = MATRIX_ROW_START_CHARPOS (row); + ptrdiff_t row_end = MATRIX_ROW_END_CHARPOS (row); - --#ifdef NS_IMPL_COCOA -- buttons -- = ns_create_font_panel_buttons (self, -- @selector (noteUserSelectedFont), -- @selector (noteUserCancelledSelection)); -- [[fm fontPanel: YES] setAccessoryView: buttons]; -- [buttons release]; --#endif ++ + if (row_start < cp_end && row_end > cp_start) + { + int window_x, window_y, window_width; @@ -516,15 +493,12 @@ index 932d209..4288e68 100644 + else + result = NSUnionRect (result, rowRect); + } -+ } + } -- [fm setSelectedFont: nsfont isMultiple: NO]; -- [fm orderFrontFontPanel: NSApp]; +- [NSApp stop: self]; + if (!found) + return NSZeroRect; - -- font_panel_active = YES; -- timeout = make_timespec (0, 100000000); ++ + /* Clip result to text area bounds. */ + { + int text_area_x, text_area_y, text_area_w, text_area_h; @@ -534,24 +508,13 @@ index 932d209..4288e68 100644 + if (NSMaxY (result) > max_y) + result.size.height = max_y - result.origin.y; + } - -- block_input (); -- while (font_panel_active --#ifdef NS_IMPL_COCOA -- && (canceled = [[fm fontPanel: YES] isVisible]) --#else -- && [[fm fontPanel: YES] isVisible] --#endif -- ) -- ns_select_1 (0, NULL, NULL, NULL, &timeout, NULL, YES); -- unblock_input (); ++ + /* Convert from EmacsView (flipped) coords to screen coords. */ + NSRect winRect = [view convertRect:result toView:nil]; + return [[view window] convertRectToScreen:winRect]; -+} + } -- if (font_panel_result) -- [font_panel_result autorelease]; +-- (void) noteUserCancelledSelection +/* AX enum numeric compatibility for NSAccessibility notifications. + Values match WebKit AXObjectCacheMac fallback enums + (AXTextStateChangeType / AXTextEditType / AXTextSelectionDirection / @@ -560,43 +523,36 @@ index 932d209..4288e68 100644 + ns_ax_text_state_change_unknown = 0, + ns_ax_text_state_change_edit = 1, + ns_ax_text_state_change_selection_move = 2, - --#ifdef NS_IMPL_COCOA -- if (!canceled) -- font_panel_result = nil; --#endif ++ + ns_ax_text_edit_type_typing = 3, - -- result = font_panel_result; -- font_panel_result = nil; ++ + ns_ax_text_selection_direction_unknown = 0, + ns_ax_text_selection_direction_previous = 3, + ns_ax_text_selection_direction_next = 4, + ns_ax_text_selection_direction_discontiguous = 5, - -- [[fm fontPanel: YES] setIsVisible: NO]; -- font_panel_active = NO; ++ + ns_ax_text_selection_granularity_unknown = 0, + ns_ax_text_selection_granularity_character = 1, + ns_ax_text_selection_granularity_word = 2, + ns_ax_text_selection_granularity_line = 3, +}; - -- if (result) -- return ns_font_desc_to_font_spec ([result fontDescriptor], -- result); ++ +static NSUInteger +ns_ax_utf16_length_for_buffer_range (struct buffer *b, ptrdiff_t start, + ptrdiff_t end) -+{ + { +- font_panel_active = NO; + if (!b || end <= start) + return 0; -- return Qnil; +- if (font_panel_result) +- [font_panel_result release]; +- font_panel_result = nil; + struct buffer *oldb = current_buffer; + if (b != current_buffer) + set_buffer_internal_1 (b); -+ + +- [NSApp stop: self]; + Lisp_Object lstr = Fbuffer_substring_no_properties (make_fixnum (start), + make_fixnum (end)); + NSString *nsstr = [NSString stringWithLispString:lstr]; @@ -607,18 +563,31 @@ index 932d209..4288e68 100644 + + return len; } +-#endif --- (BOOL)acceptsFirstResponder +-- (Lisp_Object) showFontPanel +static BOOL +ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + ptrdiff_t *out_start, + ptrdiff_t *out_end) { -- NSTRACE ("[EmacsView acceptsFirstResponder]"); +- id fm = [NSFontManager sharedFontManager]; +- struct font *font = FRAME_OUTPUT_DATA (emacsframe)->font; +- NSFont *nsfont, *result; +- struct timespec timeout; +-#ifdef NS_IMPL_COCOA +- NSView *buttons; +- BOOL canceled; +-#endif + if (!b || !out_start || !out_end) + return NO; -+ -+ Lisp_Object faceSym = intern ("completions-highlight"); + +-#ifdef NS_IMPL_GNUSTEP +- nsfont = ((struct nsfont_info *) font)->nsfont; +-#else +- nsfont = (NSFont *) macfont_get_nsctfont (font); +-#endif ++ Lisp_Object faceSym = Qcompletions_highlight; + ptrdiff_t begv = BUF_BEGV (b); + ptrdiff_t zv = BUF_ZV (b); + ptrdiff_t best_start = 0; @@ -637,7 +606,15 @@ index 932d209..4288e68 100644 + ptrdiff_t p = probes[i]; + if (p < begv || p > zv) + continue; -+ + +-#ifdef NS_IMPL_COCOA +- buttons +- = ns_create_font_panel_buttons (self, +- @selector (noteUserSelectedFont), +- @selector (noteUserCancelledSelection)); +- [[fm fontPanel: YES] setAccessoryView: buttons]; +- [buttons release]; +-#endif + Lisp_Object overlays = Foverlays_at (make_fixnum (p), Qnil); + Lisp_Object tail; + for (tail = overlays; CONSP (tail); tail = XCDR (tail)) @@ -660,7 +637,9 @@ index 932d209..4288e68 100644 + break; + } + } -+ + +- [fm setSelectedFont: nsfont isMultiple: NO]; +- [fm orderFrontFontPanel: NSApp]; + if (!found) + { + for (ptrdiff_t scan = begv; scan < zv; scan++) @@ -698,33 +677,46 @@ index 932d209..4288e68 100644 + } + } + } -+ + +- font_panel_active = YES; +- timeout = make_timespec (0, 100000000); + if (!found) + return NO; -+ + +- block_input (); +- while (font_panel_active +-#ifdef NS_IMPL_COCOA +- && (canceled = [[fm fontPanel: YES] isVisible]) +-#else +- && [[fm fontPanel: YES] isVisible] +-#endif +- ) +- ns_select_1 (0, NULL, NULL, NULL, &timeout, NULL, YES); +- unblock_input (); + *out_start = best_start; + *out_end = best_end; - return YES; - } ++ return YES; ++} --/* Tell NS we want to accept clicks that activate the window */ --- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent +- if (font_panel_result) +- [font_panel_result autorelease]; +static bool -+ns_ax_event_is_ctrl_n_or_p (int *which) - { -- NSTRACE_MSG ("First mouse event: type=%ld, clickCount=%ld", -- [theEvent type], [theEvent clickCount]); -- return ns_click_through; ++ns_ax_event_is_line_nav_key (int *which) ++{ + Lisp_Object ev = last_command_event; + if (CONSP (ev)) + ev = EVENT_HEAD (ev); -+ + +-#ifdef NS_IMPL_COCOA +- if (!canceled) +- font_panel_result = nil; +-#endif + if (!FIXNUMP (ev)) + { + /* Handle symbol events: backtab (S-Tab = previous completion). */ + if (SYMBOLP (ev) && !NILP (ev)) + { -+ if (EQ (ev, intern ("backtab"))) ++ if (EQ (ev, Qbacktab)) + { + if (which) + *which = -1; @@ -733,7 +725,9 @@ index 932d209..4288e68 100644 + } + return false; + } -+ + +- result = font_panel_result; +- font_panel_result = nil; + EMACS_INT c = XFIXNUM (ev); + if (c == 14) /* C-n */ + { @@ -754,37 +748,17 @@ index 932d209..4288e68 100644 + return true; + } + return false; - } --- (void)resetCursorRects -+ -+static bool -+ns_ax_command_is_basic_line_move (void) - { -- NSRect visible = [self visibleRect]; -- NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe); -- NSTRACE ("[EmacsView resetCursorRects]"); -+ if (!SYMBOLP (Vreal_this_command)) -+ return false; - -- if (currentCursor == nil) -- currentCursor = [NSCursor arrowCursor]; -+ Lisp_Object next = intern ("next-line"); -+ Lisp_Object prev = intern ("previous-line"); -+ return EQ (Vreal_this_command, next) || EQ (Vreal_this_command, prev); +} -- if (!NSIsEmptyRect (visible)) -- [self addCursorRect: visible cursor: currentCursor]; +- [[fm fontPanel: YES] setIsVisible: NO]; +- font_panel_active = NO; +/* =================================================================== + EmacsAccessibilityInteractiveSpan — helpers and implementation + =================================================================== */ --#if defined (NS_IMPL_GNUSTEP) || MAC_OS_X_VERSION_MIN_REQUIRED < 101300 --#if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MAX_ALLOWED >= 101300 -- if ([currentCursor respondsToSelector: @selector(setOnMouseEntered:)]) --#endif -- [currentCursor setOnMouseEntered: YES]; --#endif +- if (result) +- return ns_font_desc_to_font_spec ([result fontDescriptor], +- result); +/* Extract announcement string from completion--string property value. + The property can be a plain Lisp string (simple completion) or + a list ("candidate" "annotation") for annotated completions. @@ -797,8 +771,9 @@ index 932d209..4288e68 100644 + if (CONSP (cstr) && STRINGP (XCAR (cstr))) + return [NSString stringWithLispString: XCAR (cstr)]; + return nil; - } ++} +- return Qnil; +/* Return the Emacs buffer Lisp object for window W, or Qnil. */ +static Lisp_Object +ns_ax_window_buffer_object (struct window *w) @@ -808,52 +783,62 @@ index 932d209..4288e68 100644 + if (!BUFFERP (w->contents)) + return Qnil; + return w->contents; -+} + } +-- (BOOL)acceptsFirstResponder +/* Compute visible-end charpos for window W. + Emacs stores it as BUF_Z - window_end_pos. */ +static ptrdiff_t +ns_ax_window_end_charpos (struct window *w, struct buffer *b) -+{ + { +- NSTRACE ("[EmacsView acceptsFirstResponder]"); +- return YES; + return BUF_Z (b) - w->window_end_pos; -+} + } --/*****************************************************************************/ --/* Keyboard handling. */ --#define NS_KEYLOG 0 +-/* Tell NS we want to accept clicks that activate the window */ +-- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent +/* Fetch text property PROP at charpos POS in BUF_OBJ. */ +static Lisp_Object +ns_ax_text_prop_at (ptrdiff_t pos, Lisp_Object prop, Lisp_Object buf_obj) -+{ + { +- NSTRACE_MSG ("First mouse event: type=%ld, clickCount=%ld", +- [theEvent type], [theEvent clickCount]); +- return ns_click_through; + Lisp_Object plist = Ftext_properties_at (make_fixnum (pos), buf_obj); + return Fplist_get (plist, prop, Qnil); -+} - --- (void)keyDown: (NSEvent *)theEvent + } +-- (void)resetCursorRects ++ +/* Next charpos where PROP changes, capped at LIMIT. */ +static ptrdiff_t +ns_ax_next_prop_change (ptrdiff_t pos, Lisp_Object prop, + Lisp_Object buf_obj, ptrdiff_t limit) { -- Mouse_HLInfo *hlinfo = MOUSE_HL_INFO (emacsframe); -- int code; +- NSRect visible = [self visibleRect]; +- NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe); +- NSTRACE ("[EmacsView resetCursorRects]"); + Lisp_Object result + = Fnext_single_property_change (make_fixnum (pos), prop, + buf_obj, make_fixnum (limit)); + return FIXNUMP (result) ? XFIXNUM (result) : limit; +} -+ + +- if (currentCursor == nil) +- currentCursor = [NSCursor arrowCursor]; +/* Build label for span [START, END) in BUF_OBJ. + Priority: completion--string → buffer text → help-echo. */ +static NSString * +ns_ax_get_span_label (ptrdiff_t start, ptrdiff_t end, + Lisp_Object buf_obj) +{ -+ Lisp_Object cs = ns_ax_text_prop_at (start, intern ("completion--string"), ++ Lisp_Object cs = ns_ax_text_prop_at (start, Qcompletion__string, + buf_obj); + if (STRINGP (cs)) + return [NSString stringWithLispString: cs]; -+ + +- if (!NSIsEmptyRect (visible)) +- [self addCursorRect: visible cursor: currentCursor]; + if (end > start) + { + Lisp_Object substr = Fbuffer_substring_no_properties ( @@ -867,7 +852,13 @@ index 932d209..4288e68 100644 + return s; + } + } -+ + +-#if defined (NS_IMPL_GNUSTEP) || MAC_OS_X_VERSION_MIN_REQUIRED < 101300 +-#if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MAX_ALLOWED >= 101300 +- if ([currentCursor respondsToSelector: @selector(setOnMouseEntered:)]) +-#endif +- [currentCursor setOnMouseEntered: YES]; +-#endif + Lisp_Object he = ns_ax_text_prop_at (start, Qhelp_echo, buf_obj); + if (STRINGP (he)) + return [NSString stringWithLispString: he]; @@ -904,7 +895,7 @@ index 932d209..4288e68 100644 + /* Symbols are interned once at startup via DEFSYM in syms_of_nsterm; + reference them directly here (GC-safe, no repeated obarray lookup). */ + -+ BOOL is_completion_buf = EQ (BVAR (b, major_mode), Qax_completion_list_mode); ++ BOOL is_completion_buf = EQ (BVAR (b, major_mode), Qcompletion_list_mode); + + NSMutableArray *spans = [NSMutableArray array]; + ptrdiff_t pos = vis_start; @@ -915,25 +906,25 @@ index 932d209..4288e68 100644 + EmacsAXSpanType span_type = (EmacsAXSpanType) -1; + Lisp_Object limit_prop = Qnil; + -+ if (!NILP (Fplist_get (plist, Qax_widget, Qnil))) ++ if (!NILP (Fplist_get (plist, Qwidget, Qnil))) + { + span_type = EmacsAXSpanTypeWidget; -+ limit_prop = Qax_widget; ++ limit_prop = Qwidget; + } -+ else if (!NILP (Fplist_get (plist, Qax_button, Qnil))) ++ else if (!NILP (Fplist_get (plist, Qbutton, Qnil))) + { + span_type = EmacsAXSpanTypeButton; -+ limit_prop = Qax_button; ++ limit_prop = Qbutton; + } -+ else if (!NILP (Fplist_get (plist, Qax_follow_link, Qnil))) ++ else if (!NILP (Fplist_get (plist, Qfollow_link, Qnil))) + { + span_type = EmacsAXSpanTypeLink; -+ limit_prop = Qax_follow_link; ++ limit_prop = Qfollow_link; + } -+ else if (!NILP (Fplist_get (plist, Qax_org_link, Qnil))) ++ else if (!NILP (Fplist_get (plist, Qorg_link, Qnil))) + { + span_type = EmacsAXSpanTypeLink; -+ limit_prop = Qax_org_link; ++ limit_prop = Qorg_link; + } + else if (is_completion_buf + && !NILP (Fplist_get (plist, Qmouse_face, Qnil))) @@ -942,7 +933,7 @@ index 932d209..4288e68 100644 + don't accidentally merge two column-adjacent candidates + whose mouse-face regions may share padding whitespace. + Fall back to mouse-face if completion--string is absent. */ -+ Lisp_Object cs_sym = intern ("completion--string"); ++ Lisp_Object cs_sym = Qcompletion__string; + Lisp_Object cs_val = ns_ax_text_prop_at (pos, cs_sym, buf_obj); + span_type = EmacsAXSpanTypeCompletionItem; + limit_prop = NILP (cs_val) ? Qmouse_face : cs_sym; @@ -1046,6 +1037,9 @@ index 932d209..4288e68 100644 + dispatch_async (dispatch_get_main_queue (), ^{ + if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin))) + return; ++ /* block_input prevents SIGIO delivery while we modify point/buffer. ++ It is safe here because this GCD block runs on the main thread ++ during the Cocoa run loop, outside of any Emacs event processing. */ + block_input (); + Fselect_window (lwin, Qnil); + struct window *w = XWINDOW (lwin); @@ -1126,7 +1120,7 @@ index 932d209..4288e68 100644 + { + ptrdiff_t p = probes[i]; + Lisp_Object cstr = Fget_char_property (make_fixnum (p), -+ intern ("completion--string"), ++ Qcompletion__string, + Qnil); + if (STRINGP (cstr)) + text = [NSString stringWithLispString:cstr]; @@ -1134,7 +1128,7 @@ index 932d209..4288e68 100644 + { + /* Fallback: 'completion property used by display-completion-list. */ + cstr = Fget_char_property (make_fixnum (p), -+ intern ("completion"), ++ Qcompletion, + Qnil); + if (STRINGP (cstr)) + text = [NSString stringWithLispString:cstr]; @@ -1444,7 +1438,9 @@ index 932d209..4288e68 100644 + if (![NSThread isMainThread]) + { + __block id result; -+ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilityValue]; }); ++ dispatch_sync (dispatch_get_main_queue (), ^{ ++ result = [self accessibilityValue]; ++ }); + return result; + } + [self ensureTextCache]; @@ -1480,7 +1476,9 @@ index 932d209..4288e68 100644 + if (![NSThread isMainThread]) + { + __block NSRange result; -+ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilitySelectedTextRange]; }); ++ dispatch_sync (dispatch_get_main_queue (), ^{ ++ result = [self accessibilitySelectedTextRange]; ++ }); + return result; + } + struct window *w = [self validWindow]; @@ -1588,8 +1586,11 @@ index 932d209..4288e68 100644 + /* Post SelectedTextChanged so VoiceOver reads the current line + upon entering text interaction mode. + WebKit AXObjectCacheMac fallback enum: SelectionMove = 2. */ -+ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), -+ @"AXTextChangeElement": self}; ++ NSDictionary *info = @{ ++ @"AXTextStateChangeType": ++ @(ns_ax_text_state_change_selection_move), ++ @"AXTextChangeElement": self ++ }; + NSAccessibilityPostNotificationWithUserInfo ( + self, NSAccessibilitySelectedTextChangedNotification, info); +} @@ -1599,7 +1600,9 @@ index 932d209..4288e68 100644 + if (![NSThread isMainThread]) + { + __block NSInteger result; -+ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilityInsertionPointLineNumber]; }); ++ dispatch_sync (dispatch_get_main_queue (), ^{ ++ result = [self accessibilityInsertionPointLineNumber]; ++ }); + return result; + } + struct window *w = [self validWindow]; @@ -1719,7 +1722,9 @@ index 932d209..4288e68 100644 + if (![NSThread isMainThread]) + { + __block NSRect result; -+ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilityFrameForRange:range]; }); ++ dispatch_sync (dispatch_get_main_queue (), ^{ ++ result = [self accessibilityFrameForRange:range]; ++ }); + return result; + } + struct window *w = [self validWindow]; @@ -1740,7 +1745,10 @@ index 932d209..4288e68 100644 + if (![NSThread isMainThread]) + { + __block NSRange result; -+ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilityRangeForPosition:screenPoint]; }); ++ dispatch_sync (dispatch_get_main_queue (), ^{ ++ result ++ = [self accessibilityRangeForPosition:screenPoint]; ++ }); + return result; + } + /* Hit test: convert screen point to buffer character index. */ @@ -1935,7 +1943,7 @@ index 932d209..4288e68 100644 + direction = ns_ax_text_selection_direction_previous; + + int ctrlNP = 0; -+ bool isCtrlNP = ns_ax_event_is_ctrl_n_or_p (&ctrlNP); ++ bool isCtrlNP = ns_ax_event_is_line_nav_key (&ctrlNP); + + /* --- Granularity detection --- + Compare old and new cursor positions in cachedText to determine @@ -2074,7 +2082,7 @@ index 932d209..4288e68 100644 + /* 1. completion--string at point. */ + Lisp_Object cstr + = Fget_char_property (make_fixnum (point), -+ intern ("completion--string"), Qnil); ++ Qcompletion__string, Qnil); + announceText = ns_ax_completion_string_from_prop (cstr); + + /* 2. Fallback: full line text. */ @@ -2141,7 +2149,7 @@ index 932d209..4288e68 100644 + or a list ("candidate" "annotation") for annotated completions. + In the list case, use car (the completion itself). */ + Lisp_Object cstr = Fget_char_property (make_fixnum (point), -+ intern ("completion--string"), ++ Qcompletion__string, + Qnil); + announceText = ns_ax_completion_string_from_prop (cstr); + @@ -2192,7 +2200,7 @@ index 932d209..4288e68 100644 + /* 3) Fallback: check completions-highlight overlay span at point. */ + if (!announceText) + { -+ Lisp_Object faceSym = intern ("completions-highlight"); ++ Lisp_Object faceSym = Qcompletions_highlight; + Lisp_Object overlays = Foverlays_at (make_fixnum (point), Qnil); + Lisp_Object tail; + for (tail = overlays; CONSP (tail); tail = XCDR (tail)) @@ -2283,14 +2291,14 @@ index 932d209..4288e68 100644 + self.cachedCompletionAnnouncement = announceText; + self.cachedCompletionOverlayStart = currentOverlayStart; + self.cachedCompletionOverlayEnd = currentOverlayEnd; -+ self.cachedCompletionPoint = point; ++ self.cachedCompletionPoint = point; + } + else + { + self.cachedCompletionAnnouncement = nil; + self.cachedCompletionOverlayStart = 0; + self.cachedCompletionOverlayEnd = 0; -+ self.cachedCompletionPoint = 0; ++ self.cachedCompletionPoint = 0; + } + } + else @@ -2298,90 +2306,23 @@ index 932d209..4288e68 100644 + self.cachedCompletionAnnouncement = nil; + self.cachedCompletionOverlayStart = 0; + self.cachedCompletionOverlayEnd = 0; -+ self.cachedCompletionPoint = 0; ++ self.cachedCompletionPoint = 0; + } + } + + } + else + { ++ /* Nothing changed (no text edit, no cursor move, no mark change). ++ Overlay state cannot change without a modiff bump, so no scan ++ needed for non-focused buffers. Just reset completion cache ++ for focused buffer to avoid stale announcements. */ + if ([self isAccessibilityFocused]) + { + self.cachedCompletionAnnouncement = nil; + self.cachedCompletionOverlayStart = 0; + self.cachedCompletionOverlayEnd = 0; -+ self.cachedCompletionPoint = 0; -+ } -+ else -+ { -+ [self ensureTextCache]; -+ if (cachedText) -+ { -+ NSString *announceText = nil; -+ ptrdiff_t currentOverlayStart = 0; -+ ptrdiff_t currentOverlayEnd = 0; -+ struct buffer *oldb2 = current_buffer; -+ if (b != current_buffer) -+ set_buffer_internal_1 (b); -+ -+ if (ns_ax_find_completion_overlay_range (b, point, -+ ¤tOverlayStart, -+ ¤tOverlayEnd)) -+ { -+ announceText = ns_ax_completion_text_for_span (self, b, -+ currentOverlayStart, -+ currentOverlayEnd, -+ cachedText); -+ } -+ -+ if (b != oldb2) -+ set_buffer_internal_1 (oldb2); -+ -+ if (announceText) -+ { -+ announceText = [announceText stringByTrimmingCharactersInSet: -+ [NSCharacterSet whitespaceAndNewlineCharacterSet]]; -+ if ([announceText length] > 0) -+ { -+ BOOL textChanged = ![announceText isEqualToString: -+ self.cachedCompletionAnnouncement]; -+ BOOL overlayChanged = -+ (currentOverlayStart != self.cachedCompletionOverlayStart -+ || currentOverlayEnd != self.cachedCompletionOverlayEnd); -+ BOOL pointChanged = (point != self.cachedCompletionPoint); -+ if (textChanged || overlayChanged || pointChanged) -+ { -+ NSDictionary *annInfo = @{ -+ NSAccessibilityAnnouncementKey: announceText, -+ NSAccessibilityPriorityKey: -+ @(NSAccessibilityPriorityHigh) -+ }; -+ NSAccessibilityPostNotificationWithUserInfo ( -+ NSApp, -+ NSAccessibilityAnnouncementRequestedNotification, -+ annInfo); -+ } -+ self.cachedCompletionAnnouncement = announceText; -+ self.cachedCompletionOverlayStart = currentOverlayStart; -+ self.cachedCompletionOverlayEnd = currentOverlayEnd; -+ self.cachedCompletionPoint = point; -+ } -+ else -+ { -+ self.cachedCompletionAnnouncement = nil; -+ self.cachedCompletionOverlayStart = 0; -+ self.cachedCompletionOverlayEnd = 0; -+ self.cachedCompletionPoint = 0; -+ } -+ } -+ else -+ { -+ self.cachedCompletionAnnouncement = nil; -+ self.cachedCompletionOverlayStart = 0; -+ self.cachedCompletionOverlayEnd = 0; -+ self.cachedCompletionPoint = 0; -+ } -+ } ++ self.cachedCompletionPoint = 0; + } + } +} @@ -2655,22 +2596,10 @@ index 932d209..4288e68 100644 +#endif + [currentCursor setOnMouseEntered: YES]; +#endif -+} -+ -+ -+ -+/*****************************************************************************/ -+/* Keyboard handling. */ -+#define NS_KEYLOG 0 -+ -+- (void)keyDown: (NSEvent *)theEvent -+{ -+ Mouse_HLInfo *hlinfo = MOUSE_HL_INFO (emacsframe); -+ int code; - unsigned fnKeysym = 0; - static NSMutableArray *nsEvArray; - unsigned int flags = [theEvent modifierFlags]; -@@ -8237,6 +10364,28 @@ - (void)windowDidBecomeKey /* for direct calls */ + } + + +@@ -8237,6 +10306,31 @@ - (void)windowDidBecomeKey /* for direct calls */ XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -2686,8 +2615,11 @@ index 932d209..4288e68 100644 + { + NSAccessibilityPostNotification (focused, + NSAccessibilityFocusedUIElementChangedNotification); -+ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), -+ @"AXTextChangeElement": focused}; ++ NSDictionary *info = @{ ++ @"AXTextStateChangeType": ++ @(ns_ax_text_state_change_selection_move), ++ @"AXTextChangeElement": focused ++ }; + NSAccessibilityPostNotificationWithUserInfo (focused, + NSAccessibilitySelectedTextChangedNotification, info); + } @@ -2699,7 +2631,7 @@ index 932d209..4288e68 100644 } -@@ -9474,6 +11623,307 @@ - (int) fullscreenState +@@ -9474,6 +11568,320 @@ - (int) fullscreenState return fs_state; } @@ -2850,8 +2782,7 @@ index 932d209..4288e68 100644 + elements with current values, making change detection impossible. */ +- (void)postAccessibilityUpdates +{ -+ NSCAssert ([NSThread isMainThread], -+ @"postAccessibilityUpdates must be called on the main thread"); ++ eassert ([NSThread isMainThread]); + + if (!emacsframe) + return; @@ -2862,7 +2793,6 @@ index 932d209..4288e68 100644 + if (accessibilityUpdating) + return; + accessibilityUpdating = YES; -+ @try { + + /* Detect window tree change (split, delete, new buffer). Compare + FRAME_ROOT_WINDOW — if it changed, the tree structure changed. */ @@ -2924,8 +2854,11 @@ index 932d209..4288e68 100644 + { + NSAccessibilityPostNotification (focused, + NSAccessibilityFocusedUIElementChangedNotification); -+ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), -+ @"AXTextChangeElement": focused}; ++ NSDictionary *info = @{ ++ @"AXTextStateChangeType": ++ @(ns_ax_text_state_change_selection_move), ++ @"AXTextChangeElement": focused ++ }; + NSAccessibilityPostNotificationWithUserInfo (focused, + NSAccessibilitySelectedTextChangedNotification, info); + } @@ -2934,9 +2867,7 @@ index 932d209..4288e68 100644 + NSAccessibilityFocusedUIElementChangedNotification); + } + -+ } @finally { -+ accessibilityUpdating = NO; -+ } ++ accessibilityUpdating = NO; +} + +/* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ---- @@ -2964,11 +2895,25 @@ index 932d209..4288e68 100644 + return [win convertRectToScreen:windowRect]; +} + ++/* Modern NSAccessibility protocol entry point. Delegates to ++ accessibilityBoundsForRange: which holds the real implementation ++ shared with the legacy parameterized-attribute API. */ +- (NSRect)accessibilityFrameForRange:(NSRange)range +{ + return [self accessibilityBoundsForRange:range]; +} + ++/* Delegate to the focused virtual buffer element so both the modern ++ and legacy APIs return the correct string data. */ ++- (NSString *)accessibilityStringForRange:(NSRange)range ++{ ++ id focused = [self accessibilityFocusedUIElement]; ++ if ([focused isKindOfClass:[EmacsAccessibilityBuffer class]]) ++ return [(EmacsAccessibilityBuffer *) focused ++ accessibilityStringForRange:range]; ++ return @""; ++} ++ +/* ---- Legacy parameterized attribute APIs (Zoom uses these) ---- */ + +- (NSArray *)accessibilityParameterizedAttributeNames @@ -3007,16 +2952,20 @@ index 932d209..4288e68 100644 @end /* EmacsView */ -@@ -11303,6 +13753,14 @@ Convert an X font name (XLFD) to an NS font name. +@@ -11303,6 +13711,18 @@ Convert an X font name (XLFD) to an NS font name. DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic"); DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion"); + /* Accessibility span scanning symbols. */ -+ DEFSYM (Qax_widget, "widget"); -+ DEFSYM (Qax_button, "button"); -+ DEFSYM (Qax_follow_link, "follow-link"); -+ DEFSYM (Qax_org_link, "org-link"); -+ DEFSYM (Qax_completion_list_mode, "completion-list-mode"); ++ DEFSYM (Qwidget, "widget"); ++ DEFSYM (Qbutton, "button"); ++ DEFSYM (Qfollow_link, "follow-link"); ++ DEFSYM (Qorg_link, "org-link"); ++ DEFSYM (Qcompletion_list_mode, "completion-list-mode"); ++ DEFSYM (Qcompletion__string, "completion--string"); ++ DEFSYM (Qcompletion, "completion"); ++ DEFSYM (Qcompletions_highlight, "completions-highlight"); ++ DEFSYM (Qbacktab, "backtab"); + /* Qmouse_face and Qkeymap are defined in textprop.c / keymap.c. */ + Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier));