2 /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
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/. */
8 #import "MOXSearchInfo.h"
9 #import "MOXWebAreaAccessible.h"
10 #import "RotorRules.h"
12 #include "nsCocoaUtils.h"
13 #include "DocAccessibleParent.h"
15 using namespace mozilla::a11y;
17 @interface MOXSearchInfo ()
18 - (NSArray*)getMatchesForRule:(PivotRule&)rule;
20 - (NSArray<mozAccessible*>*)applyPostFilter:(NSArray<mozAccessible*>*)matches;
22 - (Accessible*)rootGeckoAccessible;
24 - (Accessible*)startGeckoAccessible;
26 - (BOOL)shouldApplyPostFilter;
29 @implementation MOXSearchInfo
31 - (id)initWithParameters:(NSDictionary*)params
32 andRoot:(MOXAccessibleBase*)root {
33 if (id searchKeyParam = [params objectForKey:@"AXSearchKey"]) {
34 mSearchKeys = [searchKeyParam isKindOfClass:[NSString class]]
35 ? [[NSArray alloc] initWithObjects:searchKeyParam, nil]
36 : [searchKeyParam retain];
39 if (id startElemParam = [params objectForKey:@"AXStartElement"]) {
40 mStartElem = startElemParam;
47 mResultLimit = [[params objectForKey:@"AXResultsLimit"] intValue];
50 [[params objectForKey:@"AXDirection"] isEqualToString:@"AXDirectionNext"];
52 mImmediateDescendantsOnly =
53 [[params objectForKey:@"AXImmediateDescendantsOnly"] boolValue];
55 mSearchText = [params objectForKey:@"AXSearchText"];
60 - (Accessible*)rootGeckoAccessible {
62 [mRoot isKindOfClass:[mozAccessible class]] ? mRoot : [mRoot moxParent];
64 return [static_cast<mozAccessible*>(root) geckoAccessible];
67 - (Accessible*)startGeckoAccessible {
68 if ([mStartElem isKindOfClass:[mozAccessible class]]) {
69 return [static_cast<mozAccessible*>(mStartElem) geckoAccessible];
72 // If it isn't a mozAccessible, it doesn't have a gecko accessible
73 // this is most likely the root group. Use the gecko doc as the start
75 return [self rootGeckoAccessible];
78 - (NSArray*)getMatchesForRule:(PivotRule&)rule {
79 // If we will apply a post-filter, don't limit search so we
80 // don't come up short on the final result count.
81 int resultLimit = [self shouldApplyPostFilter] ? -1 : mResultLimit;
83 NSMutableArray<mozAccessible*>* matches =
84 [[[NSMutableArray alloc] init] autorelease];
85 Accessible* geckoRootAcc = [self rootGeckoAccessible];
86 Accessible* geckoStartAcc = [self startGeckoAccessible];
87 Pivot p = Pivot(geckoRootAcc);
90 match = p.Next(geckoStartAcc, rule);
93 if (geckoRootAcc == geckoStartAcc) {
94 // If we have no explicit start accessible, start from the last match.
97 match = p.Prev(geckoStartAcc, rule);
101 while (match && resultLimit != 0) {
102 if (!mSearchForward && match == geckoRootAcc) {
103 // If searching backwards, don't include root.
107 // we use mResultLimit != 0 to capture the case where mResultLimit is -1
108 // when it is set from the params dictionary. If that's true, we want
109 // to return all matches (ie. have no limit)
110 mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(match);
112 // only add/count results for which there is a matching
114 [matches addObject:nativeMatch];
118 match = mSearchForward ? p.Next(match, rule) : p.Prev(match, rule);
121 return [self applyPostFilter:matches];
124 - (BOOL)shouldApplyPostFilter {
125 // We currently only support AXSearchText as a post-search filter.
126 return !!mSearchText;
129 - (NSArray<mozAccessible*>*)applyPostFilter:(NSArray<mozAccessible*>*)matches {
130 if (![self shouldApplyPostFilter]) {
134 NSMutableArray<mozAccessible*>* postMatches =
135 [[[NSMutableArray alloc] init] autorelease];
138 nsCocoaUtils::GetStringForNSString(mSearchText, searchText);
140 __block DocAccessibleParent* ipcDoc = nullptr;
141 __block nsTArray<uint64_t> accIds;
143 [matches enumerateObjectsUsingBlock:^(mozAccessible* match, NSUInteger idx,
145 Accessible* geckoAcc = [match geckoAccessible];
150 switch (geckoAcc->Role()) {
151 case roles::LANDMARK:
152 case roles::COMBOBOX:
153 case roles::LISTITEM:
154 case roles::COMBOBOX_LIST:
156 case roles::MENUPOPUP:
157 case roles::DOCUMENT:
158 case roles::APPLICATION:
159 // XXX: These roles either have AXTitle/AXDescription overridden as
160 // empty, or should never be returned in search text results. This
161 // should be integrated into a pivot rule in the future, and possibly
162 // better mapped somewhere.
168 if (geckoAcc->IsLocal()) {
169 AccessibleWrap* acc = static_cast<AccessibleWrap*>(geckoAcc->AsLocal());
170 if (acc->ApplyPostFilter(EWhichPostFilter::eContainsText, searchText)) {
171 if (mozAccessible* nativePostMatch =
172 GetNativeFromGeckoAccessible(acc)) {
173 [postMatches addObject:nativePostMatch];
174 if (mResultLimit > 0 &&
175 [postMatches count] >= static_cast<NSUInteger>(mResultLimit)) {
176 // If we reached the result limit, alter the `stop` pointer to YES
177 // to stop iteration.
186 RemoteAccessible* proxy = geckoAcc->AsRemote();
188 ((ipcDoc != proxy->Document()) || (idx + 1 == [matches count]))) {
189 // If the ipcDoc doesn't match the current proxy's doc, we crossed into a
190 // new document. ..or this is the last match. Apply the filter on the list
191 // of the current ipcDoc.
192 nsTArray<uint64_t> matchIds;
193 Unused << ipcDoc->GetPlatformExtension()->SendApplyPostSearchFilter(
194 accIds, mResultLimit, EWhichPostFilter::eContainsText, searchText,
196 for (size_t i = 0; i < matchIds.Length(); i++) {
197 if (RemoteAccessible* postMatch =
198 ipcDoc->GetAccessible(matchIds.ElementAt(i))) {
199 if (mozAccessible* nativePostMatch =
200 GetNativeFromGeckoAccessible(postMatch)) {
201 [postMatches addObject:nativePostMatch];
202 if (mResultLimit > 0 &&
203 [postMatches count] >= static_cast<NSUInteger>(mResultLimit)) {
204 // If we reached the result limit, alter the `stop` pointer to YES
205 // to stop iteration.
218 ipcDoc = proxy->Document();
220 accIds.AppendElement(proxy->ID());
226 - (NSArray*)performSearch {
227 Accessible* geckoRootAcc = [self rootGeckoAccessible];
228 Accessible* geckoStartAcc = [self startGeckoAccessible];
229 NSMutableArray* matches = [[[NSMutableArray alloc] init] autorelease];
230 for (id key in mSearchKeys) {
231 if ([key isEqualToString:@"AXAnyTypeSearchKey"]) {
233 mImmediateDescendantsOnly ? RotorRule(geckoRootAcc) : RotorRule();
235 if ([mStartElem isKindOfClass:[MOXWebAreaAccessible class]]) {
237 [static_cast<MOXWebAreaAccessible*>(mStartElem) rootGroup]) {
238 // Moving forward from web area, rootgroup; if it exists, is next.
239 [matches addObject:rootGroup];
240 if (mResultLimit == 1) {
241 // Found one match, continue in search keys for block.
247 if (mImmediateDescendantsOnly && mStartElem != mRoot &&
248 [mStartElem isKindOfClass:[MOXRootGroup class]]) {
249 // Moving forward from root group. If we don't match descendants,
250 // there is no match. Continue.
253 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
256 if ([key isEqualToString:@"AXHeadingSearchKey"]) {
257 RotorRoleRule rule = mImmediateDescendantsOnly
258 ? RotorRoleRule(roles::HEADING, geckoRootAcc)
259 : RotorRoleRule(roles::HEADING);
260 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
263 if ([key isEqualToString:@"AXArticleSearchKey"]) {
264 RotorRoleRule rule = mImmediateDescendantsOnly
265 ? RotorRoleRule(roles::ARTICLE, geckoRootAcc)
266 : RotorRoleRule(roles::ARTICLE);
267 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
270 if ([key isEqualToString:@"AXTableSearchKey"]) {
271 RotorRoleRule rule = mImmediateDescendantsOnly
272 ? RotorRoleRule(roles::TABLE, geckoRootAcc)
273 : RotorRoleRule(roles::TABLE);
274 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
277 if ([key isEqualToString:@"AXLandmarkSearchKey"]) {
278 RotorRoleRule rule = mImmediateDescendantsOnly
279 ? RotorRoleRule(roles::LANDMARK, geckoRootAcc)
280 : RotorRoleRule(roles::LANDMARK);
281 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
284 if ([key isEqualToString:@"AXListSearchKey"]) {
285 RotorRoleRule rule = mImmediateDescendantsOnly
286 ? RotorRoleRule(roles::LIST, geckoRootAcc)
287 : RotorRoleRule(roles::LIST);
288 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
291 if ([key isEqualToString:@"AXLinkSearchKey"]) {
292 RotorLinkRule rule = mImmediateDescendantsOnly
293 ? RotorLinkRule(geckoRootAcc)
295 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
298 if ([key isEqualToString:@"AXVisitedLinkSearchKey"]) {
299 RotorVisitedLinkRule rule = mImmediateDescendantsOnly
300 ? RotorVisitedLinkRule(geckoRootAcc)
301 : RotorVisitedLinkRule();
302 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
305 if ([key isEqualToString:@"AXUnvisitedLinkSearchKey"]) {
306 RotorUnvisitedLinkRule rule = mImmediateDescendantsOnly
307 ? RotorUnvisitedLinkRule(geckoRootAcc)
308 : RotorUnvisitedLinkRule();
309 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
312 if ([key isEqualToString:@"AXButtonSearchKey"]) {
313 RotorRoleRule rule = mImmediateDescendantsOnly
314 ? RotorRoleRule(roles::PUSHBUTTON, geckoRootAcc)
315 : RotorRoleRule(roles::PUSHBUTTON);
316 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
319 if ([key isEqualToString:@"AXControlSearchKey"]) {
320 RotorControlRule rule = mImmediateDescendantsOnly
321 ? RotorControlRule(geckoRootAcc)
322 : RotorControlRule();
323 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
326 if ([key isEqualToString:@"AXSameTypeSearchKey"]) {
327 mozAccessible* native = GetNativeFromGeckoAccessible(geckoStartAcc);
328 NSString* macRole = [native moxRole];
329 RotorMacRoleRule rule = mImmediateDescendantsOnly
330 ? RotorMacRoleRule(macRole, geckoRootAcc)
331 : RotorMacRoleRule(macRole);
332 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
335 if ([key isEqualToString:@"AXDifferentTypeSearchKey"]) {
336 mozAccessible* native = GetNativeFromGeckoAccessible(geckoStartAcc);
337 NSString* macRole = [native moxRole];
338 RotorNotMacRoleRule rule =
339 mImmediateDescendantsOnly ? RotorNotMacRoleRule(macRole, geckoRootAcc)
340 : RotorNotMacRoleRule(macRole);
341 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
344 if ([key isEqualToString:@"AXRadioGroupSearchKey"]) {
345 RotorRoleRule rule = mImmediateDescendantsOnly
346 ? RotorRoleRule(roles::RADIO_GROUP, geckoRootAcc)
347 : RotorRoleRule(roles::RADIO_GROUP);
348 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
351 if ([key isEqualToString:@"AXFrameSearchKey"]) {
352 RotorRoleRule rule = mImmediateDescendantsOnly
353 ? RotorRoleRule(roles::DOCUMENT, geckoRootAcc)
354 : RotorRoleRule(roles::DOCUMENT);
355 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
358 if ([key isEqualToString:@"AXImageSearchKey"] ||
359 [key isEqualToString:@"AXGraphicSearchKey"]) {
360 RotorRoleRule rule = mImmediateDescendantsOnly
361 ? RotorRoleRule(roles::GRAPHIC, geckoRootAcc)
362 : RotorRoleRule(roles::GRAPHIC);
363 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
366 if ([key isEqualToString:@"AXCheckBoxSearchKey"]) {
367 RotorRoleRule rule = mImmediateDescendantsOnly
368 ? RotorRoleRule(roles::CHECKBUTTON, geckoRootAcc)
369 : RotorRoleRule(roles::CHECKBUTTON);
370 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
373 if ([key isEqualToString:@"AXStaticTextSearchKey"]) {
374 RotorStaticTextRule rule = mImmediateDescendantsOnly
375 ? RotorStaticTextRule(geckoRootAcc)
376 : RotorStaticTextRule();
377 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
380 if ([key isEqualToString:@"AXHeadingLevel1SearchKey"]) {
381 RotorHeadingLevelRule rule = mImmediateDescendantsOnly
382 ? RotorHeadingLevelRule(1, geckoRootAcc)
383 : RotorHeadingLevelRule(1);
384 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
387 if ([key isEqualToString:@"AXHeadingLevel2SearchKey"]) {
388 RotorHeadingLevelRule rule = mImmediateDescendantsOnly
389 ? RotorHeadingLevelRule(2, geckoRootAcc)
390 : RotorHeadingLevelRule(2);
391 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
394 if ([key isEqualToString:@"AXHeadingLevel3SearchKey"]) {
395 RotorHeadingLevelRule rule = mImmediateDescendantsOnly
396 ? RotorHeadingLevelRule(3, geckoRootAcc)
397 : RotorHeadingLevelRule(3);
398 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
401 if ([key isEqualToString:@"AXHeadingLevel4SearchKey"]) {
402 RotorHeadingLevelRule rule = mImmediateDescendantsOnly
403 ? RotorHeadingLevelRule(4, geckoRootAcc)
404 : RotorHeadingLevelRule(4);
405 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
408 if ([key isEqualToString:@"AXHeadingLevel5SearchKey"]) {
409 RotorHeadingLevelRule rule = mImmediateDescendantsOnly
410 ? RotorHeadingLevelRule(5, geckoRootAcc)
411 : RotorHeadingLevelRule(5);
412 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
415 if ([key isEqualToString:@"AXHeadingLevel6SearchKey"]) {
416 RotorHeadingLevelRule rule = mImmediateDescendantsOnly
417 ? RotorHeadingLevelRule(6, geckoRootAcc)
418 : RotorHeadingLevelRule(6);
419 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
422 if ([key isEqualToString:@"AXBlockquoteSearchKey"]) {
423 RotorRoleRule rule = mImmediateDescendantsOnly
424 ? RotorRoleRule(roles::BLOCKQUOTE, geckoRootAcc)
425 : RotorRoleRule(roles::BLOCKQUOTE);
426 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
429 if ([key isEqualToString:@"AXTextFieldSearchKey"]) {
430 RotorTextEntryRule rule = mImmediateDescendantsOnly
431 ? RotorTextEntryRule(geckoRootAcc)
432 : RotorTextEntryRule();
433 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
436 if ([key isEqualToString:@"AXLiveRegionSearchKey"]) {
437 RotorLiveRegionRule rule = mImmediateDescendantsOnly
438 ? RotorLiveRegionRule(geckoRootAcc)
439 : RotorLiveRegionRule();
440 [matches addObjectsFromArray:[self getMatchesForRule:rule]];
448 [mSearchKeys release];