refactor: match upstream macOS a11y handling (#47144)

This commit is contained in:
Shelley Vohr
2025-05-20 17:06:57 +02:00
committed by GitHub
parent a19198d784
commit 296e39456a

View File

@@ -8,12 +8,14 @@
#include <utility>
#include "base/auto_reset.h"
#include "base/mac/mac_util.h"
#include "base/observer_list.h"
#include "base/strings/sys_string_conversions.h"
#include "content/public/browser/browser_accessibility_state.h"
#include "content/public/browser/native_event_processor_mac.h"
#include "content/public/browser/native_event_processor_observer_mac.h"
#include "content/public/browser/scoped_accessibility_mode.h"
#include "content/public/common/content_features.h"
#include "shell/browser/browser.h"
#include "shell/browser/mac/dict_util.h"
#import "shell/browser/mac/electron_application_delegate.h"
@@ -31,12 +33,19 @@ inline void dispatch_sync_main(dispatch_block_t block) {
} // namespace
@interface AtomApplication () <NativeEventProcessor> {
int _AXEnhancedUserInterfaceRequests;
BOOL _voiceOverEnabled;
BOOL _sonomaAccessibilityRefinementsAreActive;
base::ObserverList<content::NativeEventProcessorObserver>::Unchecked
observers_;
}
// Enables/disables screen reader support on changes to VoiceOver status.
- (void)voiceOverStateChanged:(BOOL)voiceOverEnabled;
@end
@implementation AtomApplication {
std::unique_ptr<content::ScopedAccessibilityMode>
_scoped_accessibility_mode_voiceover;
std::unique_ptr<content::ScopedAccessibilityMode>
_scoped_accessibility_mode_general;
}
@@ -45,6 +54,37 @@ inline void dispatch_sync_main(dispatch_block_t block) {
return (AtomApplication*)[super sharedApplication];
}
- (void)finishLaunching {
[super finishLaunching];
_sonomaAccessibilityRefinementsAreActive =
base::mac::MacOSVersion() >= 14'00'00 &&
base::FeatureList::IsEnabled(
features::kSonomaAccessibilityActivationRefinements);
}
- (void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
if ([keyPath isEqualToString:@"voiceOverEnabled"] &&
context == content::BrowserAccessibilityState::GetInstance()) {
NSNumber* newValueNumber = [change objectForKey:NSKeyValueChangeNewKey];
DCHECK([newValueNumber isKindOfClass:[NSNumber class]]);
if ([newValueNumber isKindOfClass:[NSNumber class]]) {
[self voiceOverStateChanged:[newValueNumber boolValue]];
}
return;
}
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
- (void)willPowerOff:(NSNotification*)notify {
userStoppedShutdown_ = shouldShutdown_ && !shouldShutdown_.Run();
}
@@ -214,14 +254,7 @@ inline void dispatch_sync_main(dispatch_block_t block) {
- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
bool is_manual_ax = [attribute isEqualToString:@"AXManualAccessibility"];
if ([attribute isEqualToString:@"AXEnhancedUserInterface"] || is_manual_ax) {
if (![value boolValue]) {
_scoped_accessibility_mode_general.reset();
} else if (!_scoped_accessibility_mode_general) {
_scoped_accessibility_mode_general =
content::BrowserAccessibilityState::GetInstance()
->CreateScopedModeForProcess(ui::kAXModeComplete);
}
[self enableScreenReaderCompleteModeAfterDelay:[value boolValue]];
electron::Browser::Get()->OnAccessibilitySupportChanged();
// Don't call the superclass function for AXManualAccessibility,
@@ -241,8 +274,11 @@ inline void dispatch_sync_main(dispatch_block_t block) {
// recommends turning on a11y when an AT accesses the 'accessibilityRole'
// property. This function is accessed frequently, so we only change the
// accessibility state when accessibility is already disabled.
if (!_scoped_accessibility_mode_general) {
ui::AXMode target_mode = ui::kAXModeBasic;
if (!_scoped_accessibility_mode_general &&
!_scoped_accessibility_mode_voiceover) {
ui::AXMode target_mode = _sonomaAccessibilityRefinementsAreActive
? ui::AXMode::kNativeAPIs
: ui::kAXModeBasic;
_scoped_accessibility_mode_general =
content::BrowserAccessibilityState::GetInstance()
->CreateScopedModeForProcess(target_mode |
@@ -252,6 +288,82 @@ inline void dispatch_sync_main(dispatch_block_t block) {
return [super accessibilityRole];
}
- (void)enableScreenReaderCompleteMode:(BOOL)enable {
if (enable) {
if (!_scoped_accessibility_mode_voiceover) {
_scoped_accessibility_mode_voiceover =
content::BrowserAccessibilityState::GetInstance()
->CreateScopedModeForProcess(ui::kAXModeComplete |
ui::AXMode::kFromPlatform |
ui::AXMode::kScreenReader);
}
} else {
_scoped_accessibility_mode_voiceover.reset();
}
}
// We need to call enableScreenReaderCompleteMode:YES from performSelector:...
// but there's no way to supply a BOOL as a parameter, so we have this
// explicit enable... helper method.
- (void)enableScreenReaderCompleteMode {
_AXEnhancedUserInterfaceRequests = 0;
[self enableScreenReaderCompleteMode:YES];
}
- (void)voiceOverStateChanged:(BOOL)voiceOverEnabled {
_voiceOverEnabled = voiceOverEnabled;
[self enableScreenReaderCompleteMode:voiceOverEnabled];
}
// Enables or disables screen reader support for non-VoiceOver assistive
// technology (AT), possibly after a delay.
//
// Now that we directly monitor VoiceOver status, we no longer watch for
// changes to AXEnhancedUserInterface for that signal from VO. However, other
// AT can set a value for AXEnhancedUserInterface, so we can't ignore it.
// Unfortunately, as of macOS Sonoma, we sometimes see spurious changes to
// AXEnhancedUserInterface (quick on and off). We debounce by waiting for these
// changes to settle down before updating the screen reader state.
- (void)enableScreenReaderCompleteModeAfterDelay:(BOOL)enable {
// If VoiceOver is already explicitly enabled, ignore requests from other AT.
if (_voiceOverEnabled) {
return;
}
// If this is a request to disable screen reader support, and we haven't seen
// a corresponding enable request, go ahead and disable.
if (!enable && _AXEnhancedUserInterfaceRequests == 0) {
[self enableScreenReaderCompleteMode:NO];
return;
}
// Use a counter to track requests for changes to the screen reader state.
if (enable) {
_AXEnhancedUserInterfaceRequests++;
} else {
_AXEnhancedUserInterfaceRequests--;
}
DCHECK_GE(_AXEnhancedUserInterfaceRequests, 0);
// _AXEnhancedUserInterfaceRequests > 0 means we want to enable screen
// reader support, but we'll delay that action until there are no more state
// change requests within a two-second window. Cancel any pending
// performSelector:..., and schedule a new one to restart the countdown.
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector
(enableScreenReaderCompleteMode)
object:nil];
if (_AXEnhancedUserInterfaceRequests > 0) {
const float kTwoSecondDelay = 2.0;
[self performSelector:@selector(enableScreenReaderCompleteMode)
withObject:nil
afterDelay:kTwoSecondDelay];
}
}
- (void)orderFrontStandardAboutPanel:(id)sender {
electron::Browser::Get()->ShowAboutPanel();
}