diff options
Diffstat (limited to '')
-rw-r--r-- | accessible/mac/Platform.mm | 268 |
1 files changed, 268 insertions, 0 deletions
diff --git a/accessible/mac/Platform.mm b/accessible/mac/Platform.mm new file mode 100644 index 0000000000..eb507adefb --- /dev/null +++ b/accessible/mac/Platform.mm @@ -0,0 +1,268 @@ +/* clang-format off */ +/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* clang-format on */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import <Cocoa/Cocoa.h> + +#import "MOXTextMarkerDelegate.h" + +#include "Platform.h" +#include "RemoteAccessible.h" +#include "DocAccessibleParent.h" +#include "mozTableAccessible.h" +#include "mozTextAccessible.h" +#include "MOXWebAreaAccessible.h" +#include "nsAccUtils.h" +#include "TextRange.h" + +#include "nsAppShell.h" +#include "nsCocoaUtils.h" +#include "mozilla/Telemetry.h" + +// Available from 10.13 onwards; test availability at runtime before using +@interface NSWorkspace (AvailableSinceHighSierra) +@property(readonly) BOOL isVoiceOverEnabled; +@property(readonly) BOOL isSwitchControlEnabled; +@end + +namespace mozilla { +namespace a11y { + +// Mac a11y whitelisting +static bool sA11yShouldBeEnabled = false; + +bool ShouldA11yBeEnabled() { + EPlatformDisabledState disabledState = PlatformDisabledState(); + return (disabledState == ePlatformIsForceEnabled) || + ((disabledState == ePlatformIsEnabled) && sA11yShouldBeEnabled); +} + +void PlatformInit() {} + +void PlatformShutdown() {} + +void ProxyCreated(RemoteAccessible* aProxy) { + if (aProxy->Role() == roles::WHITESPACE) { + // We don't create a native object if we're child of a "flat" accessible; + // for example, on OS X buttons shouldn't have any children, because that + // makes the OS confused. We also don't create accessibles for <br> + // (whitespace) elements. + return; + } + + // Pass in dummy state for now as retrieving proxy state requires IPC. + // Note that we can use RemoteAccessible::IsTable* functions here because they + // do not use IPC calls but that might change after bug 1210477. + Class type; + if (aProxy->IsTable()) { + type = [mozTableAccessible class]; + } else if (aProxy->IsTableRow()) { + type = [mozTableRowAccessible class]; + } else if (aProxy->IsTableCell()) { + type = [mozTableCellAccessible class]; + } else if (aProxy->IsDoc()) { + type = [MOXWebAreaAccessible class]; + } else { + type = GetTypeFromRole(aProxy->Role()); + } + + mozAccessible* mozWrapper = [[type alloc] initWithAccessible:aProxy]; + aProxy->SetWrapper(reinterpret_cast<uintptr_t>(mozWrapper)); +} + +void ProxyDestroyed(RemoteAccessible* aProxy) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy); + [wrapper expire]; + [wrapper release]; + aProxy->SetWrapper(0); + + if (aProxy->IsDoc()) { + [MOXTextMarkerDelegate destroyForDoc:aProxy]; + } +} + +void PlatformEvent(Accessible* aTarget, uint32_t aEventType) { + // Ignore event that we don't escape below, they aren't yet supported. + if (aEventType != nsIAccessibleEvent::EVENT_ALERT && + aEventType != nsIAccessibleEvent::EVENT_VALUE_CHANGE && + aEventType != nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE && + aEventType != nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE && + aEventType != nsIAccessibleEvent::EVENT_REORDER && + aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED && + aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED && + aEventType != nsIAccessibleEvent::EVENT_NAME_CHANGE && + aEventType != nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED) { + return; + } + + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); + if (wrapper) { + [wrapper handleAccessibleEvent:aEventType]; + } +} + +void PlatformStateChangeEvent(Accessible* aTarget, uint64_t aState, + bool aEnabled) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); + if (wrapper) { + [wrapper stateChanged:aState isEnabled:aEnabled]; + } +} + +void PlatformFocusEvent(Accessible* aTarget, + const LayoutDeviceIntRect& aCaretRect) { + if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) { + [wrapper handleAccessibleEvent:nsIAccessibleEvent::EVENT_FOCUS]; + } +} + +void PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset, + bool aIsSelectionCollapsed, int32_t aGranularity, + const LayoutDeviceIntRect& aCaretRect, + bool aFromUser) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); + MOXTextMarkerDelegate* delegate = [MOXTextMarkerDelegate + getOrCreateForDoc:nsAccUtils::DocumentFor(aTarget)]; + [delegate setCaretOffset:aTarget at:aOffset moveGranularity:aGranularity]; + if (aIsSelectionCollapsed) { + // If selection is collapsed, invalidate selection. + [delegate setSelectionFrom:aTarget at:aOffset to:aTarget at:aOffset]; + } + + if (wrapper) { + if (mozTextAccessible* textAcc = + static_cast<mozTextAccessible*>([wrapper moxEditableAncestor])) { + [textAcc + handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED]; + } else { + [wrapper + handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED]; + } + } +} + +void PlatformTextChangeEvent(Accessible* aTarget, const nsAString& aStr, + int32_t aStart, uint32_t aLen, bool aIsInsert, + bool aFromUser) { + Accessible* acc = aTarget; + // If there is a text input ancestor, use it as the event source. + while (acc && GetTypeFromRole(acc->Role()) != [mozTextAccessible class]) { + acc = acc->Parent(); + } + mozAccessible* wrapper = GetNativeFromGeckoAccessible(acc ? acc : aTarget); + [wrapper handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString(aStr) + inserted:aIsInsert + inContainer:aTarget + at:aStart]; +} + +void PlatformShowHideEvent(Accessible*, Accessible*, bool, bool) {} + +void PlatformSelectionEvent(Accessible* aTarget, Accessible* aWidget, + uint32_t aEventType) { + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aWidget); + if (wrapper) { + [wrapper handleAccessibleEvent:aEventType]; + } +} + +void PlatformTextSelectionChangeEvent(Accessible* aTarget, + const nsTArray<TextRange>& aSelection) { + if (aSelection.Length()) { + MOXTextMarkerDelegate* delegate = [MOXTextMarkerDelegate + getOrCreateForDoc:nsAccUtils::DocumentFor(aTarget)]; + // Cache the selection. + [delegate setSelectionFrom:aSelection[0].StartContainer() + at:aSelection[0].StartOffset() + to:aSelection[0].EndContainer() + at:aSelection[0].EndOffset()]; + } + + mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); + if (wrapper) { + [wrapper + handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED]; + } +} + +void PlatformRoleChangedEvent(Accessible* aTarget, const a11y::role& aRole, + uint8_t aRoleMapEntryIndex) { + if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) { + [wrapper handleRoleChanged:aRole]; + } +} + +} // namespace a11y +} // namespace mozilla + +@interface GeckoNSApplication (a11y) +- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute; +@end + +@implementation GeckoNSApplication (a11y) + +- (NSAccessibilityRole)accessibilityRole { + // For ATs that don't request `AXEnhancedUserInterface` we need to enable + // accessibility when a role is fetched. Not ideal, but this is needed + // for such services as Voice Control. + if (!mozilla::a11y::sA11yShouldBeEnabled) { + [self accessibilitySetValue:@YES forAttribute:@"AXEnhancedUserInterface"]; + } + return [super accessibilityRole]; +} + +- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute { + if ([attribute isEqualToString:@"AXEnhancedUserInterface"]) { + mozilla::a11y::sA11yShouldBeEnabled = ([value intValue] == 1); + if (sA11yShouldBeEnabled) { + // If accessibility should be enabled, log the appropriate client + nsAutoString client; + if ([[NSWorkspace sharedWorkspace] + respondsToSelector:@selector(isVoiceOverEnabled)] && + [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]) { + client.Assign(u"VoiceOver"_ns); + } else if ([[NSWorkspace sharedWorkspace] + respondsToSelector:@selector(isSwitchControlEnabled)] && + [[NSWorkspace sharedWorkspace] isSwitchControlEnabled]) { + client.Assign(u"SwitchControl"_ns); + } else { + // This is more complicated than the NSWorkspace queries above + // because (a) there is no "full keyboard access" query for NSWorkspace + // and (b) the [NSApplication fullKeyboardAccessEnabled] query checks + // the pre-Monterey version of full keyboard access, which is not what + // we're looking for here. For more info, see bug 1772375 comment 7. + Boolean exists; + int val = CFPreferencesGetAppIntegerValue( + CFSTR("FullKeyboardAccessEnabled"), + CFSTR("com.apple.Accessibility"), &exists); + if (exists && val == 1) { + client.Assign(u"FullKeyboardAccess"_ns); + } else { + val = CFPreferencesGetAppIntegerValue( + CFSTR("CommandAndControlEnabled"), + CFSTR("com.apple.Accessibility"), &exists); + if (exists && val == 1) { + client.Assign(u"VoiceControl"_ns); + } else { + client.Assign(u"Unknown"_ns); + } + } + } + +#if defined(MOZ_TELEMETRY_REPORTING) + mozilla::Telemetry::ScalarSet( + mozilla::Telemetry::ScalarID::A11Y_INSTANTIATORS, client); +#endif // defined(MOZ_TELEMETRY_REPORTING) + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::AccessibilityClient, + NS_ConvertUTF16toUTF8(client)); + } + } + + return [super accessibilitySetValue:value forAttribute:attribute]; +} + +@end |