mirror of
https://github.com/electron/electron.git
synced 2026-01-08 07:04:01 -05:00
refactor: match upstream macOS a11y handling (#47144)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user