mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 13:21:05 +00:00
5318c8b0a3
Differential Revision: https://phabricator.services.mozilla.com/D160836
455 lines
17 KiB
Plaintext
455 lines
17 KiB
Plaintext
/* 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 "MOXSearchInfo.h"
|
|
#import "MOXWebAreaAccessible.h"
|
|
#import "RotorRules.h"
|
|
|
|
#include "nsCocoaUtils.h"
|
|
#include "DocAccessibleParent.h"
|
|
|
|
using namespace mozilla::a11y;
|
|
|
|
@interface MOXSearchInfo ()
|
|
- (NSArray*)getMatchesForRule:(PivotRule&)rule;
|
|
|
|
- (NSArray<mozAccessible*>*)applyPostFilter:(NSArray<mozAccessible*>*)matches;
|
|
|
|
- (Accessible*)rootGeckoAccessible;
|
|
|
|
- (Accessible*)startGeckoAccessible;
|
|
|
|
- (BOOL)shouldApplyPostFilter;
|
|
@end
|
|
|
|
@implementation MOXSearchInfo
|
|
|
|
- (id)initWithParameters:(NSDictionary*)params
|
|
andRoot:(MOXAccessibleBase*)root {
|
|
if (id searchKeyParam = [params objectForKey:@"AXSearchKey"]) {
|
|
mSearchKeys = [searchKeyParam isKindOfClass:[NSString class]]
|
|
? [[NSArray alloc] initWithObjects:searchKeyParam, nil]
|
|
: [searchKeyParam retain];
|
|
}
|
|
|
|
if (id startElemParam = [params objectForKey:@"AXStartElement"]) {
|
|
mStartElem = startElemParam;
|
|
} else {
|
|
mStartElem = root;
|
|
}
|
|
|
|
mRoot = root;
|
|
|
|
mResultLimit = [[params objectForKey:@"AXResultsLimit"] intValue];
|
|
|
|
mSearchForward =
|
|
[[params objectForKey:@"AXDirection"] isEqualToString:@"AXDirectionNext"];
|
|
|
|
mImmediateDescendantsOnly =
|
|
[[params objectForKey:@"AXImmediateDescendantsOnly"] boolValue];
|
|
|
|
mSearchText = [params objectForKey:@"AXSearchText"];
|
|
|
|
return [super init];
|
|
}
|
|
|
|
- (Accessible*)rootGeckoAccessible {
|
|
id root =
|
|
[mRoot isKindOfClass:[mozAccessible class]] ? mRoot : [mRoot moxParent];
|
|
|
|
return [static_cast<mozAccessible*>(root) geckoAccessible];
|
|
}
|
|
|
|
- (Accessible*)startGeckoAccessible {
|
|
if ([mStartElem isKindOfClass:[mozAccessible class]]) {
|
|
return [static_cast<mozAccessible*>(mStartElem) geckoAccessible];
|
|
}
|
|
|
|
// If it isn't a mozAccessible, it doesn't have a gecko accessible
|
|
// this is most likely the root group. Use the gecko doc as the start
|
|
// accessible.
|
|
return [self rootGeckoAccessible];
|
|
}
|
|
|
|
- (NSArray*)getMatchesForRule:(PivotRule&)rule {
|
|
// If we will apply a post-filter, don't limit search so we
|
|
// don't come up short on the final result count.
|
|
int resultLimit = [self shouldApplyPostFilter] ? -1 : mResultLimit;
|
|
|
|
NSMutableArray<mozAccessible*>* matches =
|
|
[[[NSMutableArray alloc] init] autorelease];
|
|
Accessible* geckoRootAcc = [self rootGeckoAccessible];
|
|
Accessible* geckoStartAcc = [self startGeckoAccessible];
|
|
Pivot p = Pivot(geckoRootAcc);
|
|
Accessible* match;
|
|
if (mSearchForward) {
|
|
match = p.Next(geckoStartAcc, rule);
|
|
} else {
|
|
// Search backwards
|
|
if (geckoRootAcc == geckoStartAcc) {
|
|
// If we have no explicit start accessible, start from the last match.
|
|
match = p.Last(rule);
|
|
} else {
|
|
match = p.Prev(geckoStartAcc, rule);
|
|
}
|
|
}
|
|
|
|
while (match && resultLimit != 0) {
|
|
if (!mSearchForward && match == geckoRootAcc) {
|
|
// If searching backwards, don't include root.
|
|
break;
|
|
}
|
|
|
|
// we use mResultLimit != 0 to capture the case where mResultLimit is -1
|
|
// when it is set from the params dictionary. If that's true, we want
|
|
// to return all matches (ie. have no limit)
|
|
mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(match);
|
|
if (nativeMatch) {
|
|
// only add/count results for which there is a matching
|
|
// native accessible
|
|
[matches addObject:nativeMatch];
|
|
resultLimit -= 1;
|
|
}
|
|
|
|
match = mSearchForward ? p.Next(match, rule) : p.Prev(match, rule);
|
|
}
|
|
|
|
return [self applyPostFilter:matches];
|
|
}
|
|
|
|
- (BOOL)shouldApplyPostFilter {
|
|
// We currently only support AXSearchText as a post-search filter.
|
|
// In some cases, VO passes a non-null, empty string for AXSearchText.
|
|
// In that case, we should act as if no AXSearchText was given.
|
|
return !!mSearchText && [mSearchText length] > 0;
|
|
}
|
|
|
|
- (NSArray<mozAccessible*>*)applyPostFilter:(NSArray<mozAccessible*>*)matches {
|
|
if (![self shouldApplyPostFilter]) {
|
|
return matches;
|
|
}
|
|
|
|
NSMutableArray<mozAccessible*>* postMatches =
|
|
[[[NSMutableArray alloc] init] autorelease];
|
|
|
|
nsString searchText;
|
|
nsCocoaUtils::GetStringForNSString(mSearchText, searchText);
|
|
|
|
__block DocAccessibleParent* ipcDoc = nullptr;
|
|
__block nsTArray<uint64_t> accIds;
|
|
|
|
[matches enumerateObjectsUsingBlock:^(mozAccessible* match, NSUInteger idx,
|
|
BOOL* stop) {
|
|
Accessible* geckoAcc = [match geckoAccessible];
|
|
if (!geckoAcc) {
|
|
return;
|
|
}
|
|
|
|
switch (geckoAcc->Role()) {
|
|
case roles::LANDMARK:
|
|
case roles::COMBOBOX:
|
|
case roles::LISTITEM:
|
|
case roles::COMBOBOX_LIST:
|
|
case roles::MENUBAR:
|
|
case roles::MENUPOPUP:
|
|
case roles::DOCUMENT:
|
|
case roles::APPLICATION:
|
|
// XXX: These roles either have AXTitle/AXDescription overridden as
|
|
// empty, or should never be returned in search text results. This
|
|
// should be integrated into a pivot rule in the future, and possibly
|
|
// better mapped somewhere.
|
|
return;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (geckoAcc->IsLocal()) {
|
|
AccessibleWrap* acc = static_cast<AccessibleWrap*>(geckoAcc->AsLocal());
|
|
if (acc->ApplyPostFilter(EWhichPostFilter::eContainsText, searchText)) {
|
|
if (mozAccessible* nativePostMatch =
|
|
GetNativeFromGeckoAccessible(acc)) {
|
|
[postMatches addObject:nativePostMatch];
|
|
if (mResultLimit > 0 &&
|
|
[postMatches count] >= static_cast<NSUInteger>(mResultLimit)) {
|
|
// If we reached the result limit, alter the `stop` pointer to YES
|
|
// to stop iteration.
|
|
*stop = YES;
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
RemoteAccessible* proxy = geckoAcc->AsRemote();
|
|
if (ipcDoc &&
|
|
((ipcDoc != proxy->Document()) || (idx + 1 == [matches count]))) {
|
|
// If the ipcDoc doesn't match the current proxy's doc, we crossed into a
|
|
// new document. ..or this is the last match. Apply the filter on the list
|
|
// of the current ipcDoc.
|
|
nsTArray<uint64_t> matchIds;
|
|
Unused << ipcDoc->GetPlatformExtension()->SendApplyPostSearchFilter(
|
|
accIds, mResultLimit, EWhichPostFilter::eContainsText, searchText,
|
|
&matchIds);
|
|
for (size_t i = 0; i < matchIds.Length(); i++) {
|
|
if (RemoteAccessible* postMatch =
|
|
ipcDoc->GetAccessible(matchIds.ElementAt(i))) {
|
|
if (mozAccessible* nativePostMatch =
|
|
GetNativeFromGeckoAccessible(postMatch)) {
|
|
[postMatches addObject:nativePostMatch];
|
|
if (mResultLimit > 0 &&
|
|
[postMatches count] >= static_cast<NSUInteger>(mResultLimit)) {
|
|
// If we reached the result limit, alter the `stop` pointer to YES
|
|
// to stop iteration.
|
|
*stop = YES;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ipcDoc = nullptr;
|
|
accIds.Clear();
|
|
}
|
|
|
|
if (!ipcDoc) {
|
|
ipcDoc = proxy->Document();
|
|
}
|
|
accIds.AppendElement(proxy->ID());
|
|
}];
|
|
|
|
return postMatches;
|
|
}
|
|
|
|
- (NSArray*)performSearch {
|
|
Accessible* geckoRootAcc = [self rootGeckoAccessible];
|
|
Accessible* geckoStartAcc = [self startGeckoAccessible];
|
|
NSMutableArray* matches = [[[NSMutableArray alloc] init] autorelease];
|
|
for (id key in mSearchKeys) {
|
|
if ([key isEqualToString:@"AXAnyTypeSearchKey"]) {
|
|
RotorRule rule =
|
|
mImmediateDescendantsOnly ? RotorRule(geckoRootAcc) : RotorRule();
|
|
|
|
if ([mStartElem isKindOfClass:[MOXWebAreaAccessible class]]) {
|
|
if (id rootGroup =
|
|
[static_cast<MOXWebAreaAccessible*>(mStartElem) rootGroup]) {
|
|
// Moving forward from web area, rootgroup; if it exists, is next.
|
|
[matches addObject:rootGroup];
|
|
if (mResultLimit == 1) {
|
|
// Found one match, continue in search keys for block.
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mImmediateDescendantsOnly && mStartElem != mRoot &&
|
|
[mStartElem isKindOfClass:[MOXRootGroup class]]) {
|
|
// Moving forward from root group. If we don't match descendants,
|
|
// there is no match. Continue.
|
|
continue;
|
|
}
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXHeadingSearchKey"]) {
|
|
RotorRoleRule rule = mImmediateDescendantsOnly
|
|
? RotorRoleRule(roles::HEADING, geckoRootAcc)
|
|
: RotorRoleRule(roles::HEADING);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXArticleSearchKey"]) {
|
|
RotorRoleRule rule = mImmediateDescendantsOnly
|
|
? RotorRoleRule(roles::ARTICLE, geckoRootAcc)
|
|
: RotorRoleRule(roles::ARTICLE);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXTableSearchKey"]) {
|
|
RotorRoleRule rule = mImmediateDescendantsOnly
|
|
? RotorRoleRule(roles::TABLE, geckoRootAcc)
|
|
: RotorRoleRule(roles::TABLE);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXLandmarkSearchKey"]) {
|
|
RotorRoleRule rule = mImmediateDescendantsOnly
|
|
? RotorRoleRule(roles::LANDMARK, geckoRootAcc)
|
|
: RotorRoleRule(roles::LANDMARK);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXListSearchKey"]) {
|
|
RotorRoleRule rule = mImmediateDescendantsOnly
|
|
? RotorRoleRule(roles::LIST, geckoRootAcc)
|
|
: RotorRoleRule(roles::LIST);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXLinkSearchKey"]) {
|
|
RotorLinkRule rule = mImmediateDescendantsOnly
|
|
? RotorLinkRule(geckoRootAcc)
|
|
: RotorLinkRule();
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXVisitedLinkSearchKey"]) {
|
|
RotorVisitedLinkRule rule = mImmediateDescendantsOnly
|
|
? RotorVisitedLinkRule(geckoRootAcc)
|
|
: RotorVisitedLinkRule();
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXUnvisitedLinkSearchKey"]) {
|
|
RotorUnvisitedLinkRule rule = mImmediateDescendantsOnly
|
|
? RotorUnvisitedLinkRule(geckoRootAcc)
|
|
: RotorUnvisitedLinkRule();
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXButtonSearchKey"]) {
|
|
RotorRoleRule rule = mImmediateDescendantsOnly
|
|
? RotorRoleRule(roles::PUSHBUTTON, geckoRootAcc)
|
|
: RotorRoleRule(roles::PUSHBUTTON);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXControlSearchKey"]) {
|
|
RotorControlRule rule = mImmediateDescendantsOnly
|
|
? RotorControlRule(geckoRootAcc)
|
|
: RotorControlRule();
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXSameTypeSearchKey"]) {
|
|
mozAccessible* native = GetNativeFromGeckoAccessible(geckoStartAcc);
|
|
NSString* macRole = [native moxRole];
|
|
RotorMacRoleRule rule = mImmediateDescendantsOnly
|
|
? RotorMacRoleRule(macRole, geckoRootAcc)
|
|
: RotorMacRoleRule(macRole);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXDifferentTypeSearchKey"]) {
|
|
mozAccessible* native = GetNativeFromGeckoAccessible(geckoStartAcc);
|
|
NSString* macRole = [native moxRole];
|
|
RotorNotMacRoleRule rule =
|
|
mImmediateDescendantsOnly ? RotorNotMacRoleRule(macRole, geckoRootAcc)
|
|
: RotorNotMacRoleRule(macRole);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXRadioGroupSearchKey"]) {
|
|
RotorRoleRule rule = mImmediateDescendantsOnly
|
|
? RotorRoleRule(roles::RADIO_GROUP, geckoRootAcc)
|
|
: RotorRoleRule(roles::RADIO_GROUP);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXFrameSearchKey"]) {
|
|
RotorRoleRule rule = mImmediateDescendantsOnly
|
|
? RotorRoleRule(roles::DOCUMENT, geckoRootAcc)
|
|
: RotorRoleRule(roles::DOCUMENT);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXImageSearchKey"] ||
|
|
[key isEqualToString:@"AXGraphicSearchKey"]) {
|
|
RotorRoleRule rule = mImmediateDescendantsOnly
|
|
? RotorRoleRule(roles::GRAPHIC, geckoRootAcc)
|
|
: RotorRoleRule(roles::GRAPHIC);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXCheckBoxSearchKey"]) {
|
|
RotorRoleRule rule = mImmediateDescendantsOnly
|
|
? RotorRoleRule(roles::CHECKBUTTON, geckoRootAcc)
|
|
: RotorRoleRule(roles::CHECKBUTTON);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXStaticTextSearchKey"]) {
|
|
RotorStaticTextRule rule = mImmediateDescendantsOnly
|
|
? RotorStaticTextRule(geckoRootAcc)
|
|
: RotorStaticTextRule();
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXHeadingLevel1SearchKey"]) {
|
|
RotorHeadingLevelRule rule = mImmediateDescendantsOnly
|
|
? RotorHeadingLevelRule(1, geckoRootAcc)
|
|
: RotorHeadingLevelRule(1);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXHeadingLevel2SearchKey"]) {
|
|
RotorHeadingLevelRule rule = mImmediateDescendantsOnly
|
|
? RotorHeadingLevelRule(2, geckoRootAcc)
|
|
: RotorHeadingLevelRule(2);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXHeadingLevel3SearchKey"]) {
|
|
RotorHeadingLevelRule rule = mImmediateDescendantsOnly
|
|
? RotorHeadingLevelRule(3, geckoRootAcc)
|
|
: RotorHeadingLevelRule(3);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXHeadingLevel4SearchKey"]) {
|
|
RotorHeadingLevelRule rule = mImmediateDescendantsOnly
|
|
? RotorHeadingLevelRule(4, geckoRootAcc)
|
|
: RotorHeadingLevelRule(4);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXHeadingLevel5SearchKey"]) {
|
|
RotorHeadingLevelRule rule = mImmediateDescendantsOnly
|
|
? RotorHeadingLevelRule(5, geckoRootAcc)
|
|
: RotorHeadingLevelRule(5);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXHeadingLevel6SearchKey"]) {
|
|
RotorHeadingLevelRule rule = mImmediateDescendantsOnly
|
|
? RotorHeadingLevelRule(6, geckoRootAcc)
|
|
: RotorHeadingLevelRule(6);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXBlockquoteSearchKey"]) {
|
|
RotorRoleRule rule = mImmediateDescendantsOnly
|
|
? RotorRoleRule(roles::BLOCKQUOTE, geckoRootAcc)
|
|
: RotorRoleRule(roles::BLOCKQUOTE);
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXTextFieldSearchKey"]) {
|
|
RotorTextEntryRule rule = mImmediateDescendantsOnly
|
|
? RotorTextEntryRule(geckoRootAcc)
|
|
: RotorTextEntryRule();
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
|
|
if ([key isEqualToString:@"AXLiveRegionSearchKey"]) {
|
|
RotorLiveRegionRule rule = mImmediateDescendantsOnly
|
|
? RotorLiveRegionRule(geckoRootAcc)
|
|
: RotorLiveRegionRule();
|
|
[matches addObjectsFromArray:[self getMatchesForRule:rule]];
|
|
}
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[mSearchKeys release];
|
|
[super dealloc];
|
|
}
|
|
|
|
@end
|