Skip to content

Commit 8d57e6f

Browse files
[Windows] Ignore case optionally in AXPlatformNodeTextRangeProviderWin::FindText (flutter#39922)
When `ignore_case` is `true`, convert needle and haystack strings to lowercase before performing search, flutter/flutter#117013 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide] and the [C++, Objective-C, Java style guides]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. See [testing the engine] for instructions on writing and running engine tests. - [x] I updated/added relevant documentation (doc comments with `///`). - [ ] I signed the [CLA]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style [testing the engine]: https://github.com/flutter/flutter/wiki/Testing-the-engine [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat
1 parent 3f4305d commit 8d57e6f

File tree

4 files changed

+124
-45
lines changed

4 files changed

+124
-45
lines changed

third_party/accessibility/ax/BUILD.gn

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@ source_set("ax") {
106106
"oleacc.lib",
107107
"uiautomationcore.lib",
108108
]
109-
deps = [ "//flutter/fml:string_conversion" ]
109+
deps = [
110+
"//flutter/fml:fml",
111+
"//third_party/icu:icui18n",
112+
]
110113
}
111114

112115
public_deps = [

third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.cc

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include <UIAutomation.h>
88
#include <wrl/client.h>
9+
#include <string_view>
910

1011
#include "ax/ax_action_data.h"
1112
#include "ax/ax_range.h"
@@ -14,6 +15,7 @@
1415
#include "ax/platform/ax_platform_tree_manager.h"
1516
#include "base/win/variant_vector.h"
1617
#include "flutter/fml/platform/win/wstring_conversion.h"
18+
#include "third_party/icu/source/i18n/unicode/usearch.h"
1719

1820
#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL() \
1921
if (!GetOwner() || !GetOwner()->GetDelegate() || !start() || \
@@ -433,28 +435,63 @@ HRESULT AXPlatformNodeTextRangeProviderWin::FindAttributeRange(
433435
return S_OK;
434436
}
435437

436-
static bool StringSearch(const std::u16string& search_string,
437-
const std::u16string& find_in,
438-
size_t* find_start,
439-
size_t* find_length,
440-
bool ignore_case,
441-
bool backwards) {
442-
// TODO(schectman) Respect ignore_case/i18n.
443-
// https://github.com/flutter/flutter/issues/117013
444-
size_t match_pos;
445-
if (backwards) {
446-
match_pos = find_in.rfind(search_string);
447-
} else {
448-
match_pos = find_in.find(search_string);
449-
}
450-
if (match_pos == std::u16string::npos) {
438+
static bool StringSearchBasic(const std::u16string_view search_string,
439+
const std::u16string_view find_in,
440+
size_t* find_start,
441+
size_t* find_length,
442+
bool backwards) {
443+
size_t index =
444+
backwards ? find_in.rfind(search_string) : find_in.find(search_string);
445+
if (index == std::u16string::npos) {
451446
return false;
452447
}
453-
*find_start = match_pos;
454-
*find_length = search_string.length();
448+
*find_start = index;
449+
*find_length = search_string.size();
455450
return true;
456451
}
457452

453+
bool StringSearch(std::u16string_view search_string,
454+
std::u16string_view find_in,
455+
size_t* find_start,
456+
size_t* find_length,
457+
bool ignore_case,
458+
bool backwards) {
459+
UErrorCode status = U_ZERO_ERROR;
460+
UCollator* col = ucol_open(uloc_getDefault(), &status);
461+
UStringSearch* search = usearch_openFromCollator(
462+
search_string.data(), search_string.size(), find_in.data(),
463+
find_in.size(), col, nullptr, &status);
464+
if (!U_SUCCESS(status)) {
465+
if (search) {
466+
usearch_close(search);
467+
}
468+
return StringSearchBasic(search_string, find_in, find_start, find_length,
469+
backwards);
470+
}
471+
UCollator* collator = usearch_getCollator(search);
472+
ucol_setStrength(collator, ignore_case ? UCOL_PRIMARY : UCOL_TERTIARY);
473+
usearch_reset(search);
474+
status = U_ZERO_ERROR;
475+
usearch_setText(search, find_in.data(), find_in.size(), &status);
476+
if (!U_SUCCESS(status)) {
477+
if (search) {
478+
usearch_close(search);
479+
}
480+
return StringSearchBasic(search_string, find_in, find_start, find_length,
481+
backwards);
482+
}
483+
int32_t index = backwards ? usearch_last(search, &status)
484+
: usearch_first(search, &status);
485+
bool match = false;
486+
if (U_SUCCESS(status) && index != USEARCH_DONE) {
487+
match = true;
488+
*find_start = static_cast<size_t>(index);
489+
*find_length = static_cast<size_t>(usearch_getMatchedLength(search));
490+
}
491+
usearch_close(search);
492+
return match;
493+
}
494+
458495
HRESULT AXPlatformNodeTextRangeProviderWin::FindText(
459496
BSTR string,
460497
BOOL backwards,

third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,16 @@ class AX_EXPORT __declspec(uuid("3071e40d-a10d-45ff-a59f-6e8e1138e2c1"))
280280
TextRangeEndpoints endpoints_;
281281
};
282282

283+
// Optionally case-insensitive or reverse string search.
284+
//
285+
// Exposed as non-static for use in testing.
286+
bool StringSearch(std::u16string_view search_string,
287+
std::u16string_view find_in,
288+
size_t* find_start,
289+
size_t* find_length,
290+
bool ignore_case,
291+
bool backwards);
292+
283293
} // namespace ui
284294

285295
#endif // UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_NODE_TEXTRANGEPROVIDER_WIN_H_

third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win_unittest.cc

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <UIAutomationClient.h>
88
#include <UIAutomationCoreApi.h>
99

10+
#include <filesystem>
1011
#include <memory>
1112
#include <utility>
1213

@@ -17,6 +18,8 @@
1718
#include "base/win/scoped_bstr.h"
1819
#include "base/win/scoped_safearray.h"
1920
#include "base/win/scoped_variant.h"
21+
#include "flutter/fml/icu_util.h"
22+
#include "third_party/icu/source/common/unicode/putil.h"
2023

2124
using Microsoft::WRL::ComPtr;
2225

@@ -144,25 +147,25 @@ namespace ui {
144147
EXPECT_STREQ(expected_content, provider_content.Get()); \
145148
}
146149

147-
#define EXPECT_UIA_FIND_TEXT(text_range_provider, search_term, ignore_case, \
148-
owner) \
149-
{ \
150-
base::win::ScopedBstr find_string(search_term); \
151-
ComPtr<ITextRangeProvider> text_range_provider_found; \
152-
EXPECT_HRESULT_SUCCEEDED(text_range_provider->FindText( \
153-
find_string.Get(), false, ignore_case, &text_range_provider_found)); \
154-
if (text_range_provider_found == nullptr) { \
155-
EXPECT_TRUE(false); \
156-
} else { \
157-
SetOwner(owner, text_range_provider_found.Get()); \
158-
base::win::ScopedBstr found_content; \
159-
EXPECT_HRESULT_SUCCEEDED( \
160-
text_range_provider_found->GetText(-1, found_content.Receive())); \
161-
if (ignore_case) \
162-
EXPECT_EQ(0, _wcsicmp(found_content.Get(), find_string.Get())); \
163-
else \
164-
EXPECT_EQ(0, wcscmp(found_content.Get(), find_string.Get())); \
165-
} \
150+
#define EXPECT_UIA_FIND_TEXT(text_range_provider, search_term, ignore_case, \
151+
owner) \
152+
{ \
153+
base::win::ScopedBstr find_string(search_term); \
154+
ComPtr<ITextRangeProvider> text_range_provider_found; \
155+
EXPECT_HRESULT_SUCCEEDED(text_range_provider->FindText( \
156+
find_string.Get(), false, ignore_case, &text_range_provider_found)); \
157+
if (text_range_provider_found == nullptr) { \
158+
EXPECT_TRUE(false); \
159+
} else { \
160+
SetOwner(owner, text_range_provider_found.Get()); \
161+
base::win::ScopedBstr found_content; \
162+
EXPECT_HRESULT_SUCCEEDED( \
163+
text_range_provider_found->GetText(-1, found_content.Receive())); \
164+
if (ignore_case) \
165+
EXPECT_TRUE(StringCompareICU(found_content.Get(), find_string.Get())); \
166+
else \
167+
EXPECT_EQ(0, wcscmp(found_content.Get(), find_string.Get())); \
168+
} \
166169
}
167170

168171
#define EXPECT_UIA_FIND_TEXT_NO_MATCH(text_range_provider, search_term, \
@@ -209,6 +212,16 @@ namespace ui {
209212

210213
#define DCHECK_EQ(a, b) BASE_DCHECK((a) == (b))
211214

215+
static bool StringCompareICU(BSTR left, BSTR right) {
216+
size_t start, length;
217+
if (!StringSearch(reinterpret_cast<char16_t*>(left),
218+
reinterpret_cast<char16_t*>(right), &start, &length, true,
219+
false)) {
220+
return false;
221+
}
222+
return start == 0 && length == wcslen(left);
223+
}
224+
212225
static AXNodePosition::AXPositionInstance CreateTextPosition(
213226
const AXNode& anchor,
214227
int text_offset,
@@ -5094,9 +5107,20 @@ TEST_F(AXPlatformNodeTextRangeProviderTest,
50945107
selection.Reset();
50955108
}
50965109

5097-
// TODO(schectman) Find text cannot ignore case yet.
50985110
TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderFindText) {
5099-
Init(BuildTextDocument({"some text", "more text"},
5111+
// Initialize the ICU data from the icudtl.dat file, if it exists.
5112+
wchar_t buffer[MAX_PATH];
5113+
GetModuleFileName(nullptr, buffer, MAX_PATH);
5114+
std::filesystem::path exec_path(buffer);
5115+
exec_path.remove_filename();
5116+
exec_path.append("icudtl.dat");
5117+
const std::string icudtl_path = exec_path.string();
5118+
if (std::filesystem::exists(icudtl_path)) {
5119+
fml::icu::InitializeICU(icudtl_path);
5120+
}
5121+
5122+
// \xC3\xA9 are the UTF8 bytes for codepoint 0xE9 - accented lowercase e.
5123+
Init(BuildTextDocument({"some text", "more text", "resum\xC3\xA9"},
51005124
false /* build_word_boundaries_offsets */,
51015125
true /* place_text_on_one_line */));
51025126

@@ -5109,24 +5133,29 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderFindText) {
51095133
// Test Leaf kStaticText search.
51105134
GetTextRangeProviderFromTextNode(range, root_node->children()[0]);
51115135
EXPECT_UIA_FIND_TEXT(range, L"some text", false, owner);
5112-
// Some expectations like the one below are currently skipped until we can
5113-
// implement ignoreCase in FindText.
5114-
// EXPECT_UIA_FIND_TEXT(range, L"SoMe TeXt", false, owner);
5136+
EXPECT_UIA_FIND_TEXT(range, L"SoMe TeXt", true, owner);
51155137
GetTextRangeProviderFromTextNode(range, root_node->children()[1]);
51165138
EXPECT_UIA_FIND_TEXT(range, L"more", false, owner);
5117-
// EXPECT_UIA_FIND_TEXT(range, L"MoRe", true, owner);
5139+
EXPECT_UIA_FIND_TEXT(range, L"MoRe", true, owner);
51185140

51195141
// Test searching for leaf content from ancestor.
51205142
GetTextRangeProviderFromTextNode(range, root_node);
51215143
EXPECT_UIA_FIND_TEXT(range, L"some text", false, owner);
5122-
// EXPECT_UIA_FIND_TEXT(range, L"SoMe TeXt", true, owner);
5144+
EXPECT_UIA_FIND_TEXT(range, L"SoMe TeXt", true, owner);
51235145
EXPECT_UIA_FIND_TEXT(range, L"more text", false, owner);
5124-
// EXPECT_UIA_FIND_TEXT(range, L"MoRe TeXt", true, owner);
5146+
EXPECT_UIA_FIND_TEXT(range, L"MoRe TeXt", true, owner);
51255147
EXPECT_UIA_FIND_TEXT(range, L"more", false, owner);
5148+
// Accented lowercase e.
5149+
EXPECT_UIA_FIND_TEXT(range, L"resum\xE9", false, owner);
5150+
// Accented uppercase +e.
5151+
EXPECT_UIA_FIND_TEXT(range, L"resum\xC9", true, owner);
5152+
EXPECT_UIA_FIND_TEXT(range, L"resume", true, owner);
5153+
EXPECT_UIA_FIND_TEXT(range, L"resumE", true, owner);
51265154
// Test finding text that crosses a node boundary.
51275155
EXPECT_UIA_FIND_TEXT(range, L"textmore", false, owner);
51285156
// Test no match.
51295157
EXPECT_UIA_FIND_TEXT_NO_MATCH(range, L"no match", false, owner);
5158+
EXPECT_UIA_FIND_TEXT_NO_MATCH(range, L"resume", false, owner);
51305159

51315160
// Test if range returned is in expected anchor node.
51325161
GetTextRangeProviderFromTextNode(range, root_node->children()[1]);

0 commit comments

Comments
 (0)