- arrayWithCapacity: returns autorelease object, must retain for ivar
- Release old array before reassigning in rebuildAccessibilityTree
- Release in EmacsView dealloc
- Fixes crash: _NSPasteboardTypeCache countByEnumeratingWithState (dangling pointer)
Complete rewrite of accessibility support. Instead of flat AXTextArea
on EmacsView, implements proper tree with virtual elements:
EmacsWindow (AXWindow)
└── EmacsView (AXGroup)
├── EmacsAccessibilityBuffer (AXTextArea) — window 1
├── EmacsAccessibilityBuffer (AXTextArea) — window 2
└── ...
Each EmacsAccessibilityBuffer maps to one Emacs window and exposes:
- Buffer text via glyph row iteration (accurate, handles overlays)
- Visual line navigation (glyph_row vpos)
- Cursor tracking with glyph charpos mapping
- Hit testing (accessibilityRangeForPosition:)
- Selection range with active mark
- Screen geometry via pixel coordinate conversion
- Rich typing echo (kAXTextEditTypeTyping userInfo)
- UAZoomChangeFocus for Zoom
Dynamic tree rebuilt on each redisplay cycle from FRAME_ROOT_WINDOW.
Notifications on correct virtual elements (not container view).
3 review cycles (scores: 62 → 68 → 82), plus manual fixes for
hit testing and selection range.
Root cause: all accessibility line methods used count_lines (logical/
newline-counting), but VoiceOver compares line numbers before/after
cursor movement. Wrapped lines had same logical line number, so VO
fell back to reading single characters instead of full lines.
Fix: replaced count_lines/find_newline_no_quit with compute_motion/
vmotion (visual screen lines) in all three methods. Matches iTerm2's
approach of returning screen rows.
Changes from v11:
- accessibilityInsertionPointLineNumber: compute_motion visual lines
- accessibilityLineForIndex: compute_motion visual lines
- accessibilityRangeForLine: vmotion for line start/end
- SelectedRowsChanged only on visual line change (new ivar tracking)
- accessibilityLabel returns buffer name for window switch
- No double ValueChanged on edits (content_changed flag)
- Trailing newline stripped from line ranges
- compute_motion width=-1 for consistency with vmotion
3 pipeline iterations, scores: 78 → 92 → 95
Key insight from iTerm2 analysis: VoiceOver does NOT need rich userInfo
on SelectedTextChanged for cursor movement. Bare notifications + correct
protocol methods (accessibilityStringForRange:, accessibilityLineForIndex:,
accessibilityRangeForLine:) are sufficient. Rich userInfo with wrong values
was likely causing VoiceOver to silently discard notifications.
Changes from v10:
- SelectedTextChanged: bare notification (removed rich userInfo)
- Added SelectedRowsChanged + SelectedColumnsChanged (iTerm2 triple pattern)
- Added bare ValueChanged on every cursor draw (not just edits)
- Fixed current_buffer safety: BUF_BYTE_ADDRESS, set_buffer_internal_1
- Added accessibilityRangeForIndex:, accessibilitySelectedTextRanges,
isAccessibilityFocused, accessibilityLabel
Key changes from v9:
- SelectedTextChanged now includes rich userInfo:
direction (Next/Previous/Discontiguous) and
granularity (Character/Word/Line) inferred from
old vs new cursor position comparison
- Track lastAccessibilityCursorPos + lastAccessibilityCursorLine
ivars for position delta detection
- accessibilityInsertionPointLineNumber: buffer lines (count_lines)
instead of visual vpos
- accessibilityLineForIndex: buffer lines via count_lines
- accessibilityRangeForLine: buffer lines via find_newline_no_quit
- Skip notification on unchanged position (avoids VO repeating)
- Word granularity heuristic: same line, >1 char delta
Replace extern NSString* declarations with raw string literals
(@"AXTextStateChangeType" etc.) to avoid redeclaration type conflict
with NSAccessibilityConstants.h on macOS 26 Tahoe SDK where these
symbols were added to public headers.
Guard accessibility notifications with on_p && active_p so they only
fire when drawing the cursor in the selected window. Without this,
ns_draw_window_cursor is called for both old and new windows during
redisplay, and UAZoomChangeFocus fires for the old window last.
UAZoomChangeFocus is Apple's documented API for Zoom focus control.
NSAccessibility serves VoiceOver and other AT tools.
Both are needed - same approach as iTerm2.
References Apple developer documentation URLs in comments.
Based on verified research:
- UAZoomChangeFocus IS officially documented by Apple (UniversalAccess.h)
- iTerm2 uses UAZoomChangeFocus for Zoom tracking (PTYTextView.m)
- NSAccessibility alone insufficient for Zoom custom view tracking
- Added Apple documentation URLs in code comments
UAZoomChangeFocus (documented Apple API) + NSAccessibility.
References to official Apple documentation in comments.
Pattern matches iTerm2 implementation.
Key changes:
- Add accessibilityFrame on EmacsView returning cursor screen rect
(the standard mechanism used by Terminal.app, iTerm2, Firefox)
- Guard UAZoomChangeFocus with bundleIdentifier check (bare binaries
are silently ignored by the window server)
- Extensive English comments explaining both mechanisms
- Deduplicate legacy parameterized attribute via accessibilityBoundsForRange:
Root cause of Stéphane's failure: UAZoomChangeFocus communicates via the
window server which identifies apps by CFBundleIdentifier. Bare binaries
(src/emacs, symlinks) have no bundle ID and are ignored. NSAccessibility
notifications + accessibilityFrame work for ALL launch methods.
Compilation error: EmacsView does not declare NSAccessibility conformance
so accessibilityConvertScreenRect: is not visible to the compiler.
Replaced with manual coordinate conversion:
primaryH = NSScreen.screens.first.frame.height
cgRect.origin.y = primaryH - cgRect.origin.y - cgRect.size.height
Root cause found via research pipeline:
NSAccessibility notifications alone are insufficient for custom NSView.
macOS Zoom 'Follow keyboard focus' requires UAZoomChangeFocus() from
HIServices/UniversalAccess.h — same as iTerm2 and Chromium.
Squashed into single clean patch. 159 insertions, 2 files.
macOS Zoom is event-driven -- it only queries AXBoundsForRange after
receiving NSAccessibilitySelectedTextChangedNotification. Previous
version implemented the bounds methods but never posted notifications,
so Zoom never triggered a cursor position query.
Added:
- NSAccessibilityPostNotification(SelectedTextChanged) in ns_draw_window_cursor
- NSAccessibilityPostNotification(FocusedUIElementChanged) in windowDidBecomeKey
- accessibilityAttributeNames + accessibilityAttributeValue: for
NSAccessibilitySelectedTextRangeAttribute -> returns {0,0}
Old parameterized attribute API alone insufficient — macOS Zoom prefers
the new NSAccessibilityProtocol method accessibilityBoundsForRange:.
Also adds isAccessibilityElement returning YES.
Both APIs now implemented for compatibility across macOS versions.