The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
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.

270 lines
11KB

  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. template <typename Ptr>
  21. struct CharPtrIteratorTraits
  22. {
  23. using difference_type = int;
  24. using value_type = decltype (*std::declval<Ptr>());
  25. using pointer = value_type*;
  26. using reference = value_type;
  27. using iterator_category = std::bidirectional_iterator_tag;
  28. };
  29. } // namespace juce
  30. namespace std
  31. {
  32. template <> struct iterator_traits<juce::CharPointer_UTF8> : juce::CharPtrIteratorTraits<juce::CharPointer_UTF8> {};
  33. template <> struct iterator_traits<juce::CharPointer_UTF16> : juce::CharPtrIteratorTraits<juce::CharPointer_UTF16> {};
  34. template <> struct iterator_traits<juce::CharPointer_UTF32> : juce::CharPtrIteratorTraits<juce::CharPointer_UTF32> {};
  35. template <> struct iterator_traits<juce::CharPointer_ASCII> : juce::CharPtrIteratorTraits<juce::CharPointer_ASCII> {};
  36. } // namespace std
  37. namespace juce
  38. {
  39. struct AccessibilityTextHelpers
  40. {
  41. enum class BoundaryType
  42. {
  43. character,
  44. word,
  45. line,
  46. document
  47. };
  48. enum class Direction
  49. {
  50. forwards,
  51. backwards
  52. };
  53. enum class ExtendSelection
  54. {
  55. no,
  56. yes
  57. };
  58. /* Indicates whether a function may return the current text position, in the case that the
  59. position already falls on a text unit boundary.
  60. */
  61. enum class IncludeThisBoundary
  62. {
  63. no, //< Always search for the following boundary, even if the current position falls on a boundary
  64. yes //< Return the current position if it falls on a boundary
  65. };
  66. /* Indicates whether a word boundary should include any whitespaces that follow the
  67. non-whitespace characters.
  68. */
  69. enum class IncludeWhitespaceAfterWords
  70. {
  71. no, //< The word ends on the first whitespace character
  72. yes //< The word ends after the last whitespace character
  73. };
  74. /* Like std::distance, but always does an O(N) count rather than an O(1) count, and doesn't
  75. require the iterators to have any member type aliases.
  76. */
  77. template <typename Iter>
  78. static int countDifference (Iter from, Iter to)
  79. {
  80. int distance = 0;
  81. while (from != to)
  82. {
  83. ++from;
  84. ++distance;
  85. }
  86. return distance;
  87. }
  88. /* Returns the number of characters between ptr and the next word end in a specific
  89. direction.
  90. If ptr is inside a word, the result will be the distance to the end of the same
  91. word.
  92. */
  93. template <typename CharPtr>
  94. static int findNextWordEndOffset (CharPtr begin,
  95. CharPtr end,
  96. CharPtr ptr,
  97. Direction direction,
  98. IncludeThisBoundary includeBoundary,
  99. IncludeWhitespaceAfterWords includeWhitespace)
  100. {
  101. const auto move = [&] (auto b, auto e, auto iter)
  102. {
  103. const auto isSpace = [] (juce_wchar c) { return CharacterFunctions::isWhitespace (c); };
  104. const auto start = [&]
  105. {
  106. if (iter == b && includeBoundary == IncludeThisBoundary::yes)
  107. return b;
  108. const auto nudged = iter - (iter != b && includeBoundary == IncludeThisBoundary::yes ? 1 : 0);
  109. return includeWhitespace == IncludeWhitespaceAfterWords::yes
  110. ? std::find_if (nudged, e, isSpace)
  111. : std::find_if_not (nudged, e, isSpace);
  112. }();
  113. const auto found = includeWhitespace == IncludeWhitespaceAfterWords::yes
  114. ? std::find_if_not (start, e, isSpace)
  115. : std::find_if (start, e, isSpace);
  116. return countDifference (iter, found);
  117. };
  118. return direction == Direction::forwards ? move (begin, end, ptr)
  119. : -move (std::make_reverse_iterator (end),
  120. std::make_reverse_iterator (begin),
  121. std::make_reverse_iterator (ptr));
  122. }
  123. /* Returns the number of characters between ptr and the beginning of the next line in a
  124. specific direction.
  125. */
  126. template <typename CharPtr>
  127. static int findNextLineOffset (CharPtr begin,
  128. CharPtr end,
  129. CharPtr ptr,
  130. Direction direction,
  131. IncludeThisBoundary includeBoundary)
  132. {
  133. const auto findNewline = [] (auto from, auto to) { return std::find (from, to, juce_wchar { '\n' }); };
  134. if (direction == Direction::forwards)
  135. {
  136. if (ptr != begin && includeBoundary == IncludeThisBoundary::yes && *(ptr - 1) == '\n')
  137. return 0;
  138. const auto newline = findNewline (ptr, end);
  139. return countDifference (ptr, newline) + (newline == end ? 0 : 1);
  140. }
  141. const auto rbegin = std::make_reverse_iterator (ptr);
  142. const auto rend = std::make_reverse_iterator (begin);
  143. return -countDifference (rbegin, findNewline (rbegin + (rbegin == rend || includeBoundary == IncludeThisBoundary::yes ? 0 : 1), rend));
  144. }
  145. /* Unfortunately, the method of computing end-points of text units depends on context, and on
  146. the current platform.
  147. Some examples of different behaviour:
  148. - On Android, updating the cursor/selection always searches for the next text unit boundary;
  149. but on Windows, ExpandToEnclosingUnit() should not move the starting point of the
  150. selection if it already at a unit boundary. This means that we need both inclusive and
  151. exclusive methods for finding the next text boundary.
  152. - On Android, moving the cursor by 'words' should move to the first space following a
  153. non-space character in the requested direction. On Windows, a 'word' includes trailing
  154. whitespace, but not preceding whitespace. This means that we need a way of specifying
  155. whether whitespace should be included when navigating by words.
  156. */
  157. static int findTextBoundary (const AccessibilityTextInterface& textInterface,
  158. int currentPosition,
  159. BoundaryType boundary,
  160. Direction direction,
  161. IncludeThisBoundary includeBoundary,
  162. IncludeWhitespaceAfterWords includeWhitespace)
  163. {
  164. const auto numCharacters = textInterface.getTotalNumCharacters();
  165. const auto isForwards = (direction == Direction::forwards);
  166. const auto currentClamped = jlimit (0, numCharacters, currentPosition);
  167. switch (boundary)
  168. {
  169. case BoundaryType::character:
  170. {
  171. const auto offset = includeBoundary == IncludeThisBoundary::yes ? 0
  172. : (isForwards ? 1 : -1);
  173. return jlimit (0, numCharacters, currentPosition + offset);
  174. }
  175. case BoundaryType::word:
  176. {
  177. const auto str = textInterface.getText ({ 0, numCharacters });
  178. return currentClamped + findNextWordEndOffset (str.begin(),
  179. str.end(),
  180. str.begin() + currentClamped,
  181. direction,
  182. includeBoundary,
  183. includeWhitespace);
  184. }
  185. case BoundaryType::line:
  186. {
  187. const auto str = textInterface.getText ({ 0, numCharacters });
  188. return currentClamped + findNextLineOffset (str.begin(),
  189. str.end(),
  190. str.begin() + currentClamped,
  191. direction,
  192. includeBoundary);
  193. }
  194. case BoundaryType::document:
  195. return isForwards ? numCharacters : 0;
  196. }
  197. jassertfalse;
  198. return -1;
  199. }
  200. /* Adjusts the current text selection range, using an algorithm appropriate for cursor movement
  201. on Android.
  202. */
  203. static Range<int> findNewSelectionRangeAndroid (const AccessibilityTextInterface& textInterface,
  204. BoundaryType boundaryType,
  205. ExtendSelection extend,
  206. Direction direction)
  207. {
  208. const auto oldPos = textInterface.getTextInsertionOffset();
  209. const auto cursorPos = findTextBoundary (textInterface,
  210. oldPos,
  211. boundaryType,
  212. direction,
  213. IncludeThisBoundary::no,
  214. IncludeWhitespaceAfterWords::no);
  215. if (extend == ExtendSelection::no)
  216. return { cursorPos, cursorPos };
  217. const auto currentSelection = textInterface.getSelection();
  218. const auto start = currentSelection.getStart();
  219. const auto end = currentSelection.getEnd();
  220. return Range<int>::between (cursorPos, oldPos == start ? end : start);
  221. }
  222. };
  223. } // namespace juce