Platform.mm (13576B)
1 /* clang-format off */ 2 /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 3 /* clang-format on */ 4 /* This Source Code Form is subject to the terms of the Mozilla Public 5 * License, v. 2.0. If a copy of the MPL was not distributed with this 6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 7 8 #import <Cocoa/Cocoa.h> 9 10 #import "MOXTextMarkerDelegate.h" 11 12 #include "Platform.h" 13 #include "RemoteAccessible.h" 14 #include "DocAccessibleParent.h" 15 #include "mozTableAccessible.h" 16 #include "mozTextAccessible.h" 17 #include "MOXOuterDoc.h" 18 #include "MOXWebAreaAccessible.h" 19 #include "nsAccUtils.h" 20 #include "TextRange.h" 21 22 #include "nsAppShell.h" 23 #include "nsCocoaUtils.h" 24 #include "mozilla/EnumSet.h" 25 #include "mozilla/glean/AccessibleMetrics.h" 26 27 // Available from 10.13 onwards; test availability at runtime before using 28 @interface NSWorkspace (AvailableSinceHighSierra) 29 @property(readonly) BOOL isVoiceOverEnabled; 30 @property(readonly) BOOL isSwitchControlEnabled; 31 @end 32 33 namespace mozilla { 34 namespace a11y { 35 36 // Mac a11y whitelisting 37 static bool sA11yShouldBeEnabled = false; 38 39 bool ShouldA11yBeEnabled() { 40 EPlatformDisabledState disabledState = PlatformDisabledState(); 41 return (disabledState == ePlatformIsForceEnabled) || 42 ((disabledState == ePlatformIsEnabled) && sA11yShouldBeEnabled); 43 } 44 45 void PlatformInit() {} 46 47 void PlatformShutdown() {} 48 49 void ProxyCreated(RemoteAccessible* aProxy) { 50 if (aProxy->Role() == roles::WHITESPACE) { 51 // We don't create a native object if we're child of a "flat" accessible; 52 // for example, on OS X buttons shouldn't have any children, because that 53 // makes the OS confused. We also don't create accessibles for <br> 54 // (whitespace) elements. 55 return; 56 } 57 58 // Pass in dummy state for now as retrieving proxy state requires IPC. 59 // Note that we can use RemoteAccessible::IsTable* functions here because they 60 // do not use IPC calls but that might change after bug 1210477. 61 Class type; 62 if (aProxy->IsTable()) { 63 type = [mozTableAccessible class]; 64 } else if (aProxy->IsTableRow()) { 65 type = [mozTableRowAccessible class]; 66 } else if (aProxy->IsTableCell()) { 67 type = [mozTableCellAccessible class]; 68 } else if (aProxy->IsDoc()) { 69 type = [MOXWebAreaAccessible class]; 70 } else if (aProxy->IsOuterDoc()) { 71 type = [MOXOuterDoc class]; 72 } else { 73 type = GetTypeFromRole(aProxy->Role()); 74 } 75 76 mozAccessible* mozWrapper = [[type alloc] initWithAccessible:aProxy]; 77 aProxy->SetWrapper(reinterpret_cast<uintptr_t>(mozWrapper)); 78 } 79 80 void ProxyDestroyed(RemoteAccessible* aProxy) { 81 mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy); 82 [wrapper expire]; 83 [wrapper release]; 84 aProxy->SetWrapper(0); 85 86 if (aProxy->IsDoc()) { 87 [MOXTextMarkerDelegate destroyForDoc:aProxy]; 88 } 89 } 90 91 void PlatformEvent(Accessible* aTarget, uint32_t aEventType) { 92 // Ignore event that we don't escape below, they aren't yet supported. 93 if (aEventType != nsIAccessibleEvent::EVENT_ALERT && 94 aEventType != nsIAccessibleEvent::EVENT_VALUE_CHANGE && 95 aEventType != nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE && 96 aEventType != nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE && 97 aEventType != nsIAccessibleEvent::EVENT_REORDER && 98 aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED && 99 aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED && 100 aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_CHANGED && 101 aEventType != nsIAccessibleEvent::EVENT_NAME_CHANGE && 102 aEventType != nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED && 103 aEventType != nsIAccessibleEvent::EVENT_ERRORMESSAGE_CHANGED) { 104 return; 105 } 106 107 mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); 108 if (wrapper) { 109 [wrapper handleAccessibleEvent:aEventType]; 110 } 111 } 112 113 void PlatformStateChangeEvent(Accessible* aTarget, uint64_t aState, 114 bool aEnabled) { 115 mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); 116 if (wrapper) { 117 [wrapper stateChanged:aState isEnabled:aEnabled]; 118 } 119 } 120 121 void PlatformFocusEvent(Accessible* aTarget) { 122 if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) { 123 [wrapper handleAccessibleEvent:nsIAccessibleEvent::EVENT_FOCUS]; 124 } 125 } 126 127 void PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset, 128 bool aIsSelectionCollapsed, int32_t aGranularity, 129 bool aFromUser) { 130 mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); 131 MOXTextMarkerDelegate* delegate = [MOXTextMarkerDelegate 132 getOrCreateForDoc:nsAccUtils::DocumentFor(aTarget)]; 133 [delegate setCaretOffset:aTarget at:aOffset moveGranularity:aGranularity]; 134 if (aIsSelectionCollapsed) { 135 // If selection is collapsed, invalidate selection. 136 [delegate setSelectionFrom:aTarget at:aOffset to:aTarget at:aOffset]; 137 } 138 139 if (wrapper) { 140 if (mozAccessible* editable = [wrapper moxEditableAncestor]) { 141 [editable 142 handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED]; 143 } else { 144 [wrapper 145 handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED]; 146 } 147 } 148 } 149 150 void PlatformTextChangeEvent(Accessible* aTarget, const nsAString& aStr, 151 int32_t aStart, uint32_t aLen, bool aIsInsert, 152 bool aFromUser) { 153 mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); 154 if (wrapper) { 155 if (mozAccessible* editable = [wrapper moxEditableAncestor]) { 156 [editable handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString(aStr) 157 inserted:aIsInsert 158 inContainer:aTarget 159 at:aStart]; 160 } else { 161 [wrapper maybePostValidationErrorChanged]; 162 } 163 } 164 } 165 166 void PlatformShowHideEvent(Accessible*, Accessible*, bool, bool) {} 167 168 void PlatformSelectionEvent(Accessible* aTarget, Accessible* aWidget, 169 uint32_t aEventType) { 170 mozAccessible* wrapper = GetNativeFromGeckoAccessible(aWidget); 171 if (wrapper) { 172 [wrapper handleAccessibleEvent:aEventType]; 173 } 174 } 175 176 void PlatformTextSelectionChangeEvent(Accessible* aTarget, 177 const nsTArray<TextRange>& aSelection) { 178 if (aSelection.Length()) { 179 MOXTextMarkerDelegate* delegate = [MOXTextMarkerDelegate 180 getOrCreateForDoc:nsAccUtils::DocumentFor(aTarget)]; 181 // Cache the selection. 182 [delegate setSelectionFrom:aSelection[0].StartContainer() 183 at:aSelection[0].StartOffset() 184 to:aSelection[0].EndContainer() 185 at:aSelection[0].EndOffset()]; 186 } 187 188 mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget); 189 if (wrapper) { 190 [wrapper 191 handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED]; 192 } 193 } 194 195 void PlatformRoleChangedEvent(Accessible* aTarget, const a11y::role& aRole, 196 uint8_t aRoleMapEntryIndex) { 197 if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) { 198 [wrapper handleRoleChanged:aRole]; 199 } 200 } 201 202 // This enum lists possible assistive technology clients. It's intended for use 203 // in an EnumSet since there can be multiple ATs active at once. 204 enum class Client : uint64_t { 205 Unknown, 206 VoiceOver, 207 SwitchControl, 208 FullKeyboardAccess, 209 VoiceControl, 210 SpeakSelection, 211 SpeakItemUnderMouse, 212 SpeakTypingFeedback, 213 HoverText 214 }; 215 216 // Get the set of currently-active clients and the client to log. 217 // XXX: We should log all clients, but default to the first one encountered. 218 std::pair<EnumSet<Client>, Client> GetClients() { 219 EnumSet<Client> clients; 220 std::optional<Client> clientToLog; 221 auto AddClient = [&clients, &clientToLog](Client client) { 222 clients += client; 223 if (!clientToLog.has_value()) { 224 clientToLog = client; 225 } 226 }; 227 if ([[NSWorkspace sharedWorkspace] 228 respondsToSelector:@selector(isVoiceOverEnabled)] && 229 [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]) { 230 AddClient(Client::VoiceOver); 231 } else if ([[NSWorkspace sharedWorkspace] 232 respondsToSelector:@selector(isSwitchControlEnabled)] && 233 [[NSWorkspace sharedWorkspace] isSwitchControlEnabled]) { 234 AddClient(Client::SwitchControl); 235 } else { 236 Boolean foundSpecificClient = false; 237 238 // This is more complicated than the NSWorkspace queries above 239 // because (a) there is no "full keyboard access" query for NSWorkspace 240 // and (b) the [NSApplication fullKeyboardAccessEnabled] query checks 241 // the pre-Monterey version of full keyboard access, which is not what 242 // we're looking for here. For more info, see bug 1772375 comment 7. 243 Boolean exists; 244 long val = CFPreferencesGetAppIntegerValue( 245 CFSTR("FullKeyboardAccessEnabled"), CFSTR("com.apple.Accessibility"), 246 &exists); 247 if (exists && val == 1) { 248 foundSpecificClient = true; 249 AddClient(Client::FullKeyboardAccess); 250 } 251 252 val = CFPreferencesGetAppIntegerValue(CFSTR("CommandAndControlEnabled"), 253 CFSTR("com.apple.Accessibility"), 254 &exists); 255 if (exists && val == 1) { 256 foundSpecificClient = true; 257 AddClient(Client::VoiceControl); 258 } 259 260 val = CFPreferencesGetAppIntegerValue( 261 CFSTR("SpeakThisEnabled"), CFSTR("com.apple.universalaccess"), &exists); 262 if (exists && val == 1) { 263 foundSpecificClient = true; 264 AddClient(Client::SpeakSelection); 265 } 266 267 val = CFPreferencesGetAppIntegerValue(CFSTR("speakItemUnderMouseEnabled"), 268 CFSTR("com.apple.universalaccess"), 269 &exists); 270 if (exists && val == 1) { 271 foundSpecificClient = true; 272 AddClient(Client::SpeakItemUnderMouse); 273 } 274 275 val = CFPreferencesGetAppIntegerValue(CFSTR("typingEchoEnabled"), 276 CFSTR("com.apple.universalaccess"), 277 &exists); 278 if (exists && val == 1) { 279 foundSpecificClient = true; 280 AddClient(Client::SpeakTypingFeedback); 281 } 282 283 val = CFPreferencesGetAppIntegerValue( 284 CFSTR("hoverTextEnabled"), CFSTR("com.apple.universalaccess"), &exists); 285 if (exists && val == 1) { 286 foundSpecificClient = true; 287 AddClient(Client::HoverText); 288 } 289 290 if (!foundSpecificClient) { 291 AddClient(Client::Unknown); 292 } 293 } 294 return std::make_pair(clients, clientToLog.value()); 295 } 296 297 // Expects a single client, returns a string representation of that client. 298 constexpr const char* GetStringForClient(Client aClient) { 299 switch (aClient) { 300 case Client::Unknown: 301 return "Unknown"; 302 case Client::VoiceOver: 303 return "VoiceOver"; 304 case Client::SwitchControl: 305 return "SwitchControl"; 306 case Client::FullKeyboardAccess: 307 return "FullKeyboardAccess"; 308 case Client::VoiceControl: 309 return "VoiceControl"; 310 case Client::SpeakSelection: 311 return "SpeakSelection"; 312 case Client::SpeakItemUnderMouse: 313 return "SpeakItemUnderMouse"; 314 case Client::SpeakTypingFeedback: 315 return "SpeakTypingFeedback"; 316 case Client::HoverText: 317 return "HoverText"; 318 default: 319 break; 320 } 321 MOZ_ASSERT_UNREACHABLE("Unknown Client enum value!"); 322 return ""; 323 } 324 325 uint64_t GetCacheDomainsForKnownClients(uint64_t aCacheDomains) { 326 auto [clients, _] = GetClients(); 327 // We expect VoiceOver will require all information we have. 328 if (clients.contains(Client::VoiceOver)) { 329 return CacheDomain::All; 330 } 331 if (clients.contains(Client::FullKeyboardAccess)) { 332 aCacheDomains |= CacheDomain::Bounds; 333 } 334 if (clients.contains(Client::SwitchControl)) { 335 // XXX: Find minimum set of domains required for SwitchControl. 336 // SwitchControl can give up if we don't furnish it certain information. 337 return CacheDomain::All; 338 } 339 if (clients.contains(Client::VoiceControl)) { 340 // XXX: Find minimum set of domains required for VoiceControl. 341 return CacheDomain::All; 342 } 343 return aCacheDomains; 344 } 345 346 } // namespace a11y 347 } // namespace mozilla 348 349 @interface GeckoNSApplication (a11y) 350 - (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute; 351 @end 352 353 @implementation GeckoNSApplication (a11y) 354 355 - (NSAccessibilityRole)accessibilityRole { 356 // For ATs that don't request `AXEnhancedUserInterface` we need to enable 357 // accessibility when a role is fetched. Not ideal, but this is needed 358 // for such services as Voice Control. 359 if (!mozilla::a11y::sA11yShouldBeEnabled) { 360 [self accessibilitySetValue:@YES forAttribute:@"AXEnhancedUserInterface"]; 361 } 362 return [super accessibilityRole]; 363 } 364 365 - (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute { 366 if ([attribute isEqualToString:@"AXEnhancedUserInterface"]) { 367 mozilla::a11y::sA11yShouldBeEnabled = ([value intValue] == 1); 368 if (sA11yShouldBeEnabled) { 369 // If accessibility should be enabled, log the appropriate client 370 auto [_, clientToLog] = GetClients(); 371 const char* client = GetStringForClient(clientToLog); 372 373 #if defined(MOZ_TELEMETRY_REPORTING) 374 mozilla::glean::a11y::instantiators.Set(nsDependentCString(client)); 375 #endif // defined(MOZ_TELEMETRY_REPORTING) 376 CrashReporter::RecordAnnotationCString( 377 CrashReporter::Annotation::AccessibilityClient, client); 378 } 379 } 380 381 return [super accessibilitySetValue:value forAttribute:attribute]; 382 } 383 384 @end