mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-13 00:04:19 +00:00
302 lines
12 KiB
C++
302 lines
12 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
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 <typename CharPtr>
|
|
class CharPtrIteratorAdapter
|
|
{
|
|
public:
|
|
using difference_type = int;
|
|
using value_type = decltype (*std::declval<CharPtr>());
|
|
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 <typename CharPtr>
|
|
static auto makeCharPtrIteratorAdapter (CharPtr ptr)
|
|
{
|
|
return CharPtrIteratorAdapter<CharPtr> { 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 <typename Iter>
|
|
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 <typename CharPtr>
|
|
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 <typename CharPtr>
|
|
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<int> 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<int>::between (cursorPos, oldPos == start ? end : start);
|
|
}
|
|
};
|
|
|
|
} // namespace juce
|