/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { struct AccessibilityTextHelpers { /* Wraps a CharPtr into a stdlib-compatible iterator. MSVC's std::reverse_iterator requires the wrapped iterator to be default constructible when building in C++20 mode, but I don't really want to add public default constructors to the CharPtr types. Instead, we add a very basic default constructor here which sets the wrapped CharPtr to nullptr. */ template class CharPtrIteratorAdapter { public: using difference_type = int; using value_type = decltype (*std::declval()); using pointer = value_type*; using reference = value_type; using iterator_category = std::bidirectional_iterator_tag; CharPtrIteratorAdapter() = default; constexpr explicit CharPtrIteratorAdapter (CharPtr arg) : ptr (arg) {} constexpr auto operator*() const { return *ptr; } constexpr CharPtrIteratorAdapter& operator++() { ++ptr; return *this; } constexpr CharPtrIteratorAdapter& operator--() { --ptr; return *this; } constexpr bool operator== (const CharPtrIteratorAdapter& other) const { return ptr == other.ptr; } constexpr bool operator!= (const CharPtrIteratorAdapter& other) const { return ptr != other.ptr; } constexpr auto operator+ (difference_type offset) const { return CharPtrIteratorAdapter { ptr + offset }; } constexpr auto operator- (difference_type offset) const { return CharPtrIteratorAdapter { ptr - offset }; } private: CharPtr ptr { {} }; }; template static auto makeCharPtrIteratorAdapter (CharPtr ptr) { return CharPtrIteratorAdapter { ptr }; } enum class BoundaryType { character, word, line, document }; enum class Direction { forwards, backwards }; enum class ExtendSelection { no, yes }; /* Indicates whether a function may return the current text position, in the case that the position already falls on a text unit boundary. */ enum class IncludeThisBoundary { no, //< Always search for the following boundary, even if the current position falls on a boundary yes //< Return the current position if it falls on a boundary }; /* Indicates whether a word boundary should include any whitespaces that follow the non-whitespace characters. */ enum class IncludeWhitespaceAfterWords { no, //< The word ends on the first whitespace character yes //< The word ends after the last whitespace character }; /* Like std::distance, but always does an O(N) count rather than an O(1) count, and doesn't require the iterators to have any member type aliases. */ template static int countDifference (Iter from, Iter to) { int distance = 0; while (from != to) { ++from; ++distance; } return distance; } /* Returns the number of characters between ptr and the next word end in a specific direction. If ptr is inside a word, the result will be the distance to the end of the same word. */ template static int findNextWordEndOffset (CharPtr beginIn, CharPtr endIn, CharPtr ptrIn, Direction direction, IncludeThisBoundary includeBoundary, IncludeWhitespaceAfterWords includeWhitespace) { const auto begin = makeCharPtrIteratorAdapter (beginIn); const auto end = makeCharPtrIteratorAdapter (endIn); const auto ptr = makeCharPtrIteratorAdapter (ptrIn); const auto move = [&] (auto b, auto e, auto iter) { const auto isSpace = [] (juce_wchar c) { return CharacterFunctions::isWhitespace (c); }; const auto start = [&] { if (iter == b && includeBoundary == IncludeThisBoundary::yes) return b; const auto nudged = iter - (iter != b && includeBoundary == IncludeThisBoundary::yes ? 1 : 0); return includeWhitespace == IncludeWhitespaceAfterWords::yes ? std::find_if (nudged, e, isSpace) : std::find_if_not (nudged, e, isSpace); }(); const auto found = includeWhitespace == IncludeWhitespaceAfterWords::yes ? std::find_if_not (start, e, isSpace) : std::find_if (start, e, isSpace); return countDifference (iter, found); }; return direction == Direction::forwards ? move (begin, end, ptr) : -move (std::make_reverse_iterator (end), std::make_reverse_iterator (begin), std::make_reverse_iterator (ptr)); } /* Returns the number of characters between ptr and the beginning of the next line in a specific direction. */ template static int findNextLineOffset (CharPtr beginIn, CharPtr endIn, CharPtr ptrIn, Direction direction, IncludeThisBoundary includeBoundary) { const auto begin = makeCharPtrIteratorAdapter (beginIn); const auto end = makeCharPtrIteratorAdapter (endIn); const auto ptr = makeCharPtrIteratorAdapter (ptrIn); const auto findNewline = [] (auto from, auto to) { return std::find (from, to, juce_wchar { '\n' }); }; if (direction == Direction::forwards) { if (ptr != begin && includeBoundary == IncludeThisBoundary::yes && *(ptr - 1) == '\n') return 0; const auto newline = findNewline (ptr, end); return countDifference (ptr, newline) + (newline == end ? 0 : 1); } const auto rbegin = std::make_reverse_iterator (ptr); const auto rend = std::make_reverse_iterator (begin); return -countDifference (rbegin, findNewline (rbegin + (rbegin == rend || includeBoundary == IncludeThisBoundary::yes ? 0 : 1), rend)); } /* Unfortunately, the method of computing end-points of text units depends on context, and on the current platform. Some examples of different behaviour: - On Android, updating the cursor/selection always searches for the next text unit boundary; but on Windows, ExpandToEnclosingUnit() should not move the starting point of the selection if it already at a unit boundary. This means that we need both inclusive and exclusive methods for finding the next text boundary. - On Android, moving the cursor by 'words' should move to the first space following a non-space character in the requested direction. On Windows, a 'word' includes trailing whitespace, but not preceding whitespace. This means that we need a way of specifying whether whitespace should be included when navigating by words. */ static int findTextBoundary (const AccessibilityTextInterface& textInterface, int currentPosition, BoundaryType boundary, Direction direction, IncludeThisBoundary includeBoundary, IncludeWhitespaceAfterWords includeWhitespace) { const auto numCharacters = textInterface.getTotalNumCharacters(); const auto isForwards = (direction == Direction::forwards); const auto currentClamped = jlimit (0, numCharacters, currentPosition); switch (boundary) { case BoundaryType::character: { const auto offset = includeBoundary == IncludeThisBoundary::yes ? 0 : (isForwards ? 1 : -1); return jlimit (0, numCharacters, currentPosition + offset); } case BoundaryType::word: { const auto str = textInterface.getText ({ 0, numCharacters }); return currentClamped + findNextWordEndOffset (str.begin(), str.end(), str.begin() + currentClamped, direction, includeBoundary, includeWhitespace); } case BoundaryType::line: { const auto str = textInterface.getText ({ 0, numCharacters }); return currentClamped + findNextLineOffset (str.begin(), str.end(), str.begin() + currentClamped, direction, includeBoundary); } case BoundaryType::document: return isForwards ? numCharacters : 0; } jassertfalse; return -1; } /* Adjusts the current text selection range, using an algorithm appropriate for cursor movement on Android. */ static Range findNewSelectionRangeAndroid (const AccessibilityTextInterface& textInterface, BoundaryType boundaryType, ExtendSelection extend, Direction direction) { const auto oldPos = textInterface.getTextInsertionOffset(); const auto cursorPos = findTextBoundary (textInterface, oldPos, boundaryType, direction, IncludeThisBoundary::no, IncludeWhitespaceAfterWords::no); if (extend == ExtendSelection::no) return { cursorPos, cursorPos }; const auto currentSelection = textInterface.getSelection(); const auto start = currentSelection.getStart(); const auto end = currentSelection.getEnd(); return Range::between (cursorPos, oldPos == start ? end : start); } }; } // namespace juce