Skip to content

Commit

Permalink
[Windows] Ignore case optionally in `AXPlatformNodeTextRangeProviderW…
Browse files Browse the repository at this point in the history
…in::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
  • Loading branch information
yaakovschectman authored and zhongwuzw committed Apr 14, 2023
1 parent 73ba5d8 commit 5b96527
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 45 deletions.
5 changes: 4 additions & 1 deletion third_party/accessibility/ax/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ source_set("ax") {
"oleacc.lib",
"uiautomationcore.lib",
]
deps = [ "//flutter/fml:string_conversion" ]
deps = [
"//flutter/fml:fml",
"//third_party/icu:icui18n",
]
}

public_deps = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include <UIAutomation.h>
#include <wrl/client.h>
#include <string_view>

#include "ax/ax_action_data.h"
#include "ax/ax_range.h"
Expand All @@ -14,6 +15,7 @@
#include "ax/platform/ax_platform_tree_manager.h"
#include "base/win/variant_vector.h"
#include "flutter/fml/platform/win/wstring_conversion.h"
#include "third_party/icu/source/i18n/unicode/usearch.h"

#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL() \
if (!GetOwner() || !GetOwner()->GetDelegate() || !start() || \
Expand Down Expand Up @@ -433,28 +435,63 @@ HRESULT AXPlatformNodeTextRangeProviderWin::FindAttributeRange(
return S_OK;
}

static bool StringSearch(const std::u16string& search_string,
const std::u16string& find_in,
size_t* find_start,
size_t* find_length,
bool ignore_case,
bool backwards) {
// TODO(schectman) Respect ignore_case/i18n.
// https://github.com/flutter/flutter/issues/117013
size_t match_pos;
if (backwards) {
match_pos = find_in.rfind(search_string);
} else {
match_pos = find_in.find(search_string);
}
if (match_pos == std::u16string::npos) {
static bool StringSearchBasic(const std::u16string_view search_string,
const std::u16string_view find_in,
size_t* find_start,
size_t* find_length,
bool backwards) {
size_t index =
backwards ? find_in.rfind(search_string) : find_in.find(search_string);
if (index == std::u16string::npos) {
return false;
}
*find_start = match_pos;
*find_length = search_string.length();
*find_start = index;
*find_length = search_string.size();
return true;
}

bool StringSearch(std::u16string_view search_string,
std::u16string_view find_in,
size_t* find_start,
size_t* find_length,
bool ignore_case,
bool backwards) {
UErrorCode status = U_ZERO_ERROR;
UCollator* col = ucol_open(uloc_getDefault(), &status);
UStringSearch* search = usearch_openFromCollator(
search_string.data(), search_string.size(), find_in.data(),
find_in.size(), col, nullptr, &status);
if (!U_SUCCESS(status)) {
if (search) {
usearch_close(search);
}
return StringSearchBasic(search_string, find_in, find_start, find_length,
backwards);
}
UCollator* collator = usearch_getCollator(search);
ucol_setStrength(collator, ignore_case ? UCOL_PRIMARY : UCOL_TERTIARY);
usearch_reset(search);
status = U_ZERO_ERROR;
usearch_setText(search, find_in.data(), find_in.size(), &status);
if (!U_SUCCESS(status)) {
if (search) {
usearch_close(search);
}
return StringSearchBasic(search_string, find_in, find_start, find_length,
backwards);
}
int32_t index = backwards ? usearch_last(search, &status)
: usearch_first(search, &status);
bool match = false;
if (U_SUCCESS(status) && index != USEARCH_DONE) {
match = true;
*find_start = static_cast<size_t>(index);
*find_length = static_cast<size_t>(usearch_getMatchedLength(search));
}
usearch_close(search);
return match;
}

HRESULT AXPlatformNodeTextRangeProviderWin::FindText(
BSTR string,
BOOL backwards,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,16 @@ class AX_EXPORT __declspec(uuid("3071e40d-a10d-45ff-a59f-6e8e1138e2c1"))
TextRangeEndpoints endpoints_;
};

// Optionally case-insensitive or reverse string search.
//
// Exposed as non-static for use in testing.
bool StringSearch(std::u16string_view search_string,
std::u16string_view find_in,
size_t* find_start,
size_t* find_length,
bool ignore_case,
bool backwards);

} // namespace ui

#endif // UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_NODE_TEXTRANGEPROVIDER_WIN_H_
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <UIAutomationClient.h>
#include <UIAutomationCoreApi.h>

#include <filesystem>
#include <memory>
#include <utility>

Expand All @@ -17,6 +18,8 @@
#include "base/win/scoped_bstr.h"
#include "base/win/scoped_safearray.h"
#include "base/win/scoped_variant.h"
#include "flutter/fml/icu_util.h"
#include "third_party/icu/source/common/unicode/putil.h"

using Microsoft::WRL::ComPtr;

Expand Down Expand Up @@ -144,25 +147,25 @@ namespace ui {
EXPECT_STREQ(expected_content, provider_content.Get()); \
}

#define EXPECT_UIA_FIND_TEXT(text_range_provider, search_term, ignore_case, \
owner) \
{ \
base::win::ScopedBstr find_string(search_term); \
ComPtr<ITextRangeProvider> text_range_provider_found; \
EXPECT_HRESULT_SUCCEEDED(text_range_provider->FindText( \
find_string.Get(), false, ignore_case, &text_range_provider_found)); \
if (text_range_provider_found == nullptr) { \
EXPECT_TRUE(false); \
} else { \
SetOwner(owner, text_range_provider_found.Get()); \
base::win::ScopedBstr found_content; \
EXPECT_HRESULT_SUCCEEDED( \
text_range_provider_found->GetText(-1, found_content.Receive())); \
if (ignore_case) \
EXPECT_EQ(0, _wcsicmp(found_content.Get(), find_string.Get())); \
else \
EXPECT_EQ(0, wcscmp(found_content.Get(), find_string.Get())); \
} \
#define EXPECT_UIA_FIND_TEXT(text_range_provider, search_term, ignore_case, \
owner) \
{ \
base::win::ScopedBstr find_string(search_term); \
ComPtr<ITextRangeProvider> text_range_provider_found; \
EXPECT_HRESULT_SUCCEEDED(text_range_provider->FindText( \
find_string.Get(), false, ignore_case, &text_range_provider_found)); \
if (text_range_provider_found == nullptr) { \
EXPECT_TRUE(false); \
} else { \
SetOwner(owner, text_range_provider_found.Get()); \
base::win::ScopedBstr found_content; \
EXPECT_HRESULT_SUCCEEDED( \
text_range_provider_found->GetText(-1, found_content.Receive())); \
if (ignore_case) \
EXPECT_TRUE(StringCompareICU(found_content.Get(), find_string.Get())); \
else \
EXPECT_EQ(0, wcscmp(found_content.Get(), find_string.Get())); \
} \
}

#define EXPECT_UIA_FIND_TEXT_NO_MATCH(text_range_provider, search_term, \
Expand Down Expand Up @@ -209,6 +212,16 @@ namespace ui {

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

static bool StringCompareICU(BSTR left, BSTR right) {
size_t start, length;
if (!StringSearch(reinterpret_cast<char16_t*>(left),
reinterpret_cast<char16_t*>(right), &start, &length, true,
false)) {
return false;
}
return start == 0 && length == wcslen(left);
}

static AXNodePosition::AXPositionInstance CreateTextPosition(
const AXNode& anchor,
int text_offset,
Expand Down Expand Up @@ -5094,9 +5107,20 @@ TEST_F(AXPlatformNodeTextRangeProviderTest,
selection.Reset();
}

// TODO(schectman) Find text cannot ignore case yet.
TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderFindText) {
Init(BuildTextDocument({"some text", "more text"},
// Initialize the ICU data from the icudtl.dat file, if it exists.
wchar_t buffer[MAX_PATH];
GetModuleFileName(nullptr, buffer, MAX_PATH);
std::filesystem::path exec_path(buffer);
exec_path.remove_filename();
exec_path.append("icudtl.dat");
const std::string icudtl_path = exec_path.string();
if (std::filesystem::exists(icudtl_path)) {
fml::icu::InitializeICU(icudtl_path);
}

// \xC3\xA9 are the UTF8 bytes for codepoint 0xE9 - accented lowercase e.
Init(BuildTextDocument({"some text", "more text", "resum\xC3\xA9"},
false /* build_word_boundaries_offsets */,
true /* place_text_on_one_line */));

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

// Test searching for leaf content from ancestor.
GetTextRangeProviderFromTextNode(range, root_node);
EXPECT_UIA_FIND_TEXT(range, L"some text", false, owner);
// EXPECT_UIA_FIND_TEXT(range, L"SoMe TeXt", true, owner);
EXPECT_UIA_FIND_TEXT(range, L"SoMe TeXt", true, owner);
EXPECT_UIA_FIND_TEXT(range, L"more text", false, owner);
// EXPECT_UIA_FIND_TEXT(range, L"MoRe TeXt", true, owner);
EXPECT_UIA_FIND_TEXT(range, L"MoRe TeXt", true, owner);
EXPECT_UIA_FIND_TEXT(range, L"more", false, owner);
// Accented lowercase e.
EXPECT_UIA_FIND_TEXT(range, L"resum\xE9", false, owner);
// Accented uppercase +e.
EXPECT_UIA_FIND_TEXT(range, L"resum\xC9", true, owner);
EXPECT_UIA_FIND_TEXT(range, L"resume", true, owner);
EXPECT_UIA_FIND_TEXT(range, L"resumE", true, owner);
// Test finding text that crosses a node boundary.
EXPECT_UIA_FIND_TEXT(range, L"textmore", false, owner);
// Test no match.
EXPECT_UIA_FIND_TEXT_NO_MATCH(range, L"no match", false, owner);
EXPECT_UIA_FIND_TEXT_NO_MATCH(range, L"resume", false, owner);

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

0 comments on commit 5b96527

Please sign in to comment.