From 6a256eb9269e0cfc4d1270ef61c228dd9f9989da Mon Sep 17 00:00:00 2001 From: Daneel Date: Thu, 26 Feb 2026 18:41:28 +0100 Subject: [PATCH] ns: implement AXBoundsForRange and VoiceOver interaction fixes --- nsterm.h | 71 ++ nsterm.m | 2201 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 2133 insertions(+), 139 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h index 7c1ee4c..22828f2 100644 --- a/src/nsterm.h +++ b/src/nsterm.h @@ -453,6 +453,62 @@ enum ns_return_frame_mode @end +/* ========================================================================== + + Accessibility virtual elements (macOS / Cocoa only) + + ========================================================================== */ + +#ifdef NS_IMPL_COCOA +@class EmacsView; + +/* Base class for virtual accessibility elements attached to EmacsView. */ +@interface EmacsAccessibilityElement : NSAccessibilityElement +@property (nonatomic, unsafe_unretained) EmacsView *emacsView; +@property (nonatomic, assign) struct window *emacsWindow; +- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)w height:(int)h; +@end + +/* A visible run: maps a contiguous range of accessibility indices + to a contiguous range of buffer character positions. Invisible + text is skipped, so ax_start values are consecutive across runs + while charpos values may have gaps. */ +typedef struct ns_ax_visible_run +{ + ptrdiff_t charpos; /* Buffer charpos where this visible run starts. */ + ptrdiff_t length; /* Number of visible Emacs characters in this run. */ + NSUInteger ax_start; /* Starting index in the accessibility string. */ + NSUInteger ax_length; /* Length in accessibility string (UTF-16 units). */ +} ns_ax_visible_run; + +/* Virtual AXTextArea element — one per visible Emacs window (buffer). */ +@interface EmacsAccessibilityBuffer : EmacsAccessibilityElement +{ + ns_ax_visible_run *visibleRuns; + NSUInteger visibleRunCount; +} +@property (nonatomic, retain) NSString *cachedText; +@property (nonatomic, assign) ptrdiff_t cachedTextModiff; +@property (nonatomic, assign) ptrdiff_t cachedTextStart; +@property (nonatomic, assign) ptrdiff_t cachedModiff; +@property (nonatomic, assign) ptrdiff_t cachedPoint; +@property (nonatomic, assign) BOOL cachedMarkActive; +@property (nonatomic, copy) NSString *cachedCompletionAnnouncement; +@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayStart; +@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayEnd; +@property (nonatomic, assign) ptrdiff_t cachedCompletionPoint; +- (void)invalidateTextCache; +- (void)postAccessibilityNotificationsForFrame:(struct frame *)f; +- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx; +- (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos; +@end + +/* Virtual AXStaticText element — one per mode line. */ +@interface EmacsAccessibilityModeLine : EmacsAccessibilityElement +@end +#endif /* NS_IMPL_COCOA */ + + /* ========================================================================== The main Emacs view @@ -471,6 +527,14 @@ enum ns_return_frame_mode #ifdef NS_IMPL_COCOA char *old_title; BOOL maximizing_resize; + NSMutableArray *accessibilityElements; + Lisp_Object lastSelectedWindow; + Lisp_Object lastRootWindow; + BOOL accessibilityTreeValid; + BOOL accessibilityUpdating; + @public + NSRect lastAccessibilityCursorRect; + @protected #endif BOOL font_panel_active; NSFont *font_panel_result; @@ -528,6 +592,13 @@ enum ns_return_frame_mode - (void)windowWillExitFullScreen; - (void)windowDidExitFullScreen; - (void)windowDidBecomeKey; + +#ifdef NS_IMPL_COCOA +/* Accessibility support. */ +- (void)rebuildAccessibilityTree; +- (void)invalidateAccessibilityTree; +- (void)postAccessibilityUpdates; +#endif @end diff --git a/src/nsterm.m b/src/nsterm.m index 932d209..da40369 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -1104,6 +1104,11 @@ ns_update_end (struct frame *f) unblock_input (); ns_updating_frame = NULL; + +#ifdef NS_IMPL_COCOA + /* Post accessibility notifications after each redisplay cycle. */ + [view postAccessibilityUpdates]; +#endif } static void @@ -3232,6 +3237,37 @@ ns_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, /* Prevent the cursor from being drawn outside the text area. */ r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA)); +#ifdef NS_IMPL_COCOA + /* Accessibility: store cursor rect for Zoom and bounds queries. + VoiceOver notifications are handled solely by + postAccessibilityUpdates (called from ns_update_end) + to avoid duplicate notifications and mid-redisplay fragility. */ + { + EmacsView *view = FRAME_NS_VIEW (f); + if (view && on_p && active_p) + { + view->lastAccessibilityCursorRect = r; + + /* Tell macOS Zoom where the cursor is. UAZoomChangeFocus() + expects top-left origin (CG coordinate space). */ + if (UAZoomEnabled ()) + { + NSRect windowRect = [view convertRect:r toView:nil]; + NSRect screenRect = [[view window] convertRectToScreen:windowRect]; + CGRect cgRect = NSRectToCGRect (screenRect); + + CGFloat primaryH + = [[[NSScreen screens] firstObject] frame].size.height; + cgRect.origin.y + = primaryH - cgRect.origin.y - cgRect.size.height; + + UAZoomChangeFocus (&cgRect, &cgRect, + kUAZoomFocusTypeInsertionPoint); + } + } + } +#endif + ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; @@ -6849,214 +6885,1779 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) /* ========================================================================== - EmacsView implementation + Accessibility virtual elements (macOS / Cocoa only) ========================================================================== */ +#ifdef NS_IMPL_COCOA -@implementation EmacsView +/* ---- Helper: extract buffer text for accessibility ---- */ -- (void)windowDidEndLiveResize:(NSNotification *)notification +/* Maximum characters exposed via accessibilityValue. */ +#define NS_AX_TEXT_CAP 100000 + +/* 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(). */ + +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 @""; + } + + struct buffer *b = XBUFFER (w->contents); + if (!b) + { + *out_start = 0; + return @""; + } + + ptrdiff_t begv = BUF_BEGV (b); + ptrdiff_t zv = BUF_ZV (b); + + *out_start = begv; + + if (zv <= begv) + return @""; + + 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; + + NSMutableString *result = [NSMutableString string]; + ptrdiff_t pos = begv; + + while (pos < zv) + { + /* Check invisible property (text properties + overlays). */ + Lisp_Object invis = Fget_char_property (make_fixnum (pos), + Qinvisible, Qnil); + /* Check if invisible property means truly invisible. + TEXT_PROP_MEANS_INVISIBLE is defined only in xdisp.c, + so we replicate: EQ(invis, Qt), or invis is on the + buffer's invisibility-spec list. Simplified: any + non-nil invisible property hides the text. This matches + the common case (invisible t) and org-mode/dired usage. */ + if (!NILP (invis)) + { + /* Skip to the next position where invisible changes. */ + Lisp_Object next = Fnext_single_char_property_change ( + make_fixnum (pos), Qinvisible, Qnil, make_fixnum (zv)); + pos = FIXNUMP (next) ? XFIXNUM (next) : zv; + continue; + } + + /* 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; + + /* 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) + run_len = (ptrdiff_t) (NS_AX_TEXT_CAP - ax_offset); + if (run_len <= 0) + break; + run_end = pos + run_len; + + /* Extract this visible run's text. Use + Fbuffer_substring_no_properties which correctly handles the + buffer gap — raw BUF_BYTE_ADDRESS reads across the gap would + include garbage bytes when the run spans the gap position. */ + Lisp_Object lstr = Fbuffer_substring_no_properties ( + make_fixnum (pos), make_fixnum (run_end)); + NSString *nsstr = [NSString stringWithLispString:lstr]; + NSUInteger ns_len = [nsstr length]; + [result appendString:nsstr]; + + /* Record this visible run in the mapping. */ + if (nruns >= run_capacity) + { + run_capacity *= 2; + runs = xrealloc (runs, run_capacity + * sizeof (ns_ax_visible_run)); + } + runs[nruns].charpos = pos; + runs[nruns].length = run_len; + runs[nruns].ax_start = ax_offset; + runs[nruns].ax_length = ns_len; + nruns++; + + ax_offset += ns_len; + pos = run_end; + } + + if (b != oldb) + set_buffer_internal_1 (oldb); + + *out_runs = runs; + *out_nruns = nruns; + return result; } -/* Needed to inform when window closed from lisp. */ -- (void) setWindowClosing: (BOOL)closing + +/* ---- Helper: extract mode line text from glyph rows ---- */ + +static NSString * +ns_ax_mode_line_text (struct window *w) { - NSTRACE ("[EmacsView setWindowClosing:%d]", closing); + if (!w || !w->current_matrix) + return @""; - windowClosing = closing; + struct glyph_matrix *matrix = w->current_matrix; + NSMutableString *text = [NSMutableString string]; + + for (int i = 0; i < matrix->nrows; i++) + { + struct glyph_row *row = matrix->rows + i; + if (!row->enabled_p || !row->mode_line_p) + continue; + + struct glyph *g = row->glyphs[TEXT_AREA]; + struct glyph *end = g + row->used[TEXT_AREA]; + for (; g < end; g++) + { + if (g->type == CHAR_GLYPH && g->u.ch >= 32) + { + unichar uch = (unichar) g->u.ch; + [text appendString:[NSString stringWithCharacters:&uch + length:1]]; + } + } + } + return text; } -- (void)dealloc +/* ---- 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]"); + if (!w || !w->current_matrix || !view) + return NSZeroRect; - /* Clear the view resize notification. */ - [[NSNotificationCenter defaultCenter] - removeObserver:self - name:NSViewFrameDidChangeNotification - object: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]; + 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]; -#endif + for (int i = 0; i < matrix->nrows; i++) + { + 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; - [[self menu] release]; - [super dealloc]; + ptrdiff_t row_start = MATRIX_ROW_START_CHARPOS (row); + ptrdiff_t row_end = MATRIX_ROW_END_CHARPOS (row); + + if (row_start < cp_end && row_end > cp_start) + { + int window_x, window_y, window_width; + window_box (w, TEXT_AREA, &window_x, &window_y, + &window_width, 0); + + NSRect rowRect; + rowRect.origin.x = window_x; + rowRect.origin.y = WINDOW_TO_FRAME_PIXEL_Y (w, MAX (0, row->y)); + rowRect.origin.y = MAX (rowRect.origin.y, window_y); + rowRect.size.width = window_width; + rowRect.size.height = row->height; + + if (!found) + { + result = rowRect; + found = YES; + } + else + result = NSUnionRect (result, rowRect); + } + } + + if (!found) + return NSZeroRect; + + /* Clip result to text area bounds. */ + { + int text_area_x, text_area_y, text_area_w, text_area_h; + window_box (w, TEXT_AREA, &text_area_x, &text_area_y, + &text_area_w, &text_area_h); + CGFloat max_y = WINDOW_TO_FRAME_PIXEL_Y (w, text_area_y + text_area_h); + if (NSMaxY (result) > max_y) + result.size.height = max_y - result.origin.y; + } + + /* Convert from EmacsView (flipped) coords to screen coords. */ + NSRect winRect = [view convertRect:result toView:nil]; + return [[view window] convertRectToScreen:winRect]; } +/* AX enum numeric compatibility for NSAccessibility notifications. + Values match WebKit AXObjectCacheMac fallback enums + (AXTextStateChangeType / AXTextEditType / AXTextSelectionDirection / + AXTextSelectionGranularity). */ +enum { + ns_ax_text_state_change_unknown = 0, + 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; + 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 + 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; + 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]; +static NSUInteger +ns_ax_utf16_length_for_buffer_range (struct buffer *b, ptrdiff_t start, + ptrdiff_t end) +{ + if (!b || end <= start) + return 0; - font_panel_result = (NSFont *) [sender convertFont: nsfont]; + 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 +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; + 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); + ptrdiff_t best_start = 0; + ptrdiff_t best_end = 0; + ptrdiff_t best_dist = PTRDIFF_MAX; + BOOL found = NO; - if (!font_panel_result && FRAME_FONT (emacsframe)) + /* Fast path: look at point and immediate neighbors first. */ + ptrdiff_t probes[3] = { point, point - 1, point + 1 }; + for (int i = 0; i < 3 && !found; i++) { - font_panel_result - = macfont_get_nsctfont (FRAME_FONT (emacsframe)); + ptrdiff_t p = probes[i]; + if (p < begv || p > zv) + continue; - if (font_panel_result) - [font_panel_result retain]; + Lisp_Object overlays = Foverlays_at (make_fixnum (p), Qnil); + Lisp_Object tail; + for (tail = overlays; CONSP (tail); tail = XCDR (tail)) + { + Lisp_Object ov = XCAR (tail); + Lisp_Object face = Foverlay_get (ov, Qface); + if (!(EQ (face, faceSym) + || (CONSP (face) && !NILP (Fmemq (faceSym, face))))) + continue; + + ptrdiff_t ov_start = OVERLAY_START (ov); + ptrdiff_t ov_end = OVERLAY_END (ov); + if (ov_end <= ov_start) + continue; + + best_start = ov_start; + best_end = ov_end; + best_dist = 0; + found = YES; + break; + } } - [NSApp stop: self]; + if (!found) + { + for (ptrdiff_t scan = begv; scan < zv; scan++) + { + Lisp_Object overlays = Foverlays_at (make_fixnum (scan), Qnil); + Lisp_Object tail; + for (tail = overlays; CONSP (tail); tail = XCDR (tail)) + { + Lisp_Object ov = XCAR (tail); + Lisp_Object face = Foverlay_get (ov, Qface); + if (!(EQ (face, faceSym) + || (CONSP (face) && !NILP (Fmemq (faceSym, face))))) + continue; + + ptrdiff_t ov_start = OVERLAY_START (ov); + ptrdiff_t ov_end = OVERLAY_END (ov); + if (ov_end <= ov_start) + continue; + + ptrdiff_t dist = 0; + if (point < ov_start) + dist = ov_start - point; + else if (point > ov_end) + dist = point - ov_end; + + if (!found || dist < best_dist + || (dist == best_dist + && (ov_start < point && best_start >= point))) + { + best_start = ov_start; + best_end = ov_end; + best_dist = dist; + found = YES; + } + } + } + } + + if (!found) + return NO; + + *out_start = best_start; + *out_end = best_end; + return YES; } -- (void) noteUserCancelledSelection +static bool +ns_ax_event_is_ctrl_n_or_p (int *which) { - font_panel_active = NO; + 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)) + return false; - [NSApp stop: self]; + EMACS_INT c = XFIXNUM (ev); + if (c == ('n' & 0x1f)) + { + if (which) + *which = 1; + return true; + } + if (c == ('p' & 0x1f)) + { + if (which) + *which = -1; + return true; + } + return false; } -#endif -- (Lisp_Object) showFontPanel +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 + if (!SYMBOLP (real_this_command)) + return false; -#ifdef NS_IMPL_GNUSTEP - nsfont = ((struct nsfont_info *) font)->nsfont; -#else - nsfont = (NSFont *) macfont_get_nsctfont (font); -#endif + Lisp_Object next = intern ("next-line"); + Lisp_Object prev = intern ("previous-line"); + return EQ (real_this_command, next) || EQ (real_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]; -#endif +static NSString * +ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, + struct buffer *b, + ptrdiff_t start, + ptrdiff_t end, + NSString *cachedText) +{ + 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) + set_buffer_internal_1 (b); - font_panel_active = YES; - timeout = make_timespec (0, 100000000); + /* Prefer canonical completion candidate string from text property. */ + ptrdiff_t probes[2] = { start, end - 1 }; + for (int i = 0; i < 2 && !text; i++) + { + ptrdiff_t p = probes[i]; + Lisp_Object cstr = Fget_char_property (make_fixnum (p), + intern ("completion--string"), + Qnil); + if (STRINGP (cstr)) + text = [NSString stringWithLispString:cstr]; + } - 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 (!text) + { + NSUInteger ax_s = [elem accessibilityIndexForCharpos:start]; + NSUInteger ax_e = [elem accessibilityIndexForCharpos:end]; + if (ax_e > ax_s && ax_e <= [cachedText length]) + text = [cachedText substringWithRange:NSMakeRange (ax_s, ax_e - ax_s)]; + } - if (font_panel_result) - [font_panel_result autorelease]; + if (b != oldb) + set_buffer_internal_1 (oldb); -#ifdef NS_IMPL_COCOA - if (!canceled) - font_panel_result = nil; -#endif + if (text) + { + text = [text stringByTrimmingCharactersInSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if ([text length] == 0) + text = nil; + } - result = font_panel_result; - font_panel_result = nil; + return text; +} - [[fm fontPanel: YES] setIsVisible: NO]; - font_panel_active = NO; - if (result) - return ns_font_desc_to_font_spec ([result fontDescriptor], - result); +@implementation EmacsAccessibilityElement - return Qnil; +- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)ew height:(int)eh +{ + EmacsView *view = self.emacsView; + if (!view || ![view window]) + return NSZeroRect; + + NSRect r = NSMakeRect (x, y, ew, eh); + NSRect winRect = [view convertRect:r toView:nil]; + return [[view window] convertRectToScreen:winRect]; } -- (BOOL)acceptsFirstResponder +- (BOOL)isAccessibilityElement { - NSTRACE ("[EmacsView acceptsFirstResponder]"); return YES; } -/* Tell NS we want to accept clicks that activate the window */ -- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent +/* ---- 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 -{ - 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]; +- (id)accessibilityWindow +{ + return [self.emacsView window]; +} -#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 +- (id)accessibilityTopLevelUIElement +{ + return [self.emacsView window]; } +@end -/*****************************************************************************/ -/* Keyboard handling. */ -#define NS_KEYLOG 0 +@implementation EmacsAccessibilityBuffer +@synthesize cachedText; +@synthesize cachedTextModiff; +@synthesize cachedTextStart; +@synthesize cachedModiff; +@synthesize cachedPoint; +@synthesize cachedMarkActive; +@synthesize cachedCompletionAnnouncement; +@synthesize cachedCompletionOverlayStart; +@synthesize cachedCompletionOverlayEnd; +@synthesize cachedCompletionPoint; + +- (void)dealloc +{ + [cachedText release]; + [cachedCompletionAnnouncement release]; + if (visibleRuns) + xfree (visibleRuns); + [super dealloc]; +} + +/* ---- Text cache ---- */ + +- (void)invalidateTextCache +{ + [cachedText release]; + cachedText = nil; + if (visibleRuns) + { + xfree (visibleRuns); + visibleRuns = NULL; + } + visibleRunCount = 0; +} + +- (void)ensureTextCache +{ + struct window *w = self.emacsWindow; + if (!w || !WINDOW_LEAF_P (w)) + return; + + struct buffer *b = XBUFFER (w->contents); + if (!b) + return; + + ptrdiff_t modiff = BUF_MODIFF (b); + ptrdiff_t pt = BUF_PT (b); + NSUInteger textLen = cachedText ? [cachedText length] : 0; + if (cachedText && cachedTextModiff == modiff + && pt >= cachedTextStart + && (textLen == 0 + || [self accessibilityIndexForCharpos:pt] <= textLen)) + return; + + ptrdiff_t start; + ns_ax_visible_run *runs = NULL; + NSUInteger nruns = 0; + NSString *text = ns_ax_buffer_text (w, &start, &runs, &nruns); + + [cachedText release]; + cachedText = [text retain]; + cachedTextModiff = modiff; + cachedTextStart = start; + + if (visibleRuns) + xfree (visibleRuns); + visibleRuns = runs; + visibleRunCount = nruns; +} + +/* ---- Index mapping ---- */ + +/* Convert buffer charpos to accessibility string index. */ +- (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos +{ + struct window *w = self.emacsWindow; + struct buffer *b = (w && WINDOW_LEAF_P (w)) ? XBUFFER (w->contents) : NULL; + + for (NSUInteger i = 0; i < visibleRunCount; i++) + { + ns_ax_visible_run *r = &visibleRuns[i]; + if (charpos >= r->charpos && charpos < r->charpos + r->length) + { + if (!b) + return r->ax_start; + NSUInteger delta = ns_ax_utf16_length_for_buffer_range (b, r->charpos, + charpos); + if (delta > r->ax_length) + delta = r->ax_length; + return r->ax_start + delta; + } + /* If charpos falls in an invisible gap before the next run, + map it to the start of the next visible run. */ + if (charpos < r->charpos) + return r->ax_start; + } + /* Past end — return total length. */ + if (visibleRunCount > 0) + { + ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; + return last->ax_start + last->ax_length; + } + return 0; +} + +/* Convert accessibility string index to buffer charpos. */ +- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx +{ + struct window *w = self.emacsWindow; + struct buffer *b = (w && WINDOW_LEAF_P (w)) ? XBUFFER (w->contents) : NULL; + + for (NSUInteger i = 0; i < visibleRunCount; i++) + { + ns_ax_visible_run *r = &visibleRuns[i]; + if (ax_idx >= r->ax_start + && ax_idx < r->ax_start + r->ax_length) + { + if (!b) + return r->charpos; + + NSUInteger target = ax_idx - r->ax_start; + ptrdiff_t lo = r->charpos; + ptrdiff_t hi = r->charpos + r->length; + + while (lo < hi) + { + ptrdiff_t mid = lo + (hi - lo) / 2; + NSUInteger mid_len = ns_ax_utf16_length_for_buffer_range (b, + r->charpos, + mid); + if (mid_len < target) + lo = mid + 1; + else + hi = mid; + } + + return lo; + } + } + /* Past end — return last charpos. */ + if (visibleRunCount > 0) + { + ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; + return last->charpos + last->length; + } + return cachedTextStart; +} + +/* ---- NSAccessibility protocol ---- */ + +- (NSAccessibilityRole)accessibilityRole +{ + return NSAccessibilityTextAreaRole; +} + +- (NSString *)accessibilityRoleDescription +{ + struct window *w = self.emacsWindow; + if (w && MINI_WINDOW_P (w)) + return @"minibuffer"; + return @"editor"; +} + +- (NSString *)accessibilityLabel +{ + struct window *w = self.emacsWindow; + if (w && WINDOW_LEAF_P (w)) + { + if (MINI_WINDOW_P (w)) + return @"Minibuffer"; + + struct buffer *b = XBUFFER (w->contents); + if (b) + { + Lisp_Object name = BVAR (b, name); + if (STRINGP (name)) + return [NSString stringWithLispString:name]; + } + } + return @"buffer"; +} + +- (BOOL)isAccessibilityFocused +{ + struct window *w = self.emacsWindow; + if (!w) + return NO; + EmacsView *view = self.emacsView; + if (!view || !view->emacsframe) + return NO; + struct frame *f = view->emacsframe; + return (w == XWINDOW (f->selected_window)); +} + +- (id)accessibilityValue +{ + [self ensureTextCache]; + return cachedText ? cachedText : @""; +} + +- (NSInteger)accessibilityNumberOfCharacters +{ + [self ensureTextCache]; + return cachedText ? [cachedText length] : 0; +} + +- (NSString *)accessibilitySelectedText +{ + struct window *w = self.emacsWindow; + if (!w || !WINDOW_LEAF_P (w)) + return @""; + + struct buffer *b = XBUFFER (w->contents); + if (!b || NILP (BVAR (b, mark_active))) + return @""; + + NSRange sel = [self accessibilitySelectedTextRange]; + [self ensureTextCache]; + if (!cachedText || sel.location == NSNotFound + || sel.location + sel.length > [cachedText length]) + return @""; + return [cachedText substringWithRange:sel]; +} + +- (NSRange)accessibilitySelectedTextRange +{ + struct window *w = self.emacsWindow; + if (!w || !WINDOW_LEAF_P (w)) + return NSMakeRange (0, 0); + + struct buffer *b = XBUFFER (w->contents); + if (!b) + return NSMakeRange (0, 0); + + [self ensureTextCache]; + ptrdiff_t pt = BUF_PT (b); + NSUInteger point_idx = [self accessibilityIndexForCharpos:pt]; + + if (NILP (BVAR (b, mark_active))) + return NSMakeRange (point_idx, 0); + + ptrdiff_t mark_pos = marker_position (BVAR (b, mark)); + NSUInteger mark_idx = [self accessibilityIndexForCharpos:mark_pos]; + NSUInteger start_idx = MIN (point_idx, mark_idx); + NSUInteger end_idx = MAX (point_idx, mark_idx); + return NSMakeRange (start_idx, end_idx - start_idx); +} + +- (void)setAccessibilitySelectedTextRange:(NSRange)range +{ + struct window *w = self.emacsWindow; + if (!w || !WINDOW_LEAF_P (w)) + return; + + struct buffer *b = XBUFFER (w->contents); + if (!b) + return; + + [self ensureTextCache]; + + /* Convert accessibility index to buffer charpos via mapping. */ + ptrdiff_t charpos = [self charposForAccessibilityIndex:range.location]; + + /* Clamp to buffer bounds. */ + if (charpos < BUF_BEGV (b)) + charpos = BUF_BEGV (b); + if (charpos > BUF_ZV (b)) + charpos = BUF_ZV (b); + + block_input (); + + /* Move point directly in the buffer. Use set_point_both which + operates on the current buffer — temporarily switch if needed. */ + struct buffer *oldb = current_buffer; + if (b != current_buffer) + set_buffer_internal_1 (b); + + SET_PT_BOTH (charpos, CHAR_TO_BYTE (charpos)); + + /* Keep mark state aligned with requested selection range. */ + if (range.length > 0) + { + ptrdiff_t mark_charpos = [self charposForAccessibilityIndex: + range.location + range.length]; + if (mark_charpos > BUF_ZV (b)) + mark_charpos = BUF_ZV (b); + Fset_marker (BVAR (b, mark), make_fixnum (mark_charpos), + Fcurrent_buffer ()); + bset_mark_active (b, Qt); + } + else + bset_mark_active (b, Qnil); + + if (b != oldb) + set_buffer_internal_1 (oldb); + + unblock_input (); + + /* Update cached state so the next notification cycle doesn't + re-announce this movement. */ + self.cachedPoint = charpos; + self.cachedMarkActive = (range.length > 0); +} + +- (void)setAccessibilityFocused:(BOOL)flag +{ + if (!flag) + return; + + struct window *w = self.emacsWindow; + if (!w || !WINDOW_LEAF_P (w)) + return; + + EmacsView *view = self.emacsView; + if (!view || !view->emacsframe) + return; + + block_input (); + + /* Raise the frame's NS window to ensure keyboard focus. */ + NSWindow *nswin = [view window]; + if (nswin && ![nswin isKeyWindow]) + [nswin makeKeyAndOrderFront:nil]; + + unblock_input (); + + /* 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}; + NSAccessibilityPostNotificationWithUserInfo ( + self, NSAccessibilitySelectedTextChangedNotification, info); +} + +- (NSInteger)accessibilityInsertionPointLineNumber +{ + struct window *w = self.emacsWindow; + if (!w || !WINDOW_LEAF_P (w)) + return 0; + + struct buffer *b = XBUFFER (w->contents); + if (!b) + return 0; + + [self ensureTextCache]; + if (!cachedText) + return 0; + + ptrdiff_t pt = BUF_PT (b); + NSUInteger point_idx = [self accessibilityIndexForCharpos:pt]; + if (point_idx > [cachedText length]) + point_idx = [cachedText length]; + + /* Count newlines from start to point_idx. */ + NSInteger line = 0; + for (NSUInteger i = 0; i < point_idx; i++) + { + if ([cachedText characterAtIndex:i] == '\n') + line++; + } + return line; +} + +- (NSString *)accessibilityStringForRange:(NSRange)range +{ + [self ensureTextCache]; + if (!cachedText || range.location + range.length > [cachedText length]) + return @""; + return [cachedText substringWithRange:range]; +} + +- (NSAttributedString *)accessibilityAttributedStringForRange:(NSRange)range +{ + NSString *str = [self accessibilityStringForRange:range]; + return [[[NSAttributedString alloc] initWithString:str] autorelease]; +} + +- (NSInteger)accessibilityLineForIndex:(NSInteger)index +{ + [self ensureTextCache]; + if (!cachedText || index < 0) + return 0; + + NSUInteger idx = (NSUInteger) index; + if (idx > [cachedText length]) + idx = [cachedText length]; + + /* Count newlines from start of cachedText to idx. */ + NSInteger line = 0; + for (NSUInteger i = 0; i < idx; i++) + { + if ([cachedText characterAtIndex:i] == '\n') + line++; + } + return line; +} + +- (NSRange)accessibilityRangeForLine:(NSInteger)line +{ + [self ensureTextCache]; + if (!cachedText || line < 0) + return NSMakeRange (NSNotFound, 0); + + NSUInteger len = [cachedText length]; + NSInteger cur_line = 0; + + for (NSUInteger i = 0; i <= len; i++) + { + if (cur_line == line) + { + /* Find end of this line. */ + 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') + { + cur_line++; + } + } + /* Phantom final line after the last newline. */ + if (cur_line == line) + return NSMakeRange (len, 0); + return NSMakeRange (NSNotFound, 0); +} + +- (NSRange)accessibilityRangeForIndex:(NSInteger)index +{ + [self ensureTextCache]; + if (!cachedText || index < 0 + || (NSUInteger) index >= [cachedText length]) + return NSMakeRange (NSNotFound, 0); + return [cachedText rangeOfComposedCharacterSequenceAtIndex:(NSUInteger)index]; +} + +- (NSRange)accessibilityStyleRangeForIndex:(NSInteger)index +{ + /* Return the range of the current line — simple approach. */ + NSInteger line = [self accessibilityLineForIndex:index]; + return [self accessibilityRangeForLine:line]; +} + +- (NSRect)accessibilityFrameForRange:(NSRange)range +{ + struct window *w = self.emacsWindow; + EmacsView *view = self.emacsView; + if (!w || !view) + return NSZeroRect; + /* Convert ax-index range to charpos range for glyph lookup. */ + [self ensureTextCache]; + ptrdiff_t cp_start = [self charposForAccessibilityIndex:range.location]; + ptrdiff_t cp_end = [self charposForAccessibilityIndex: + range.location + range.length]; + NSRange charRange = NSMakeRange (0, (NSUInteger) (cp_end - cp_start)); + return ns_ax_frame_for_range (w, view, cp_start, charRange); +} + +- (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint +{ + /* Hit test: convert screen point to buffer character index. */ + struct window *w = self.emacsWindow; + EmacsView *view = self.emacsView; + if (!w || !view || !w->current_matrix) + return NSMakeRange (0, 0); + + /* Convert screen point to EmacsView coordinates. */ + NSPoint windowPoint = [[view window] convertPointFromScreen:screenPoint]; + NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil]; + + /* Convert to window-relative pixel coordinates. */ + int x = (int) viewPoint.x - w->pixel_left; + int y = (int) viewPoint.y - w->pixel_top; + + if (x < 0 || y < 0 || x >= w->pixel_width || y >= w->pixel_height) + return NSMakeRange (0, 0); + + /* Find the glyph row at this y coordinate. */ + struct glyph_matrix *matrix = w->current_matrix; + struct glyph_row *hit_row = NULL; + + for (int i = 0; i < matrix->nrows; i++) + { + struct glyph_row *row = matrix->rows + i; + if (!row->enabled_p || !row->displays_text_p || row->mode_line_p) + continue; + int row_top = WINDOW_TO_FRAME_PIXEL_Y (w, MAX (0, row->y)); + if ((int) viewPoint.y >= row_top + && (int) viewPoint.y < row_top + row->visible_height) + { + hit_row = row; + break; + } + } + + if (!hit_row) + return NSMakeRange (0, 0); + + /* Find the glyph at this x coordinate within the row. */ + struct glyph *glyph = hit_row->glyphs[TEXT_AREA]; + struct glyph *end = glyph + hit_row->used[TEXT_AREA]; + int glyph_x = 0; + ptrdiff_t best_charpos = MATRIX_ROW_START_CHARPOS (hit_row); + + for (; glyph < end; glyph++) + { + if (glyph->type == CHAR_GLYPH && glyph->charpos > 0) + { + if (x >= glyph_x && x < glyph_x + glyph->pixel_width) + { + best_charpos = glyph->charpos; + break; + } + best_charpos = glyph->charpos; + } + glyph_x += glyph->pixel_width; + } + + /* Convert buffer charpos to accessibility index via mapping. */ + [self ensureTextCache]; + NSUInteger ax_idx = [self accessibilityIndexForCharpos:best_charpos]; + if (cachedText && ax_idx > [cachedText length]) + ax_idx = [cachedText length]; + return NSMakeRange (ax_idx, 1); +} + +- (NSRange)accessibilityVisibleCharacterRange +{ + /* Return the full cached text range. VoiceOver interprets the + visible range boundary as end-of-text, so we must expose the + entire buffer to avoid premature "end of text" announcements. */ + [self ensureTextCache]; + return NSMakeRange (0, cachedText ? [cachedText length] : 0); +} + +- (NSRect)accessibilityFrame +{ + struct window *w = self.emacsWindow; + if (!w) + return NSZeroRect; + + /* Subtract mode line height so the buffer element does not overlap it. */ + int text_h = w->pixel_height; + if (w->current_matrix) + { + for (int i = w->current_matrix->nrows - 1; i >= 0; i--) + { + struct glyph_row *row = w->current_matrix->rows + i; + if (row->enabled_p && row->mode_line_p) + { + text_h -= row->visible_height; + break; + } + } + } + return [self screenRectFromEmacsX:w->pixel_left + y:w->pixel_top + width:w->pixel_width + height:text_h]; +} + +/* ---- Notification dispatch ---- */ + +- (void)postAccessibilityNotificationsForFrame:(struct frame *)f +{ + struct window *w = self.emacsWindow; + if (!w || !WINDOW_LEAF_P (w)) + return; + + struct buffer *b = XBUFFER (w->contents); + if (!b) + return; + + ptrdiff_t modiff = BUF_MODIFF (b); + ptrdiff_t point = BUF_PT (b); + BOOL markActive = !NILP (BVAR (b, mark_active)); + + /* --- Text changed → typing echo --- + WebKit AXObjectCacheMac fallback enum: Edit = 1, Typing = 3. */ + if (modiff != self.cachedModiff) + { + /* Capture changed char before invalidating cache. */ + NSString *changedChar = @""; + if (point > self.cachedPoint + && point - self.cachedPoint == 1) + { + /* Single char inserted — refresh cache and grab it. */ + [self invalidateTextCache]; + [self ensureTextCache]; + if (cachedText) + { + NSUInteger idx = [self accessibilityIndexForCharpos:point - 1]; + if (idx < [cachedText length]) + changedChar = [cachedText substringWithRange: + NSMakeRange (idx, 1)]; + } + } + else + { + [self invalidateTextCache]; + } + + self.cachedModiff = modiff; + /* Update cachedPoint here so the selection-move branch below + does NOT fire for point changes caused by edits. WebKit and + Chromium never send both ValueChanged and SelectedTextChanged + for the same user action — they are mutually exclusive. */ + self.cachedPoint = point; + + NSDictionary *change = @{ + @"AXTextEditType": @(ns_ax_text_edit_type_typing), + @"AXTextChangeValue": changedChar, + @"AXTextChangeValueLength": @([changedChar length]) + }; + NSDictionary *userInfo = @{ + @"AXTextStateChangeType": @(ns_ax_text_state_change_edit), + @"AXTextChangeValues": @[change], + @"AXTextChangeElement": self + }; + NSAccessibilityPostNotificationWithUserInfo ( + self, NSAccessibilityValueChangedNotification, userInfo); + } + + /* --- Cursor moved or selection changed → line reading --- + WebKit AXObjectCacheMac fallback enum: SelectionMove = 2. + Use 'else if' — edits and selection moves are mutually exclusive + per the WebKit/Chromium pattern. VoiceOver gets confused if + both notifications arrive in the same runloop iteration. */ + else if (point != self.cachedPoint || markActive != self.cachedMarkActive) + { + ptrdiff_t oldPoint = self.cachedPoint; + self.cachedPoint = point; + self.cachedMarkActive = markActive; + + /* Compute direction. */ + NSInteger direction = ns_ax_text_selection_direction_discontiguous; + if (point > oldPoint) + direction = ns_ax_text_selection_direction_next; + else if (point < oldPoint) + direction = ns_ax_text_selection_direction_previous; + + int ctrlNP = 0; + bool isCtrlNP = ns_ax_event_is_ctrl_n_or_p (&ctrlNP); + + /* Compute granularity from movement distance. + Prefer robust line-range comparison for vertical movement, + otherwise single char (1) or unknown (0). */ + NSInteger granularity = ns_ax_text_selection_granularity_unknown; + [self ensureTextCache]; + if (cachedText && oldPoint > 0) + { + ptrdiff_t delta = point - oldPoint; + if (delta == 1 || delta == -1) + granularity = ns_ax_text_selection_granularity_character; /* Character. */ + else + { + NSUInteger tlen = [cachedText length]; + NSUInteger oldIdx = [self accessibilityIndexForCharpos:oldPoint]; + NSUInteger newIdx = [self accessibilityIndexForCharpos:point]; + if (oldIdx > tlen) + oldIdx = tlen; + if (newIdx > tlen) + newIdx = tlen; + + NSRange oldLine = [cachedText lineRangeForRange: + NSMakeRange (oldIdx, 0)]; + NSRange newLine = [cachedText lineRangeForRange: + NSMakeRange (newIdx, 0)]; + if (oldLine.location != newLine.location) + granularity = ns_ax_text_selection_granularity_line; /* Line. */ + + } + } + + /* Force line semantics for explicit C-n/C-p keystrokes. + This isolates the key-path difference from arrow-down/up. */ + if (isCtrlNP) + { + direction = (ctrlNP > 0 + ? ns_ax_text_selection_direction_next + : ns_ax_text_selection_direction_previous); + granularity = ns_ax_text_selection_granularity_line; + } + + NSDictionary *moveInfo = @{ + @"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), + @"AXTextSelectionDirection": @(direction), + @"AXTextSelectionGranularity": @(granularity), + @"AXTextChangeElement": self + }; + NSAccessibilityPostNotificationWithUserInfo ( + self, + NSAccessibilitySelectedTextChangedNotification, + moveInfo); + + /* C-n/C-p (`next-line' / `previous-line') can diverge from + arrow-down/up command paths in some major modes (completion list, + Dired, etc.). Emit an explicit line announcement for this basic + line-motion path so VoiceOver tracks the Emacs point reliably. */ + if ([self isAccessibilityFocused] + && cachedText + && (isCtrlNP || ns_ax_command_is_basic_line_move ()) + && (direction == ns_ax_text_selection_direction_next + || direction == ns_ax_text_selection_direction_previous)) + { + NSUInteger point_idx = [self accessibilityIndexForCharpos:point]; + if (point_idx <= [cachedText length]) + { + NSInteger lineNum = [self accessibilityLineForIndex:point_idx]; + NSRange lineRange = [self accessibilityRangeForLine:lineNum]; + if (lineRange.location != NSNotFound + && lineRange.length > 0 + && lineRange.location + lineRange.length <= [cachedText length]) + { + NSString *lineText = [cachedText substringWithRange:lineRange]; + lineText = [lineText stringByTrimmingCharactersInSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if ([lineText length] > 0) + { + NSDictionary *annInfo = @{ + NSAccessibilityAnnouncementKey: lineText, + NSAccessibilityPriorityKey: @(NSAccessibilityPriorityHigh) + }; + NSAccessibilityPostNotificationWithUserInfo ( + NSApp, + NSAccessibilityAnnouncementRequestedNotification, + annInfo); + } + } + } + } + + /* --- Completions announcement --- + When point changes in a non-focused buffer (e.g. *Completions* + while the minibuffer has keyboard focus), VoiceOver won't read + the change because it's tracking the focused element. Post an + announcement so the user hears the selected completion. + + If there is a `completions-highlight` overlay at point (Emacs + highlights the selected completion candidate), read its full + text instead of just the current line. */ + if (![self isAccessibilityFocused] && 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); + + /* 1) Prefer explicit completion candidate property when present. */ + Lisp_Object cstr = Fget_char_property (make_fixnum (point), + intern ("completion--string"), + Qnil); + if (STRINGP (cstr)) + announceText = [NSString stringWithLispString:cstr]; + + /* 2) Fallback: announce the mouse-face span at point. + completion-list-mode often marks the active candidate this way. */ + if (!announceText) + { + Lisp_Object mf = Fget_char_property (make_fixnum (point), + Qmouse_face, Qnil); + if (!NILP (mf)) + { + ptrdiff_t begv2 = BUF_BEGV (b); + ptrdiff_t zv2 = BUF_ZV (b); + ptrdiff_t s2 = point; + ptrdiff_t e2 = point; + + while (s2 > begv2) + { + Lisp_Object prev = Fget_char_property ( + make_fixnum (s2 - 1), Qmouse_face, Qnil); + if (!NILP (Fequal (prev, mf))) + s2--; + else + break; + } + + while (e2 < zv2) + { + Lisp_Object cur = Fget_char_property ( + make_fixnum (e2), Qmouse_face, Qnil); + if (!NILP (Fequal (cur, mf))) + e2++; + else + break; + } + + if (e2 > s2) + { + NSUInteger ax_s = [self accessibilityIndexForCharpos:s2]; + NSUInteger ax_e = [self accessibilityIndexForCharpos:e2]; + if (ax_e > ax_s && ax_e <= [cachedText length]) + announceText = [cachedText substringWithRange: + NSMakeRange (ax_s, ax_e - ax_s)]; + } + } + } + + /* 3) Fallback: check completions-highlight overlay span at point. */ + if (!announceText) + { + Lisp_Object faceSym = intern ("completions-highlight"); + Lisp_Object overlays = Foverlays_at (make_fixnum (point), Qnil); + Lisp_Object tail; + for (tail = overlays; CONSP (tail); tail = XCDR (tail)) + { + Lisp_Object ov = XCAR (tail); + Lisp_Object face = Foverlay_get (ov, Qface); + if (EQ (face, faceSym) + || (CONSP (face) + && !NILP (Fmemq (faceSym, face)))) + { + ptrdiff_t ov_start = OVERLAY_START (ov); + ptrdiff_t ov_end = OVERLAY_END (ov); + if (ov_end > ov_start) + { + announceText = ns_ax_completion_text_for_span (self, b, + ov_start, + ov_end, + cachedText); + currentOverlayStart = ov_start; + currentOverlayEnd = ov_end; + } + break; + } + } + } + + /* 4) Fallback: select the best completions-highlight overlay. + Prefer overlay nearest to point over first-found in buffer. */ + if (!announceText) + { + ptrdiff_t ov_start = 0; + ptrdiff_t ov_end = 0; + if (ns_ax_find_completion_overlay_range (b, point, &ov_start, &ov_end)) + { + announceText = ns_ax_completion_text_for_span (self, b, + ov_start, + ov_end, + cachedText); + currentOverlayStart = ov_start; + currentOverlayEnd = ov_end; + } + } + + if (b != oldb2) + set_buffer_internal_1 (oldb2); + + /* Final fallback: read the current line at point. */ + if (!announceText) + { + NSUInteger point_idx = [self accessibilityIndexForCharpos:point]; + if (point_idx <= [cachedText length]) + { + NSInteger lineNum = [self accessibilityLineForIndex: + point_idx]; + NSRange lineRange = [self accessibilityRangeForLine:lineNum]; + if (lineRange.location != NSNotFound + && lineRange.length > 0 + && lineRange.location + lineRange.length + <= [cachedText length]) + announceText = [cachedText substringWithRange:lineRange]; + } + } + + 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; + } + } + + } + else + { + 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; + } + } + } + } +} + +@end + + +@implementation EmacsAccessibilityModeLine + +- (NSAccessibilityRole)accessibilityRole +{ + return NSAccessibilityStaticTextRole; +} + +- (NSString *)accessibilityLabel +{ + struct window *w = self.emacsWindow; + if (w && WINDOW_LEAF_P (w)) + { + struct buffer *b = XBUFFER (w->contents); + if (b) + { + Lisp_Object name = BVAR (b, name); + if (STRINGP (name)) + { + NSString *bufName = [NSString stringWithLispString:name]; + return [NSString stringWithFormat:@"Mode Line - %@", bufName]; + } + } + } + return @"Mode Line"; +} + +- (id)accessibilityValue +{ + struct window *w = self.emacsWindow; + if (!w) + return @""; + return ns_ax_mode_line_text (w); +} + +- (NSRect)accessibilityFrame +{ + struct window *w = self.emacsWindow; + if (!w || !w->current_matrix) + return NSZeroRect; + + /* Find the mode line row and return its screen rect. */ + struct glyph_matrix *matrix = w->current_matrix; + for (int i = 0; i < matrix->nrows; i++) + { + struct glyph_row *row = matrix->rows + i; + if (row->enabled_p && row->mode_line_p) + { + return [self screenRectFromEmacsX:w->pixel_left + y:WINDOW_TO_FRAME_PIXEL_Y (w, + MAX (0, row->y)) + width:w->pixel_width + height:row->visible_height]; + } + } + return NSZeroRect; +} + +@end + +#endif /* NS_IMPL_COCOA */ + + +/* ========================================================================== + + EmacsView implementation + + ========================================================================== */ + + +@implementation EmacsView + +- (void)windowDidEndLiveResize:(NSNotification *)notification +{ + [self updateFramePosition]; +} + +/* Needed to inform when window closed from lisp. */ +- (void) setWindowClosing: (BOOL)closing +{ + NSTRACE ("[EmacsView setWindowClosing:%d]", closing); + + windowClosing = closing; +} + + +- (void)dealloc +{ + NSTRACE ("[EmacsView dealloc]"); + + /* Clear the view resize notification. */ + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:NSViewFrameDidChangeNotification + object:nil]; + + if (fs_state == FULLSCREEN_BOTH) + [nonfs_window release]; + +#if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MIN_REQUIRED >= 101400 + /* Release layer and menu */ + EmacsLayer *layer = (EmacsLayer *)[self layer]; + [layer release]; +#endif + + [accessibilityElements release]; + [[self menu] release]; + [super dealloc]; +} + + +/* Called on font panel selection. */ +- (void) changeFont: (id) sender +{ + struct font *font = FRAME_OUTPUT_DATA (emacsframe)->font; + NSFont *nsfont; + +#ifdef NS_IMPL_GNUSTEP + nsfont = ((struct nsfont_info *) font)->nsfont; +#else + nsfont = (NSFont *) macfont_get_nsctfont (font); +#endif + + if (!font_panel_active) + return; + + if (font_panel_result) + [font_panel_result release]; + + font_panel_result = (NSFont *) [sender convertFont: nsfont]; + + if (font_panel_result) + [font_panel_result retain]; + +#ifndef NS_IMPL_COCOA + font_panel_active = NO; + [NSApp stop: self]; +#endif +} + +#ifdef NS_IMPL_COCOA +- (void) noteUserSelectedFont +{ + font_panel_active = NO; + + /* If no font was previously selected, use the currently selected + font. */ + + if (!font_panel_result && FRAME_FONT (emacsframe)) + { + font_panel_result + = macfont_get_nsctfont (FRAME_FONT (emacsframe)); + + if (font_panel_result) + [font_panel_result retain]; + } + + [NSApp stop: self]; +} + +- (void) noteUserCancelledSelection +{ + font_panel_active = NO; + + if (font_panel_result) + [font_panel_result release]; + font_panel_result = nil; + + [NSApp stop: self]; +} +#endif + +- (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 + +#ifdef NS_IMPL_GNUSTEP + nsfont = ((struct nsfont_info *) font)->nsfont; +#else + nsfont = (NSFont *) macfont_get_nsctfont (font); +#endif + +#ifdef NS_IMPL_COCOA + buttons + = ns_create_font_panel_buttons (self, + @selector (noteUserSelectedFont), + @selector (noteUserCancelledSelection)); + [[fm fontPanel: YES] setAccessoryView: buttons]; + [buttons release]; +#endif + + [fm setSelectedFont: nsfont isMultiple: NO]; + [fm orderFrontFontPanel: NSApp]; + + font_panel_active = YES; + timeout = make_timespec (0, 100000000); + + 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 (font_panel_result) + [font_panel_result autorelease]; + +#ifdef NS_IMPL_COCOA + if (!canceled) + font_panel_result = nil; +#endif + + result = font_panel_result; + font_panel_result = nil; + + [[fm fontPanel: YES] setIsVisible: NO]; + font_panel_active = NO; + + if (result) + return ns_font_desc_to_font_spec ([result fontDescriptor], + result); + + return Qnil; +} + +- (BOOL)acceptsFirstResponder +{ + NSTRACE ("[EmacsView acceptsFirstResponder]"); + return YES; +} + +/* Tell NS we want to accept clicks that activate the window */ +- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent +{ + NSTRACE_MSG ("First mouse event: type=%ld, clickCount=%ld", + [theEvent type], [theEvent clickCount]); + return ns_click_through; +} +- (void)resetCursorRects +{ + 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 { @@ -8237,6 +9838,28 @@ ns_in_echo_area (void) XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop + +#ifdef NS_IMPL_COCOA + /* Notify VoiceOver that the focused accessibility element changed. + Post on the focused virtual element so VoiceOver starts tracking it. + This is critical for initial focus and app-switch scenarios. */ + { + id focused = [self accessibilityFocusedUIElement]; + if (focused + && [focused isKindOfClass:[EmacsAccessibilityBuffer class]]) + { + NSAccessibilityPostNotification (focused, + NSAccessibilityFocusedUIElementChangedNotification); + NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), + @"AXTextChangeElement": focused}; + NSAccessibilityPostNotificationWithUserInfo (focused, + NSAccessibilitySelectedTextChangedNotification, info); + } + else if (focused) + NSAccessibilityPostNotification (focused, + NSAccessibilityFocusedUIElementChangedNotification); + } +#endif } @@ -9474,6 +11097,298 @@ ns_in_echo_area (void) return fs_state; } +#ifdef NS_IMPL_COCOA + +/* ---- Accessibility: walk the Emacs window tree ---- */ + +static void +ns_ax_collect_windows (Lisp_Object window, EmacsView *view, + NSMutableArray *elements, + NSDictionary *existing) +{ + if (NILP (window)) + return; + + struct window *w = XWINDOW (window); + + if (WINDOW_LEAF_P (w)) + { + /* Buffer element — reuse existing if available. */ + EmacsAccessibilityBuffer *elem + = [existing objectForKey:[NSValue valueWithPointer:w]]; + if (!elem) + { + elem = [[EmacsAccessibilityBuffer alloc] init]; + elem.emacsView = view; + + /* Initialize cached state to -1 to force first notification. */ + elem.cachedModiff = -1; + elem.cachedPoint = -1; + elem.cachedMarkActive = NO; + } + else + { + [elem retain]; + } + elem.emacsWindow = w; + [elements addObject:elem]; + [elem release]; + + /* Mode line element (skip for minibuffer). */ + if (!MINI_WINDOW_P (w)) + { + EmacsAccessibilityModeLine *ml + = [[EmacsAccessibilityModeLine alloc] init]; + ml.emacsView = view; + ml.emacsWindow = w; + [elements addObject:ml]; + [ml release]; + } + } + else + { + /* Internal (combination) window — recurse into children. */ + Lisp_Object child = w->contents; + while (!NILP (child)) + { + ns_ax_collect_windows (child, view, elements, existing); + child = XWINDOW (child)->next; + } + } +} + +- (void)rebuildAccessibilityTree +{ + if (!emacsframe) + return; + + /* Build map of existing elements by window pointer for reuse. */ + NSMutableDictionary *existing = [NSMutableDictionary dictionary]; + if (accessibilityElements) + { + for (EmacsAccessibilityElement *elem in accessibilityElements) + { + if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]] + && elem.emacsWindow) + [existing setObject:elem + forKey:[NSValue valueWithPointer: + elem.emacsWindow]]; + } + } + + NSMutableArray *newElements = [NSMutableArray arrayWithCapacity:8]; + + /* Collect from main window tree. */ + Lisp_Object root = FRAME_ROOT_WINDOW (emacsframe); + ns_ax_collect_windows (root, self, newElements, existing); + + /* Include minibuffer. */ + Lisp_Object mini = emacsframe->minibuffer_window; + if (!NILP (mini)) + ns_ax_collect_windows (mini, self, newElements, existing); + + [accessibilityElements release]; + accessibilityElements = [newElements retain]; + accessibilityTreeValid = YES; +} + +- (void)invalidateAccessibilityTree +{ + accessibilityTreeValid = NO; +} + +- (NSAccessibilityRole)accessibilityRole +{ + return NSAccessibilityGroupRole; +} + +- (NSString *)accessibilityLabel +{ + return @"Emacs"; +} + +- (BOOL)isAccessibilityElement +{ + return YES; +} + +- (NSArray *)accessibilityChildren +{ + if (!accessibilityElements || !accessibilityTreeValid) + [self rebuildAccessibilityTree]; + return accessibilityElements; +} + +- (id)accessibilityFocusedUIElement +{ + if (!emacsframe) + return self; + + if (!accessibilityElements || !accessibilityTreeValid) + [self rebuildAccessibilityTree]; + + struct window *sel = XWINDOW (emacsframe->selected_window); + for (EmacsAccessibilityElement *elem in accessibilityElements) + { + if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]] + && elem.emacsWindow == sel) + return elem; + } + return self; +} + +/* Called from ns_update_end to post AX notifications. + + Important: post notifications BEFORE rebuilding the tree. + 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. */ +- (void)postAccessibilityUpdates +{ + if (!emacsframe) + return; + + /* Re-entrance guard: VoiceOver callbacks during notification posting + can trigger redisplay, which calls ns_update_end, which calls us + again. Prevent infinite recursion. */ + if (accessibilityUpdating) + return; + accessibilityUpdating = YES; + + /* Detect window tree change (split, delete, new buffer). Compare + FRAME_ROOT_WINDOW — if it changed, the tree structure changed. */ + Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); + if (!EQ (curRoot, lastRootWindow)) + { + lastRootWindow = curRoot; + accessibilityTreeValid = NO; + } + + /* If tree is stale, rebuild FIRST so we don't iterate freed + window pointers. Skip notifications for this cycle — the + freshly-built elements have no previous state to diff against. */ + if (!accessibilityTreeValid) + { + [self rebuildAccessibilityTree]; + NSAccessibilityPostNotification (self, + NSAccessibilityLayoutChangedNotification); + + /* Post focus change so VoiceOver picks up the new tree. */ + id focused = [self accessibilityFocusedUIElement]; + if (focused && focused != self) + NSAccessibilityPostNotification (focused, + NSAccessibilityFocusedUIElementChangedNotification); + + lastSelectedWindow = emacsframe->selected_window; + accessibilityUpdating = NO; + return; + } + + /* Post per-buffer notifications using EXISTING elements that have + cached state from the previous cycle. Validate each window + pointer before use. */ + for (EmacsAccessibilityElement *elem in accessibilityElements) + { + if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]]) + { + struct window *w = elem.emacsWindow; + if (w && WINDOW_LEAF_P (w) + && BUFFERP (w->contents) && XBUFFER (w->contents)) + [(EmacsAccessibilityBuffer *) elem + postAccessibilityNotificationsForFrame:emacsframe]; + } + } + + /* Check for window switch (C-x o). */ + Lisp_Object curSel = emacsframe->selected_window; + BOOL windowSwitched = !EQ (curSel, lastSelectedWindow); + if (windowSwitched) + { + lastSelectedWindow = curSel; + id focused = [self accessibilityFocusedUIElement]; + if (focused && focused != self + && [focused isKindOfClass:[EmacsAccessibilityBuffer class]]) + { + NSAccessibilityPostNotification (focused, + NSAccessibilityFocusedUIElementChangedNotification); + NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), + @"AXTextChangeElement": focused}; + NSAccessibilityPostNotificationWithUserInfo (focused, + NSAccessibilitySelectedTextChangedNotification, info); + } + else if (focused && focused != self) + NSAccessibilityPostNotification (focused, + NSAccessibilityFocusedUIElementChangedNotification); + } + + accessibilityUpdating = NO; +} + +/* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ---- + + accessibilityFrame returns the VIEW's frame (standard behavior). + The cursor location is exposed through accessibilityBoundsForRange: + which AT tools query using the selectedTextRange. */ + +- (NSRect)accessibilityBoundsForRange:(NSRange)range +{ + /* Return cursor screen rect. AT tools call this with the + selectedTextRange to locate the insertion point. */ + NSRect viewRect = lastAccessibilityCursorRect; + + if (viewRect.size.width < 1) + viewRect.size.width = 1; + if (viewRect.size.height < 1) + viewRect.size.height = 8; + + NSWindow *win = [self window]; + if (win == nil) + return NSZeroRect; + + NSRect windowRect = [self convertRect:viewRect toView:nil]; + return [win convertRectToScreen:windowRect]; +} + +- (NSRect)accessibilityFrameForRange:(NSRange)range +{ + return [self accessibilityBoundsForRange:range]; +} + +/* ---- Legacy parameterized attribute APIs (Zoom uses these) ---- */ + +- (NSArray *)accessibilityParameterizedAttributeNames +{ + NSArray *superAttrs = [super accessibilityParameterizedAttributeNames]; + if (superAttrs == nil) + superAttrs = @[]; + return [superAttrs arrayByAddingObjectsFromArray: + @[NSAccessibilityBoundsForRangeParameterizedAttribute, + NSAccessibilityStringForRangeParameterizedAttribute]]; +} + +- (id)accessibilityAttributeValue:(NSString *)attribute + forParameter:(id)parameter +{ + if ([attribute isEqualToString: + NSAccessibilityBoundsForRangeParameterizedAttribute]) + { + NSRange range = [(NSValue *) parameter rangeValue]; + return [NSValue valueWithRect: + [self accessibilityBoundsForRange:range]]; + } + + if ([attribute isEqualToString: + NSAccessibilityStringForRangeParameterizedAttribute]) + { + NSRange range = [(NSValue *) parameter rangeValue]; + return [self accessibilityStringForRange:range]; + } + + return [super accessibilityAttributeValue:attribute forParameter:parameter]; +} + +#endif /* NS_IMPL_COCOA */ + @end /* EmacsView */ @@ -9941,6 +11856,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c) return [super accessibilityAttributeValue:attribute]; } + +- (id)accessibilityFocusedUIElement +{ + EmacsView *view = (EmacsView *)[self delegate]; + if (view && [view respondsToSelector:@selector(accessibilityFocusedUIElement)]) + return [view accessibilityFocusedUIElement]; + return self; +} #endif /* NS_IMPL_COCOA */ /* Constrain size and placement of a frame. -- 2.43.0