mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-17 00:44:19 +00:00
Previously, when navigating in a text editor by words, the cursor would get 'stuck' after moving a single word. This issue should now be resolved. Additionally, the cursor position was not updated properly when adjusting a selection, and would instead be moved to the end of the selected range. With this patch applied, the cursor should now be set to the correct position when modifying selections. When extending a selection backwards, the cursor will display at the beginning of the selected range, rather than the end. Finally, most Android apps announce the 'skipped' characters or words whenever the cursor is moved, but this feature was broken in JUCE. This patch enables this feature.
269 lines
11 KiB
C++
269 lines
11 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE library.
|
|
Copyright (c) 2020 - 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 6 End-User License
|
|
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
|
|
|
End User License Agreement: www.juce.com/juce-6-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
|
|
{
|
|
|
|
template <typename Ptr>
|
|
struct CharPtrIteratorTraits
|
|
{
|
|
using difference_type = int;
|
|
using value_type = decltype (*std::declval<Ptr>());
|
|
using pointer = value_type*;
|
|
using reference = value_type;
|
|
using iterator_category = std::bidirectional_iterator_tag;
|
|
};
|
|
|
|
} // namespace juce
|
|
|
|
namespace std
|
|
{
|
|
|
|
template <> struct iterator_traits<juce::CharPointer_UTF8> : juce::CharPtrIteratorTraits<juce::CharPointer_UTF8> {};
|
|
template <> struct iterator_traits<juce::CharPointer_UTF16> : juce::CharPtrIteratorTraits<juce::CharPointer_UTF16> {};
|
|
template <> struct iterator_traits<juce::CharPointer_UTF32> : juce::CharPtrIteratorTraits<juce::CharPointer_UTF32> {};
|
|
template <> struct iterator_traits<juce::CharPointer_ASCII> : juce::CharPtrIteratorTraits<juce::CharPointer_ASCII> {};
|
|
|
|
} // namespace std
|
|
|
|
namespace juce
|
|
{
|
|
|
|
struct AccessibilityTextHelpers
|
|
{
|
|
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 begin,
|
|
CharPtr end,
|
|
CharPtr ptr,
|
|
Direction direction,
|
|
IncludeThisBoundary includeBoundary,
|
|
IncludeWhitespaceAfterWords includeWhitespace)
|
|
{
|
|
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 begin,
|
|
CharPtr end,
|
|
CharPtr ptr,
|
|
Direction direction,
|
|
IncludeThisBoundary includeBoundary)
|
|
{
|
|
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
|