summaryrefslogtreecommitdiffstats
path: root/accessible/mac/Platform.mm
blob: c0200a7e5c11d626765dbde47dfa782150a2942f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
/* 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 "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 ProxyEvent(RemoteAccessible* aProxy, uint32_t aEventType) {
  // Ignore event that we don't escape below, they aren't yet supported.
  if (aEventType != nsIAccessibleEvent::EVENT_ALERT &&
      aEventType != nsIAccessibleEvent::EVENT_FOCUS &&
      aEventType != nsIAccessibleEvent::EVENT_VALUE_CHANGE &&
      aEventType != nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE &&
      aEventType != nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED &&
      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(aProxy);
  if (wrapper) {
    [wrapper handleAccessibleEvent:aEventType];
  }
}

void ProxyStateChangeEvent(RemoteAccessible* aProxy, uint64_t aState,
                           bool aEnabled) {
  mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy);
  if (wrapper) {
    [wrapper stateChanged:aState isEnabled:aEnabled];
  }
}

void ProxyCaretMoveEvent(RemoteAccessible* aTarget, int32_t aOffset,
                         bool aIsSelectionCollapsed, int32_t aGranularity) {
  mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
  MOXTextMarkerDelegate* delegate =
      [MOXTextMarkerDelegate getOrCreateForDoc:aTarget->Document()];
  [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 ProxyTextChangeEvent(RemoteAccessible* aTarget, const nsAString& aStr,
                          int32_t aStart, uint32_t aLen, bool aIsInsert,
                          bool aFromUser) {
  RemoteAccessible* acc = aTarget;
  // If there is a text input ancestor, use it as the event source.
  while (acc && GetTypeFromRole(acc->Role()) != [mozTextAccessible class]) {
    acc = acc->RemoteParent();
  }
  mozAccessible* wrapper = GetNativeFromGeckoAccessible(acc ? acc : aTarget);
  [wrapper handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString(aStr)
                                  inserted:aIsInsert
                               inContainer:aTarget
                                        at:aStart];
}

void ProxyShowHideEvent(RemoteAccessible*, RemoteAccessible*, bool, bool) {}

void ProxySelectionEvent(RemoteAccessible* aTarget, RemoteAccessible* aWidget,
                         uint32_t aEventType) {
  mozAccessible* wrapper = GetNativeFromGeckoAccessible(aWidget);
  if (wrapper) {
    [wrapper handleAccessibleEvent:aEventType];
  }
}

void ProxyTextSelectionChangeEvent(RemoteAccessible* aTarget,
                                   const nsTArray<TextRangeData>& aSelection) {
  if (aSelection.Length()) {
    MOXTextMarkerDelegate* delegate =
        [MOXTextMarkerDelegate getOrCreateForDoc:aTarget->Document()];
    DocAccessibleParent* doc = aTarget->Document();
    RemoteAccessible* startContainer =
        doc->GetAccessible(aSelection[0].StartID());
    RemoteAccessible* endContainer = doc->GetAccessible(aSelection[0].EndID());
    // Cache the selection.
    [delegate setSelectionFrom:startContainer
                            at:aSelection[0].StartOffset()
                            to:endContainer
                            at:aSelection[0].EndOffset()];
  }

  mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
  if (wrapper) {
    [wrapper
        handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED];
  }
}

void ProxyRoleChangedEvent(RemoteAccessible* 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)

- (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 {
          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