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 65e3be3..39ac7e8 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -1,19 +1,19 @@ -From 03ad0337a0f4cf8b01261eb34068fb17cc925e96 Mon Sep 17 00:00:00 2001 +From c0d683db88b52e0e154b6434a5f29ed3ed36703d Mon Sep 17 00:00:00 2001 From: Martin Sukany -Date: Fri, 27 Feb 2026 10:10:36 +0100 +Date: Fri, 27 Feb 2026 10:26:21 +0100 Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line - nav, completions) + nav, completions, interactive spans) --- - src/nsterm.h | 73 ++ - src/nsterm.m | 2262 +++++++++++++++++++++++++++++++++++++++++++++++--- - 2 files changed, 2210 insertions(+), 125 deletions(-) + src/nsterm.h | 110 +++ + src/nsterm.m | 2646 +++++++++++++++++++++++++++++++++++++++++++++++--- + 2 files changed, 2606 insertions(+), 150 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h -index 7c1ee4c..717a838 100644 +index 7c1ee4c..bc9e17b 100644 --- a/src/nsterm.h +++ b/src/nsterm.h -@@ -453,6 +453,64 @@ enum ns_return_frame_mode +@@ -453,6 +453,101 @@ enum ns_return_frame_mode @end @@ -51,7 +51,9 @@ index 7c1ee4c..717a838 100644 +@interface EmacsAccessibilityBuffer : EmacsAccessibilityElement +{ + ns_ax_visible_run *visibleRuns; -+ NSUInteger visibleRunCount; ++ NSUInteger visibleRunCount; ++ NSMutableArray *cachedInteractiveSpans; ++ BOOL interactiveSpansDirty; +} +@property (nonatomic, retain) NSString *cachedText; +@property (nonatomic, assign) ptrdiff_t cachedTextModiff; @@ -64,6 +66,7 @@ index 7c1ee4c..717a838 100644 +@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayEnd; +@property (nonatomic, assign) ptrdiff_t cachedCompletionPoint; +- (void)invalidateTextCache; ++- (void)invalidateInteractiveSpans; +- (void)postAccessibilityNotificationsForFrame:(struct frame *)f; +- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx; +- (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos; @@ -72,13 +75,47 @@ index 7c1ee4c..717a838 100644 +/* Virtual AXStaticText element — one per mode line. */ +@interface EmacsAccessibilityModeLine : EmacsAccessibilityElement +@end ++ ++/* Span types for interactive AX child elements. */ ++typedef NS_ENUM (NSInteger, EmacsAXSpanType) ++{ ++ EmacsAXSpanTypeButton = 0, ++ EmacsAXSpanTypeLink = 1, ++ EmacsAXSpanTypeCheckBox = 2, ++ EmacsAXSpanTypeTextField = 3, ++ EmacsAXSpanTypePopUpButton = 4, ++ EmacsAXSpanTypeCompletionItem = 5, ++ EmacsAXSpanTypeWidget = 6, ++}; ++ ++/* A lightweight AX element representing one interactive text span ++ (button, link, checkbox, completion candidate, etc.) within a buffer ++ window. Exposed as AX child of EmacsAccessibilityBuffer so VoiceOver ++ Tab navigation can reach individual interactive elements. */ ++@interface EmacsAccessibilityInteractiveSpan : EmacsAccessibilityElement ++ ++@property (nonatomic, assign) ptrdiff_t charposStart; ++@property (nonatomic, assign) ptrdiff_t charposEnd; ++@property (nonatomic, assign) EmacsAXSpanType spanType; ++@property (nonatomic, copy) NSString *spanLabel; ++@property (nonatomic, copy) NSString *spanValue; ++@property (nonatomic, weak) EmacsAccessibilityBuffer *parentBuffer; ++ ++- (NSAccessibilityRole) accessibilityRole; ++- (NSString *) accessibilityLabel; ++- (NSRect) accessibilityFrame; ++- (BOOL) isAccessibilityElement; ++- (BOOL) isAccessibilityFocused; ++- (void) setAccessibilityFocused: (BOOL) focused; ++ ++@end +#endif /* NS_IMPL_COCOA */ + + /* ========================================================================== The main Emacs view -@@ -471,6 +529,14 @@ enum ns_return_frame_mode +@@ -471,6 +566,14 @@ enum ns_return_frame_mode #ifdef NS_IMPL_COCOA char *old_title; BOOL maximizing_resize; @@ -93,7 +130,7 @@ index 7c1ee4c..717a838 100644 #endif BOOL font_panel_active; NSFont *font_panel_result; -@@ -528,6 +594,13 @@ enum ns_return_frame_mode +@@ -528,6 +631,13 @@ enum ns_return_frame_mode - (void)windowWillExitFullScreen; - (void)windowDidExitFullScreen; - (void)windowDidBecomeKey; @@ -108,7 +145,7 @@ index 7c1ee4c..717a838 100644 diff --git a/src/nsterm.m b/src/nsterm.m -index 932d209..0fa6b2d 100644 +index 932d209..1acdc42 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) @@ -169,7 +206,7 @@ index 932d209..0fa6b2d 100644 ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -@@ -6849,194 +6886,1849 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg +@@ -6849,220 +6886,2205 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg /* ========================================================================== @@ -184,57 +221,93 @@ index 932d209..0fa6b2d 100644 +/* ---- Helper: extract buffer text for accessibility ---- */ -- (void)windowDidEndLiveResize:(NSNotification *)notification +-{ +- [self updateFramePosition]; +-} +/* Maximum characters exposed via accessibilityValue. */ +#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) - { -- [self updateFramePosition]; ++{ + *out_runs = NULL; + *out_nruns = 0; -+ + + if (!w || !WINDOW_LEAF_P (w)) + { + *out_start = 0; + return @""; + } -+ + +-- (void)dealloc +-{ +- NSTRACE ("[EmacsView dealloc]"); + struct buffer *b = XBUFFER (w->contents); + if (!b) + { + *out_start = 0; + return @""; + } -+ + +- /* 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); -+ + +- if (fs_state == FULLSCREEN_BOTH) +- [nonfs_window release]; + *out_start = begv; -+ + +-#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); -+ + + /* First pass: count visible runs to allocate the mapping array. */ + NSUInteger run_capacity = 64; + ns_ax_visible_run *runs = xmalloc (run_capacity + * sizeof (ns_ax_visible_run)); + NSUInteger nruns = 0; + NSUInteger ax_offset = 0; -+ + +-/* 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). @@ -252,12 +325,16 @@ index 932d209..0fa6b2d 100644 + pos = FIXNUMP (next) ? XFIXNUM (next) : zv; + continue; + } -+ + +- if (!font_panel_active) +- return; + /* 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]; + /* 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) @@ -288,41 +365,54 @@ index 932d209..0fa6b2d 100644 + runs[nruns].ax_start = ax_offset; + runs[nruns].ax_length = ns_len; + nruns++; -+ + +- font_panel_result = (NSFont *) [sender convertFont: nsfont]; + ax_offset += ns_len; + pos = run_end; + } -+ + +- if (font_panel_result) +- [font_panel_result retain]; + if (b != oldb) + set_buffer_internal_1 (oldb); -+ + +-#ifndef NS_IMPL_COCOA +- font_panel_active = NO; +- [NSApp stop: self]; +-#endif + *out_runs = runs; + *out_nruns = nruns; + return result; } --/* Needed to inform when window closed from lisp. */ --- (void) setWindowClosing: (BOOL)closing +-#ifdef NS_IMPL_COCOA +-- (void) noteUserSelectedFont + +/* ---- Helper: extract mode line text from glyph rows ---- */ + +static NSString * +ns_ax_mode_line_text (struct window *w) { -- NSTRACE ("[EmacsView setWindowClosing:%d]", closing); +- font_panel_active = NO; + if (!w || !w->current_matrix) + return @""; -- windowClosing = closing; +- /* 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++) @@ -334,41 +424,47 @@ index 932d209..0fa6b2d 100644 + length:1]]; + } + } -+ } + } +- +- [NSApp stop: self]; + return text; } - --- (void)dealloc +-- (void) noteUserCancelledSelection ++ +/* ---- Helper: screen rect for a character range via glyph matrix ---- */ + +static NSRect +ns_ax_frame_for_range (struct window *w, EmacsView *view, + ptrdiff_t text_start, NSRange range) { -- NSTRACE ("[EmacsView dealloc]"); +- font_panel_active = NO; + if (!w || !w->current_matrix || !view) + return NSZeroRect; -- /* Clear the view resize notification. */ -- [[NSNotificationCenter defaultCenter] -- removeObserver:self -- name:NSViewFrameDidChangeNotification -- object:nil]; +- if (font_panel_result) +- [font_panel_result release]; +- font_panel_result = nil; + /* 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; -- if (fs_state == FULLSCREEN_BOTH) -- [nonfs_window release]; +- [NSApp stop: self]; +-} +-#endif + struct glyph_matrix *matrix = w->current_matrix; + NSRect result = NSZeroRect; + BOOL found = NO; --#if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MIN_REQUIRED >= 101400 -- /* Release layer and menu */ -- EmacsLayer *layer = (EmacsLayer *)[self layer]; -- [layer release]; +-- (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++) + { @@ -378,11 +474,22 @@ index 932d209..0fa6b2d 100644 + if (!row->displays_text_p && !row->ends_at_zv_p) + continue; -- [[self menu] release]; -- [super dealloc]; +-#ifdef NS_IMPL_GNUSTEP +- nsfont = ((struct nsfont_info *) font)->nsfont; +-#else +- nsfont = (NSFont *) macfont_get_nsctfont (font); +-#endif + 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; @@ -405,10 +512,14 @@ index 932d209..0fa6b2d 100644 + result = NSUnionRect (result, rowRect); + } + } -+ + +- [fm setSelectedFont: nsfont isMultiple: NO]; +- [fm orderFrontFontPanel: NSApp]; + 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; @@ -418,12 +529,24 @@ index 932d209..0fa6b2d 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]; +/* AX enum numeric compatibility for NSAccessibility notifications. + Values match WebKit AXObjectCacheMac fallback enums + (AXTextStateChangeType / AXTextEditType / AXTextSelectionDirection / @@ -433,32 +556,29 @@ index 932d209..0fa6b2d 100644 + ns_ax_text_state_change_edit = 1, + ns_ax_text_state_change_selection_move = 2, --/* Called on font panel selection. */ --- (void) changeFont: (id) sender --{ -- struct font *font = FRAME_OUTPUT_DATA (emacsframe)->font; -- NSFont *nsfont; +-#ifdef NS_IMPL_COCOA +- if (!canceled) +- font_panel_result = nil; +-#endif + ns_ax_text_edit_type_typing = 3, --#ifdef NS_IMPL_GNUSTEP -- nsfont = ((struct nsfont_info *) font)->nsfont; --#else -- nsfont = (NSFont *) macfont_get_nsctfont (font); --#endif +- 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, -- if (!font_panel_active) -- return; +- [[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_line = 3, +}; -- if (font_panel_result) -- [font_panel_result release]; +- 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) @@ -466,41 +586,32 @@ index 932d209..0fa6b2d 100644 + if (!b || end <= start) + return 0; -- font_panel_result = (NSFont *) [sender convertFont: nsfont]; +- return Qnil; + struct buffer *oldb = current_buffer; + if (b != current_buffer) + set_buffer_internal_1 (b); - -- if (font_panel_result) -- [font_panel_result retain]; ++ + Lisp_Object lstr = Fbuffer_substring_no_properties (make_fixnum (start), + make_fixnum (end)); + NSString *nsstr = [NSString stringWithLispString:lstr]; + NSUInteger len = [nsstr length]; - --#ifndef NS_IMPL_COCOA -- font_panel_active = NO; -- [NSApp stop: self]; --#endif ++ + if (b != oldb) + set_buffer_internal_1 (oldb); + + return len; } --#ifdef NS_IMPL_COCOA --- (void) noteUserSelectedFont +-- (BOOL)acceptsFirstResponder +static BOOL +ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + ptrdiff_t *out_start, + ptrdiff_t *out_end) { -- font_panel_active = NO; +- NSTRACE ("[EmacsView acceptsFirstResponder]"); + if (!b || !out_start || !out_end) + return NO; - -- /* If no font was previously selected, use the currently selected -- font. */ ++ + Lisp_Object faceSym = intern ("completions-highlight"); + ptrdiff_t begv = BUF_BEGV (b); + ptrdiff_t zv = BUF_ZV (b); @@ -520,11 +631,7 @@ index 932d209..0fa6b2d 100644 + ptrdiff_t p = probes[i]; + if (p < begv || p > zv) + continue; - -- if (!font_panel_result && FRAME_FONT (emacsframe)) -- { -- font_panel_result -- = macfont_get_nsctfont (FRAME_FONT (emacsframe)); ++ + Lisp_Object overlays = Foverlays_at (make_fixnum (p), Qnil); + Lisp_Object tail; + for (tail = overlays; CONSP (tail); tail = XCDR (tail)) @@ -547,9 +654,7 @@ index 932d209..0fa6b2d 100644 + break; + } + } - -- if (font_panel_result) -- [font_panel_result retain]; ++ + if (!found) + { + for (ptrdiff_t scan = begv; scan < zv; scan++) @@ -586,29 +691,28 @@ index 932d209..0fa6b2d 100644 + } + } + } - } - -- [NSApp stop: self]; ++ } ++ + if (!found) + return NO; + + *out_start = best_start; + *out_end = best_end; -+ return YES; + return YES; } --- (void) noteUserCancelledSelection +-/* Tell NS we want to accept clicks that activate the window */ +-- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent +static bool +ns_ax_event_is_ctrl_n_or_p (int *which) { -- font_panel_active = NO; +- NSTRACE_MSG ("First mouse event: type=%ld, clickCount=%ld", +- [theEvent type], [theEvent clickCount]); +- return ns_click_through; + Lisp_Object ev = last_command_event; + if (CONSP (ev)) + ev = EVENT_HEAD (ev); - -- if (font_panel_result) -- [font_panel_result release]; -- font_panel_result = nil; ++ + if (!FIXNUMP (ev)) + { + /* Handle symbol events: backtab (S-Tab = previous completion). */ @@ -623,8 +727,7 @@ index 932d209..0fa6b2d 100644 + } + return false; + } - -- [NSApp stop: self]; ++ + EMACS_INT c = XFIXNUM (ev); + if (c == 14) /* C-n */ + { @@ -646,41 +749,353 @@ index 932d209..0fa6b2d 100644 + } + return false; } --#endif - --- (Lisp_Object) showFontPanel +-- (void)resetCursorRects ++ +static bool +ns_ax_command_is_basic_line_move (void) { -- 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 +- NSRect visible = [self visibleRect]; +- NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe); +- NSTRACE ("[EmacsView resetCursorRects]"); + if (!SYMBOLP (Vreal_this_command)) + return false; --#ifdef NS_IMPL_GNUSTEP -- nsfont = ((struct nsfont_info *) font)->nsfont; --#else -- nsfont = (NSFont *) macfont_get_nsctfont (font); --#endif +- 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); +} --#ifdef NS_IMPL_COCOA -- buttons -- = ns_create_font_panel_buttons (self, -- @selector (noteUserSelectedFont), -- @selector (noteUserCancelledSelection)); -- [[fm fontPanel: YES] setAccessoryView: buttons]; -- [buttons release]; +- if (!NSIsEmptyRect (visible)) +- [self addCursorRect: visible cursor: currentCursor]; ++/* =================================================================== ++ 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 ++/* Return the Emacs buffer Lisp object for window W, or Qnil. */ ++static Lisp_Object ++ns_ax_window_buffer_object (struct window *w) ++{ ++ if (!w) ++ return Qnil; ++ if (!BUFFERP (w->contents)) ++ return Qnil; ++ return w->contents; + } + ++/* 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) ++{ ++ return BUF_Z (b) - w->window_end_pos; ++} + ++/* 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) ++{ ++ Lisp_Object plist = Ftext_properties_at (make_fixnum (pos), buf_obj); ++ return Fplist_get (plist, prop); ++} + +-/*****************************************************************************/ +-/* Keyboard handling. */ +-#define NS_KEYLOG 0 ++/* 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) ++{ ++ Lisp_Object result ++ = Fnext_single_property_change (make_fixnum (pos), prop, ++ buf_obj, make_fixnum (limit)); ++ return FIXNUMP (result) ? XFIXNUM (result) : limit; ++} + +-- (void)keyDown: (NSEvent *)theEvent ++/* 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) + { +- Mouse_HLInfo *hlinfo = MOUSE_HL_INFO (emacsframe); +- int code; +- unsigned fnKeysym = 0; ++ Lisp_Object cs = ns_ax_text_prop_at (start, intern ("completion--string"), ++ buf_obj); ++ if (STRINGP (cs)) ++ return [NSString stringWithLispString: cs]; ++ ++ if (end > start) ++ { ++ Lisp_Object substr = Fbuffer_substring_no_properties ( ++ make_fixnum (start), make_fixnum (end)); ++ if (STRINGP (substr)) ++ { ++ NSString *s = [NSString stringWithLispString: substr]; ++ s = [s stringByTrimmingCharactersInSet: ++ [NSCharacterSet whitespaceAndNewlineCharacterSet]]; ++ if (s.length > 0) ++ return s; ++ } ++ } ++ ++ Lisp_Object he = ns_ax_text_prop_at (start, Qhelp_echo, buf_obj); ++ if (STRINGP (he)) ++ return [NSString stringWithLispString: he]; ++ ++ return @""; ++} ++ ++/* Scan visible range of window W for interactive spans. ++ Returns NSArray. ++ ++ Priority when properties overlap: ++ widget > button > follow-link > org-link > ++ completion-candidate > keymap-overlay. */ ++static NSArray * ++ns_ax_scan_interactive_spans (struct window *w, ++ EmacsAccessibilityBuffer *parent_buf) ++{ ++ if (!w) ++ return @[]; ++ ++ Lisp_Object buf_obj = ns_ax_window_buffer_object (w); ++ if (NILP (buf_obj)) ++ return @[]; ++ ++ struct buffer *b = XBUFFER (buf_obj); ++ ptrdiff_t vis_start = marker_position (w->start); ++ ptrdiff_t vis_end = ns_ax_window_end_charpos (w, b); ++ ++ if (vis_start < BUF_BEGV (b)) vis_start = BUF_BEGV (b); ++ if (vis_end > BUF_ZV (b)) vis_end = BUF_ZV (b); ++ if (vis_start >= vis_end) ++ return @[]; ++ ++ /* Cache interned symbols (intern is idempotent but avoids repeated ++ obarray lookup on every iteration). */ ++ static Lisp_Object Qwidget_sym, Qbutton_sym, Qfollow_link_sym; ++ static Lisp_Object Qorg_link_sym, Qmouse_face_sym, Qkeymap_sym; ++ static Lisp_Object Qcomp_list_mode_sym; ++ static BOOL syms_initialized = NO; ++ if (!syms_initialized) ++ { ++ Qwidget_sym = intern ("widget"); ++ Qbutton_sym = intern ("button"); ++ Qfollow_link_sym = intern ("follow-link"); ++ Qorg_link_sym = intern ("org-link"); ++ Qmouse_face_sym = intern ("mouse-face"); ++ Qkeymap_sym = intern ("keymap"); ++ Qcomp_list_mode_sym = intern ("completion-list-mode"); ++ syms_initialized = YES; ++ } ++ ++ BOOL is_completion_buf = EQ (BVAR (b, major_mode), Qcomp_list_mode_sym); ++ ++ NSMutableArray *spans = [NSMutableArray array]; ++ ptrdiff_t pos = vis_start; ++ ++ while (pos < vis_end) ++ { ++ Lisp_Object plist = Ftext_properties_at (make_fixnum (pos), buf_obj); ++ EmacsAXSpanType span_type = (EmacsAXSpanType) -1; ++ Lisp_Object limit_prop = Qnil; ++ ++ if (!NILP (Fplist_get (plist, Qwidget_sym))) ++ { ++ span_type = EmacsAXSpanTypeWidget; ++ limit_prop = Qwidget_sym; ++ } ++ else if (!NILP (Fplist_get (plist, Qbutton_sym))) ++ { ++ span_type = EmacsAXSpanTypeButton; ++ limit_prop = Qbutton_sym; ++ } ++ else if (!NILP (Fplist_get (plist, Qfollow_link_sym))) ++ { ++ span_type = EmacsAXSpanTypeLink; ++ limit_prop = Qfollow_link_sym; ++ } ++ else if (!NILP (Fplist_get (plist, Qorg_link_sym))) ++ { ++ span_type = EmacsAXSpanTypeLink; ++ limit_prop = Qorg_link_sym; ++ } ++ else if (is_completion_buf ++ && !NILP (Fplist_get (plist, Qmouse_face_sym))) ++ { ++ span_type = EmacsAXSpanTypeCompletionItem; ++ limit_prop = Qmouse_face_sym; ++ } ++ else ++ { ++ /* Check overlays for keymap. */ ++ Lisp_Object ovs ++ = Foverlays_in (make_fixnum (pos), make_fixnum (pos + 1)); ++ while (CONSP (ovs)) ++ { ++ if (!NILP (Foverlay_get (XCAR (ovs), Qkeymap_sym))) ++ { ++ span_type = EmacsAXSpanTypeButton; ++ limit_prop = Qkeymap_sym; ++ break; ++ } ++ ovs = XCDR (ovs); ++ } ++ } ++ ++ if ((NSInteger) span_type == -1) ++ { ++ pos++; ++ continue; ++ } ++ ++ ptrdiff_t span_end = !NILP (limit_prop) ++ ? ns_ax_next_prop_change (pos, limit_prop, buf_obj, vis_end) ++ : pos + 1; ++ ++ if (span_end > vis_end) span_end = vis_end; ++ if (span_end <= pos) span_end = pos + 1; ++ ++ EmacsAccessibilityInteractiveSpan *span ++ = [[EmacsAccessibilityInteractiveSpan alloc] init]; ++ span.charposStart = pos; ++ span.charposEnd = span_end; ++ span.spanType = span_type; ++ span.parentBuffer = parent_buf; ++ span.emacsView = parent_buf.emacsView; ++ span.lispWindow = parent_buf.lispWindow; ++ span.spanLabel = ns_ax_get_span_label (pos, span_end, buf_obj); ++ ++ [spans addObject: span]; ++ [span release]; ++ ++ pos = span_end; ++ } ++ ++ return [spans copy]; ++} ++ ++@implementation EmacsAccessibilityInteractiveSpan ++ ++- (BOOL) isAccessibilityElement { return YES; } ++ ++- (NSAccessibilityRole) accessibilityRole ++{ ++ switch (self.spanType) ++ { ++ case EmacsAXSpanTypeLink: return NSAccessibilityLinkRole; ++ case EmacsAXSpanTypeCheckBox: return NSAccessibilityCheckBoxRole; ++ case EmacsAXSpanTypeTextField: return NSAccessibilityTextFieldRole; ++ case EmacsAXSpanTypePopUpButton: return NSAccessibilityPopUpButtonRole; ++ default: return NSAccessibilityButtonRole; ++ } ++} ++ ++- (NSString *) accessibilityLabel { return self.spanLabel ?: @""; } ++- (NSString *) accessibilityValue { return self.spanValue; } ++ ++- (NSRect) accessibilityFrame ++{ ++ EmacsAccessibilityBuffer *pb = self.parentBuffer; ++ if (!pb || ![self validWindow]) ++ return NSZeroRect; ++ NSUInteger ax_s = [pb accessibilityIndexForCharpos: self.charposStart]; ++ NSUInteger ax_e = [pb accessibilityIndexForCharpos: self.charposEnd]; ++ if (ax_e < ax_s) ax_e = ax_s; ++ return [pb accessibilityFrameForRange: NSMakeRange (ax_s, ax_e - ax_s)]; ++} ++ ++- (BOOL) isAccessibilityFocused ++{ ++ struct window *w = [self validWindow]; ++ if (!w) ++ return NO; ++ ptrdiff_t pt = marker_position (w->pointm); ++ return pt >= self.charposStart && pt < self.charposEnd; ++} ++ ++- (void) setAccessibilityFocused: (BOOL) focused ++{ ++ if (!focused) ++ return; ++ ptrdiff_t target = self.charposStart; ++ Lisp_Object lwin = self.lispWindow; ++ dispatch_async (dispatch_get_main_queue (), ^{ ++ if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin))) ++ return; ++ block_input (); ++ Fselect_window (lwin, Qnil); ++ struct window *w = XWINDOW (lwin); ++ struct buffer *b = XBUFFER (w->contents); ++ struct buffer *oldb = current_buffer; ++ if (b != current_buffer) ++ set_buffer_internal_1 (b); ++ ptrdiff_t pos = target; ++ if (pos < BUF_BEGV (b)) pos = BUF_BEGV (b); ++ if (pos > BUF_ZV (b)) pos = BUF_ZV (b); ++ SET_PT_BOTH (pos, CHAR_TO_BYTE (pos)); ++ if (b != oldb) ++ set_buffer_internal_1 (oldb); ++ unblock_input (); ++ }); ++} ++ ++@end ++ ++/* EmacsAccessibilityBuffer — InteractiveSpans category. ++ Methods are kept here (same .m file) so they access the ivars ++ declared in the @interface ivar block. */ ++@implementation EmacsAccessibilityBuffer (InteractiveSpans) ++ ++- (void) invalidateInteractiveSpans ++{ ++ interactiveSpansDirty = YES; ++} ++ ++- (NSArray *) accessibilityChildrenInNavigationOrder ++{ ++ if (!interactiveSpansDirty && cachedInteractiveSpans != nil) ++ return cachedInteractiveSpans; ++ ++ if (![NSThread isMainThread]) ++ { ++ __block NSArray *result; ++ dispatch_sync (dispatch_get_main_queue (), ^{ ++ result = [self accessibilityChildrenInNavigationOrder]; ++ }); ++ return result; ++ } ++ ++ struct window *w = [self validWindow]; ++ NSArray *spans = ns_ax_scan_interactive_spans (w, self); ++ ++ if (!cachedInteractiveSpans) ++ cachedInteractiveSpans = [[NSMutableArray alloc] init]; ++ [cachedInteractiveSpans setArray: spans]; ++ interactiveSpansDirty = NO; ++ ++ return cachedInteractiveSpans; ++} ++ ++- (NSArray *) accessibilityChildren ++{ ++ return [self accessibilityChildrenInNavigationOrder]; ++} ++ ++@end ++ ++ +static NSString * +ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, + struct buffer *b, @@ -690,9 +1105,7 @@ index 932d209..0fa6b2d 100644 +{ + if (!elem || !b || !cachedText || end <= start) + return nil; - -- [fm setSelectedFont: nsfont isMultiple: NO]; -- [fm orderFrontFontPanel: NSApp]; ++ + NSString *text = nil; + struct buffer *oldb = current_buffer; + if (b != current_buffer) @@ -720,9 +1133,7 @@ index 932d209..0fa6b2d 100644 + text = [NSString stringWithLispString:cstr]; + } + } - -- font_panel_active = YES; -- timeout = make_timespec (0, 100000000); ++ + if (!text) + { + NSUInteger ax_s = [elem accessibilityIndexForCharpos:start]; @@ -730,22 +1141,10 @@ index 932d209..0fa6b2d 100644 + if (ax_e > ax_s && ax_e <= [cachedText length]) + text = [cachedText substringWithRange:NSMakeRange (ax_s, ax_e - ax_s)]; + } - -- 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 (); ++ + if (b != oldb) + set_buffer_internal_1 (oldb); - -- if (font_panel_result) -- [font_panel_result autorelease]; ++ + if (text) + { + text = [text stringByTrimmingCharactersInSet: @@ -753,24 +1152,13 @@ index 932d209..0fa6b2d 100644 + if ([text length] == 0) + text = nil; + } - --#ifdef NS_IMPL_COCOA -- if (!canceled) -- font_panel_result = nil; --#endif ++ + return text; +} - -- result = font_panel_result; -- font_panel_result = nil; - -- [[fm fontPanel: YES] setIsVisible: NO]; -- font_panel_active = NO; ++ ++ +@implementation EmacsAccessibilityElement - -- if (result) -- return ns_font_desc_to_font_spec ([result fontDescriptor], -- result); ++ +- (instancetype)init +{ + self = [super init]; @@ -778,8 +1166,7 @@ index 932d209..0fa6b2d 100644 + self.lispWindow = Qnil; + return self; +} - -- return Qnil; ++ +/* Return the associated Emacs window if it is still live, else NULL. + Use this instead of storing a raw struct window * which can become a + dangling pointer after delete-window or kill-buffer. */ @@ -788,9 +1175,8 @@ index 932d209..0fa6b2d 100644 + if (NILP (self.lispWindow) || !WINDOW_LIVE_P (self.lispWindow)) + return NULL; + return XWINDOW (self.lispWindow); - } - --- (BOOL)acceptsFirstResponder ++} ++ +- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)ew height:(int)eh +{ + EmacsView *view = self.emacsView; @@ -803,29 +1189,19 @@ index 932d209..0fa6b2d 100644 +} + +- (BOOL)isAccessibilityElement - { -- NSTRACE ("[EmacsView acceptsFirstResponder]"); - return YES; - } - --/* Tell NS we want to accept clicks that activate the window */ --- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent ++{ ++ return YES; ++} ++ +/* ---- Hierarchy plumbing (required for VoiceOver to find us) ---- */ + +- (id)accessibilityParent - { -- NSTRACE_MSG ("First mouse event: type=%ld, clickCount=%ld", -- [theEvent type], [theEvent clickCount]); -- return ns_click_through; ++{ + return NSAccessibilityUnignoredAncestor (self.emacsView); - } --- (void)resetCursorRects ++} + +- (id)accessibilityWindow - { -- NSRect visible = [self visibleRect]; -- NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe); -- NSTRACE ("[EmacsView resetCursorRects]"); ++{ + return [self.emacsView window]; +} + @@ -853,6 +1229,7 @@ index 932d209..0fa6b2d 100644 +{ + [cachedText release]; + [cachedCompletionAnnouncement release]; ++ [cachedInteractiveSpans release]; + if (visibleRuns) + xfree (visibleRuns); + [super dealloc]; @@ -870,6 +1247,7 @@ index 932d209..0fa6b2d 100644 + visibleRuns = NULL; + } + visibleRunCount = 0; ++ [self invalidateInteractiveSpans]; +} + +- (void)ensureTextCache @@ -989,9 +1367,23 @@ index 932d209..0fa6b2d 100644 + +- (NSAccessibilityRole)accessibilityRole +{ ++ struct window *w = [self validWindow]; ++ if (w && MINI_WINDOW_P (w)) ++ return NSAccessibilityTextFieldRole; + return NSAccessibilityTextAreaRole; +} + ++- (NSString *)accessibilityPlaceholderValue ++{ ++ struct window *w = [self validWindow]; ++ if (!w || !MINI_WINDOW_P (w)) ++ return nil; ++ Lisp_Object prompt = Fminibuffer_prompt (); ++ if (STRINGP (prompt)) ++ return [NSString stringWithLispString: prompt]; ++ return nil; ++} ++ +- (NSString *)accessibilityRoleDescription +{ + struct window *w = [self validWindow]; @@ -2141,10 +2533,36 @@ index 932d209..0fa6b2d 100644 + NSRect visible = [self visibleRect]; + NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe); + NSTRACE ("[EmacsView resetCursorRects]"); ++ ++ if (currentCursor == nil) ++ currentCursor = [NSCursor arrowCursor]; ++ ++ if (!NSIsEmptyRect (visible)) ++ [self addCursorRect: visible cursor: currentCursor]; ++ ++#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 ++} ++ ++ ++ ++/*****************************************************************************/ ++/* 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]; - if (currentCursor == nil) - currentCursor = [NSCursor arrowCursor]; -@@ -8237,6 +9929,28 @@ - (void)windowDidBecomeKey /* for direct calls */ +@@ -8237,6 +10259,28 @@ - (void)windowDidBecomeKey /* for direct calls */ XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -2173,7 +2591,7 @@ index 932d209..0fa6b2d 100644 } -@@ -9474,6 +11188,304 @@ - (int) fullscreenState +@@ -9474,6 +11518,308 @@ - (int) fullscreenState return fs_state; } @@ -2354,6 +2772,10 @@ index 932d209..0fa6b2d 100644 + if (!accessibilityTreeValid) + { + [self rebuildAccessibilityTree]; ++ /* Invalidate span cache — window layout changed. */ ++ for (EmacsAccessibilityElement *elem in accessibilityElements) ++ if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]]) ++ [(EmacsAccessibilityBuffer *) elem invalidateInteractiveSpans]; + NSAccessibilityPostNotification (self, + NSAccessibilityLayoutChangedNotification); +