Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

juce_AccessibilityTextHelpers.h 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. namespace juce
  19. {
  20. struct AccessibilityTextHelpers
  21. {
  22. /* Wraps a CharPtr into a stdlib-compatible iterator.
  23. MSVC's std::reverse_iterator requires the wrapped iterator to be default constructible
  24. when building in C++20 mode, but I don't really want to add public default constructors to
  25. the CharPtr types. Instead, we add a very basic default constructor here which sets the
  26. wrapped CharPtr to nullptr.
  27. */
  28. template <typename CharPtr>
  29. class CharPtrIteratorAdapter
  30. {
  31. public:
  32. using difference_type = int;
  33. using value_type = decltype (*std::declval<CharPtr>());
  34. using pointer = value_type*;
  35. using reference = value_type;
  36. using iterator_category = std::bidirectional_iterator_tag;
  37. CharPtrIteratorAdapter() = default;
  38. constexpr explicit CharPtrIteratorAdapter (CharPtr arg) : ptr (arg) {}
  39. constexpr auto operator*() const { return *ptr; }
  40. constexpr CharPtrIteratorAdapter& operator++()
  41. {
  42. ++ptr;
  43. return *this;
  44. }
  45. constexpr CharPtrIteratorAdapter& operator--()
  46. {
  47. --ptr;
  48. return *this;
  49. }
  50. constexpr bool operator== (const CharPtrIteratorAdapter& other) const { return ptr == other.ptr; }
  51. constexpr bool operator!= (const CharPtrIteratorAdapter& other) const { return ptr != other.ptr; }
  52. constexpr auto operator+ (difference_type offset) const { return CharPtrIteratorAdapter { ptr + offset }; }
  53. constexpr auto operator- (difference_type offset) const { return CharPtrIteratorAdapter { ptr - offset }; }
  54. private:
  55. CharPtr ptr { {} };
  56. };
  57. template <typename CharPtr>
  58. static auto makeCharPtrIteratorAdapter (CharPtr ptr)
  59. {
  60. return CharPtrIteratorAdapter<CharPtr> { ptr };
  61. }
  62. enum class BoundaryType
  63. {
  64. character,
  65. word,
  66. line,
  67. document
  68. };
  69. enum class Direction
  70. {
  71. forwards,
  72. backwards
  73. };
  74. enum class ExtendSelection
  75. {
  76. no,
  77. yes
  78. };
  79. /* Indicates whether a function may return the current text position, in the case that the
  80. position already falls on a text unit boundary.
  81. */
  82. enum class IncludeThisBoundary
  83. {
  84. no, //< Always search for the following boundary, even if the current position falls on a boundary
  85. yes //< Return the current position if it falls on a boundary
  86. };
  87. /* Indicates whether a word boundary should include any whitespaces that follow the
  88. non-whitespace characters.
  89. */
  90. enum class IncludeWhitespaceAfterWords
  91. {
  92. no, //< The word ends on the first whitespace character
  93. yes //< The word ends after the last whitespace character
  94. };
  95. /* Like std::distance, but always does an O(N) count rather than an O(1) count, and doesn't
  96. require the iterators to have any member type aliases.
  97. */
  98. template <typename Iter>
  99. static int countDifference (Iter from, Iter to)
  100. {
  101. int distance = 0;
  102. while (from != to)
  103. {
  104. ++from;
  105. ++distance;
  106. }
  107. return distance;
  108. }
  109. /* Returns the number of characters between ptr and the next word end in a specific
  110. direction.
  111. If ptr is inside a word, the result will be the distance to the end of the same
  112. word.
  113. */
  114. template <typename CharPtr>
  115. static int findNextWordEndOffset (CharPtr beginIn,
  116. CharPtr endIn,
  117. CharPtr ptrIn,
  118. Direction direction,
  119. IncludeThisBoundary includeBoundary,
  120. IncludeWhitespaceAfterWords includeWhitespace)
  121. {
  122. const auto begin = makeCharPtrIteratorAdapter (beginIn);
  123. const auto end = makeCharPtrIteratorAdapter (endIn);
  124. const auto ptr = makeCharPtrIteratorAdapter (ptrIn);
  125. const auto move = [&] (auto b, auto e, auto iter)
  126. {
  127. const auto isSpace = [] (juce_wchar c) { return CharacterFunctions::isWhitespace (c); };
  128. const auto start = [&]
  129. {
  130. if (iter == b && includeBoundary == IncludeThisBoundary::yes)
  131. return b;
  132. const auto nudged = iter - (iter != b && includeBoundary == IncludeThisBoundary::yes ? 1 : 0);
  133. return includeWhitespace == IncludeWhitespaceAfterWords::yes
  134. ? std::find_if (nudged, e, isSpace)
  135. : std::find_if_not (nudged, e, isSpace);
  136. }();
  137. const auto found = includeWhitespace == IncludeWhitespaceAfterWords::yes
  138. ? std::find_if_not (start, e, isSpace)
  139. : std::find_if (start, e, isSpace);
  140. return countDifference (iter, found);
  141. };
  142. return direction == Direction::forwards ? move (begin, end, ptr)
  143. : -move (std::make_reverse_iterator (end),
  144. std::make_reverse_iterator (begin),
  145. std::make_reverse_iterator (ptr));
  146. }
  147. /* Returns the number of characters between ptr and the beginning of the next line in a
  148. specific direction.
  149. */
  150. template <typename CharPtr>
  151. static int findNextLineOffset (CharPtr beginIn,
  152. CharPtr endIn,
  153. CharPtr ptrIn,
  154. Direction direction,
  155. IncludeThisBoundary includeBoundary)
  156. {
  157. const auto begin = makeCharPtrIteratorAdapter (beginIn);
  158. const auto end = makeCharPtrIteratorAdapter (endIn);
  159. const auto ptr = makeCharPtrIteratorAdapter (ptrIn);
  160. const auto findNewline = [] (auto from, auto to) { return std::find (from, to, juce_wchar { '\n' }); };
  161. if (direction == Direction::forwards)
  162. {
  163. if (ptr != begin && includeBoundary == IncludeThisBoundary::yes && *(ptr - 1) == '\n')
  164. return 0;
  165. const auto newline = findNewline (ptr, end);
  166. return countDifference (ptr, newline) + (newline == end ? 0 : 1);
  167. }
  168. const auto rbegin = std::make_reverse_iterator (ptr);
  169. const auto rend = std::make_reverse_iterator (begin);
  170. return -countDifference (rbegin, findNewline (rbegin + (rbegin == rend || includeBoundary == IncludeThisBoundary::yes ? 0 : 1), rend));
  171. }
  172. /* Unfortunately, the method of computing end-points of text units depends on context, and on
  173. the current platform.
  174. Some examples of different behaviour:
  175. - On Android, updating the cursor/selection always searches for the next text unit boundary;
  176. but on Windows, ExpandToEnclosingUnit() should not move the starting point of the
  177. selection if it already at a unit boundary. This means that we need both inclusive and
  178. exclusive methods for finding the next text boundary.
  179. - On Android, moving the cursor by 'words' should move to the first space following a
  180. non-space character in the requested direction. On Windows, a 'word' includes trailing
  181. whitespace, but not preceding whitespace. This means that we need a way of specifying
  182. whether whitespace should be included when navigating by words.
  183. */
  184. static int findTextBoundary (const AccessibilityTextInterface& textInterface,
  185. int currentPosition,
  186. BoundaryType boundary,
  187. Direction direction,
  188. IncludeThisBoundary includeBoundary,
  189. IncludeWhitespaceAfterWords includeWhitespace)
  190. {
  191. const auto numCharacters = textInterface.getTotalNumCharacters();
  192. const auto isForwards = (direction == Direction::forwards);
  193. const auto currentClamped = jlimit (0, numCharacters, currentPosition);
  194. switch (boundary)
  195. {
  196. case BoundaryType::character:
  197. {
  198. const auto offset = includeBoundary == IncludeThisBoundary::yes ? 0
  199. : (isForwards ? 1 : -1);
  200. return jlimit (0, numCharacters, currentPosition + offset);
  201. }
  202. case BoundaryType::word:
  203. {
  204. const auto str = textInterface.getText ({ 0, numCharacters });
  205. return currentClamped + findNextWordEndOffset (str.begin(),
  206. str.end(),
  207. str.begin() + currentClamped,
  208. direction,
  209. includeBoundary,
  210. includeWhitespace);
  211. }
  212. case BoundaryType::line:
  213. {
  214. const auto str = textInterface.getText ({ 0, numCharacters });
  215. return currentClamped + findNextLineOffset (str.begin(),
  216. str.end(),
  217. str.begin() + currentClamped,
  218. direction,
  219. includeBoundary);
  220. }
  221. case BoundaryType::document:
  222. return isForwards ? numCharacters : 0;
  223. }
  224. jassertfalse;
  225. return -1;
  226. }
  227. /* Adjusts the current text selection range, using an algorithm appropriate for cursor movement
  228. on Android.
  229. */
  230. static Range<int> findNewSelectionRangeAndroid (const AccessibilityTextInterface& textInterface,
  231. BoundaryType boundaryType,
  232. ExtendSelection extend,
  233. Direction direction)
  234. {
  235. const auto oldPos = textInterface.getTextInsertionOffset();
  236. const auto cursorPos = findTextBoundary (textInterface,
  237. oldPos,
  238. boundaryType,
  239. direction,
  240. IncludeThisBoundary::no,
  241. IncludeWhitespaceAfterWords::no);
  242. if (extend == ExtendSelection::no)
  243. return { cursorPos, cursorPos };
  244. const auto currentSelection = textInterface.getSelection();
  245. const auto start = currentSelection.getStart();
  246. const auto end = currentSelection.getEnd();
  247. return Range<int>::between (cursorPos, oldPos == start ? end : start);
  248. }
  249. };
  250. } // namespace juce