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.

868 lines
36KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2020 - 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 6 End-User License
  8. Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
  9. End User License Agreement: www.juce.com/juce-6-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. #define JUCE_NATIVE_ACCESSIBILITY_INCLUDED 1
  21. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  22. METHOD (constructor, "<init>", "()V") \
  23. METHOD (setSource, "setSource", "(Landroid/view/View;I)V") \
  24. METHOD (addChild, "addChild", "(Landroid/view/View;I)V") \
  25. METHOD (setParent, "setParent", "(Landroid/view/View;)V") \
  26. METHOD (setVirtualParent, "setParent", "(Landroid/view/View;I)V") \
  27. METHOD (setBoundsInScreen, "setBoundsInScreen", "(Landroid/graphics/Rect;)V") \
  28. METHOD (setBoundsInParent, "setBoundsInParent", "(Landroid/graphics/Rect;)V") \
  29. METHOD (setPackageName, "setPackageName", "(Ljava/lang/CharSequence;)V") \
  30. METHOD (setClassName, "setClassName", "(Ljava/lang/CharSequence;)V") \
  31. METHOD (setContentDescription, "setContentDescription", "(Ljava/lang/CharSequence;)V") \
  32. METHOD (setCheckable, "setCheckable", "(Z)V") \
  33. METHOD (setChecked, "setChecked", "(Z)V") \
  34. METHOD (setClickable, "setClickable", "(Z)V") \
  35. METHOD (setEnabled, "setEnabled", "(Z)V") \
  36. METHOD (setFocusable, "setFocusable", "(Z)V") \
  37. METHOD (setFocused, "setFocused", "(Z)V") \
  38. METHOD (setPassword, "setPassword", "(Z)V") \
  39. METHOD (setSelected, "setSelected", "(Z)V") \
  40. METHOD (setVisibleToUser, "setVisibleToUser", "(Z)V") \
  41. METHOD (setAccessibilityFocused, "setAccessibilityFocused", "(Z)V") \
  42. METHOD (setText, "setText", "(Ljava/lang/CharSequence;)V") \
  43. METHOD (setMovementGranularities, "setMovementGranularities", "(I)V") \
  44. METHOD (addAction, "addAction", "(I)V") \
  45. DECLARE_JNI_CLASS (AndroidAccessibilityNodeInfo, "android/view/accessibility/AccessibilityNodeInfo")
  46. #undef JNI_CLASS_MEMBERS
  47. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  48. STATICMETHOD (obtain, "obtain", "(I)Landroid/view/accessibility/AccessibilityEvent;") \
  49. METHOD (setPackageName, "setPackageName", "(Ljava/lang/CharSequence;)V") \
  50. METHOD (setSource, "setSource", "(Landroid/view/View;I)V") \
  51. DECLARE_JNI_CLASS (AndroidAccessibilityEvent, "android/view/accessibility/AccessibilityEvent")
  52. #undef JNI_CLASS_MEMBERS
  53. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  54. METHOD (isEnabled, "isEnabled", "()Z") \
  55. DECLARE_JNI_CLASS (AndroidAccessibilityManager, "android/view/accessibility/AccessibilityManager")
  56. #undef JNI_CLASS_MEMBERS
  57. namespace
  58. {
  59. constexpr int HOST_VIEW_ID = -1;
  60. constexpr int TYPE_VIEW_CLICKED = 0x00000001,
  61. TYPE_VIEW_SELECTED = 0x00000004,
  62. TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000,
  63. TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000,
  64. TYPE_WINDOW_CONTENT_CHANGED = 0x00000800,
  65. TYPE_VIEW_TEXT_SELECTION_CHANGED = 0x00002000,
  66. TYPE_VIEW_TEXT_CHANGED = 0x00000010;
  67. constexpr int ACTION_ACCESSIBILITY_FOCUS = 0x00000040,
  68. ACTION_CLEAR_ACCESSIBILITY_FOCUS = 0x00000080,
  69. ACTION_CLEAR_FOCUS = 0x00000002,
  70. ACTION_CLEAR_SELECTION = 0x00000008,
  71. ACTION_CLICK = 0x00000010,
  72. ACTION_COLLAPSE = 0x00080000,
  73. ACTION_EXPAND = 0x00040000,
  74. ACTION_FOCUS = 0x00000001,
  75. ACTION_NEXT_AT_MOVEMENT_GRANULARITY = 0x00000100,
  76. ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY = 0x00000200,
  77. ACTION_SCROLL_BACKWARD = 0x00002000,
  78. ACTION_SCROLL_FORWARD = 0x00001000,
  79. ACTION_SELECT = 0x00000004,
  80. ACTION_SET_SELECTION = 0x00020000,
  81. ACTION_SET_TEXT = 0x00200000;
  82. constexpr int MOVEMENT_GRANULARITY_CHARACTER = 0x00000001,
  83. MOVEMENT_GRANULARITY_LINE = 0x00000004,
  84. MOVEMENT_GRANULARITY_PAGE = 0x00000010,
  85. MOVEMENT_GRANULARITY_PARAGRAPH = 0x00000008,
  86. MOVEMENT_GRANULARITY_WORD = 0x00000002,
  87. ALL_GRANULARITIES = MOVEMENT_GRANULARITY_CHARACTER
  88. | MOVEMENT_GRANULARITY_LINE
  89. | MOVEMENT_GRANULARITY_PAGE
  90. | MOVEMENT_GRANULARITY_PARAGRAPH
  91. | MOVEMENT_GRANULARITY_WORD;
  92. constexpr int ACCESSIBILITY_LIVE_REGION_POLITE = 0x00000001;
  93. }
  94. static jmethodID nodeInfoSetEditable = nullptr;
  95. static jmethodID nodeInfoSetTextSelection = nullptr;
  96. static jmethodID nodeInfoSetLiveRegion = nullptr;
  97. static void loadSDKDependentMethods()
  98. {
  99. static bool hasChecked = false;
  100. if (! hasChecked)
  101. {
  102. hasChecked = true;
  103. auto* env = getEnv();
  104. const auto sdkVersion = getAndroidSDKVersion();
  105. if (sdkVersion >= 18)
  106. {
  107. nodeInfoSetEditable = env->GetMethodID (AndroidAccessibilityNodeInfo, "setEditable", "(Z)V");
  108. nodeInfoSetTextSelection = env->GetMethodID (AndroidAccessibilityNodeInfo, "setTextSelection", "(II)V");
  109. }
  110. if (sdkVersion >= 19)
  111. nodeInfoSetLiveRegion = env->GetMethodID (AndroidAccessibilityNodeInfo, "setLiveRegion", "(I)V");
  112. }
  113. }
  114. static constexpr auto getClassName (AccessibilityRole role)
  115. {
  116. switch (role)
  117. {
  118. case AccessibilityRole::editableText: return "android.widget.EditText";
  119. case AccessibilityRole::toggleButton: return "android.widget.CheckBox";
  120. case AccessibilityRole::radioButton: return "android.widget.RadioButton";
  121. case AccessibilityRole::image: return "android.widget.ImageView";
  122. case AccessibilityRole::popupMenu: return "android.widget.PopupMenu";
  123. case AccessibilityRole::comboBox: return "android.widget.Spinner";
  124. case AccessibilityRole::tree: return "android.widget.ExpandableListView";
  125. case AccessibilityRole::list: return "android.widget.ListView";
  126. case AccessibilityRole::table: return "android.widget.TableLayout";
  127. case AccessibilityRole::progressBar: return "android.widget.ProgressBar";
  128. case AccessibilityRole::scrollBar:
  129. case AccessibilityRole::slider: return "android.widget.SeekBar";
  130. case AccessibilityRole::hyperlink:
  131. case AccessibilityRole::button: return "android.widget.Button";
  132. case AccessibilityRole::label:
  133. case AccessibilityRole::staticText: return "android.widget.TextView";
  134. case AccessibilityRole::tooltip:
  135. case AccessibilityRole::splashScreen:
  136. case AccessibilityRole::dialogWindow: return "android.widget.PopupWindow";
  137. case AccessibilityRole::column:
  138. case AccessibilityRole::row:
  139. case AccessibilityRole::cell:
  140. case AccessibilityRole::menuItem:
  141. case AccessibilityRole::menuBar:
  142. case AccessibilityRole::listItem:
  143. case AccessibilityRole::treeItem:
  144. case AccessibilityRole::window:
  145. case AccessibilityRole::tableHeader:
  146. case AccessibilityRole::unspecified:
  147. case AccessibilityRole::group:
  148. case AccessibilityRole::ignored: break;
  149. }
  150. return "android.view.View";
  151. }
  152. static auto getRootView()
  153. {
  154. LocalRef<jobject> activity (getMainActivity());
  155. if (activity != nullptr)
  156. {
  157. auto* env = getEnv();
  158. LocalRef<jobject> mainWindow (env->CallObjectMethod (activity.get(), AndroidActivity.getWindow));
  159. LocalRef<jobject> decorView (env->CallObjectMethod (mainWindow.get(), AndroidWindow.getDecorView));
  160. return LocalRef<jobject> (env->CallObjectMethod (decorView.get(), AndroidView.getRootView));
  161. }
  162. return LocalRef<jobject>();
  163. }
  164. static jobject getSourceView (const AccessibilityHandler& handler)
  165. {
  166. if (auto* peer = handler.getComponent().getPeer())
  167. return (jobject) peer->getNativeHandle();
  168. return nullptr;
  169. }
  170. void sendAccessibilityEventImpl (const AccessibilityHandler& handler, int eventType);
  171. //==============================================================================
  172. class AccessibilityNativeHandle
  173. {
  174. public:
  175. static AccessibilityHandler* getAccessibilityHandlerForVirtualViewId (int virtualViewId)
  176. {
  177. auto iter = virtualViewIdMap.find (virtualViewId);
  178. if (iter != virtualViewIdMap.end())
  179. return iter->second;
  180. return nullptr;
  181. }
  182. explicit AccessibilityNativeHandle (AccessibilityHandler& h)
  183. : accessibilityHandler (h),
  184. virtualViewId (getVirtualViewIdForHandler (accessibilityHandler))
  185. {
  186. loadSDKDependentMethods();
  187. if (virtualViewId != HOST_VIEW_ID)
  188. virtualViewIdMap[virtualViewId] = &accessibilityHandler;
  189. }
  190. ~AccessibilityNativeHandle()
  191. {
  192. if (virtualViewId != HOST_VIEW_ID)
  193. virtualViewIdMap.erase (virtualViewId);
  194. }
  195. int getVirtualViewId() const noexcept { return virtualViewId; }
  196. void populateNodeInfo (jobject info)
  197. {
  198. const auto sourceView = getSourceView (accessibilityHandler);
  199. if (sourceView == nullptr)
  200. return;
  201. auto* env = getEnv();
  202. {
  203. for (auto* child : accessibilityHandler.getChildren())
  204. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addChild,
  205. sourceView, child->getNativeImplementation()->getVirtualViewId());
  206. if (auto* parent = accessibilityHandler.getParent())
  207. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setVirtualParent,
  208. sourceView, parent->getNativeImplementation()->getVirtualViewId());
  209. else
  210. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setParent, sourceView);
  211. }
  212. {
  213. const auto scale = Desktop::getInstance().getDisplays().getPrimaryDisplay()->scale;
  214. const auto screenBounds = accessibilityHandler.getComponent().getScreenBounds() * scale;
  215. LocalRef<jobject> rect (env->NewObject (AndroidRect, AndroidRect.constructor,
  216. screenBounds.getX(), screenBounds.getY(),
  217. screenBounds.getRight(), screenBounds.getBottom()));
  218. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setBoundsInScreen, rect.get());
  219. const auto boundsInParent = accessibilityHandler.getComponent().getBoundsInParent() * scale;
  220. rect = LocalRef<jobject> (env->NewObject (AndroidRect, AndroidRect.constructor,
  221. boundsInParent.getX(), boundsInParent.getY(),
  222. boundsInParent.getRight(), boundsInParent.getBottom()));
  223. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setBoundsInParent, rect.get());
  224. }
  225. const auto state = accessibilityHandler.getCurrentState();
  226. env->CallVoidMethod (info,
  227. AndroidAccessibilityNodeInfo.setEnabled,
  228. ! state.isIgnored());
  229. env->CallVoidMethod (info,
  230. AndroidAccessibilityNodeInfo.setVisibleToUser,
  231. true);
  232. env->CallVoidMethod (info,
  233. AndroidAccessibilityNodeInfo.setPackageName,
  234. env->CallObjectMethod (getAppContext().get(),
  235. AndroidContext.getPackageName));
  236. env->CallVoidMethod (info,
  237. AndroidAccessibilityNodeInfo.setSource,
  238. sourceView,
  239. virtualViewId);
  240. env->CallVoidMethod (info,
  241. AndroidAccessibilityNodeInfo.setClassName,
  242. javaString (getClassName (accessibilityHandler.getRole())).get());
  243. env->CallVoidMethod (info,
  244. AndroidAccessibilityNodeInfo.setContentDescription,
  245. getDescriptionString().get());
  246. if (state.isFocusable())
  247. {
  248. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.setFocusable, true);
  249. const auto& component = accessibilityHandler.getComponent();
  250. if (component.getWantsKeyboardFocus())
  251. {
  252. const auto hasKeyboardFocus = component.hasKeyboardFocus (false);
  253. env->CallVoidMethod (info,
  254. AndroidAccessibilityNodeInfo.setFocused,
  255. hasKeyboardFocus);
  256. env->CallVoidMethod (info,
  257. AndroidAccessibilityNodeInfo.addAction,
  258. hasKeyboardFocus ? ACTION_CLEAR_FOCUS : ACTION_FOCUS);
  259. }
  260. const auto isAccessibleFocused = accessibilityHandler.hasFocus (false);
  261. env->CallVoidMethod (info,
  262. AndroidAccessibilityNodeInfo.setAccessibilityFocused,
  263. isAccessibleFocused);
  264. env->CallVoidMethod (info,
  265. AndroidAccessibilityNodeInfo.addAction,
  266. isAccessibleFocused ? ACTION_CLEAR_ACCESSIBILITY_FOCUS
  267. : ACTION_ACCESSIBILITY_FOCUS);
  268. }
  269. if (state.isCheckable())
  270. {
  271. env->CallVoidMethod (info,
  272. AndroidAccessibilityNodeInfo.setCheckable,
  273. true);
  274. env->CallVoidMethod (info,
  275. AndroidAccessibilityNodeInfo.setChecked,
  276. state.isChecked());
  277. }
  278. if (state.isSelectable() || state.isMultiSelectable())
  279. {
  280. const auto isSelected = state.isSelected();
  281. env->CallVoidMethod (info,
  282. AndroidAccessibilityNodeInfo.setSelected,
  283. isSelected);
  284. env->CallVoidMethod (info,
  285. AndroidAccessibilityNodeInfo.addAction,
  286. isSelected ? ACTION_CLEAR_SELECTION : ACTION_SELECT);
  287. }
  288. if (accessibilityHandler.getActions().contains (AccessibilityActionType::press))
  289. {
  290. env->CallVoidMethod (info,
  291. AndroidAccessibilityNodeInfo.setClickable,
  292. true);
  293. env->CallVoidMethod (info,
  294. AndroidAccessibilityNodeInfo.addAction,
  295. ACTION_CLICK);
  296. }
  297. if (accessibilityHandler.getActions().contains (AccessibilityActionType::showMenu)
  298. && state.isExpandable())
  299. {
  300. env->CallVoidMethod (info,
  301. AndroidAccessibilityNodeInfo.addAction,
  302. state.isExpanded() ? ACTION_COLLAPSE : ACTION_EXPAND);
  303. }
  304. if (auto* textInterface = accessibilityHandler.getTextInterface())
  305. {
  306. env->CallVoidMethod (info,
  307. AndroidAccessibilityNodeInfo.setText,
  308. javaString (textInterface->getText ({ 0, textInterface->getTotalNumCharacters() })).get());
  309. const auto isReadOnly = textInterface->isReadOnly();
  310. env->CallVoidMethod (info,
  311. AndroidAccessibilityNodeInfo.setPassword,
  312. textInterface->isDisplayingProtectedText());
  313. if (nodeInfoSetEditable != nullptr)
  314. env->CallVoidMethod (info, nodeInfoSetEditable, ! isReadOnly);
  315. const auto selection = textInterface->getSelection();
  316. if (nodeInfoSetTextSelection != nullptr && ! selection.isEmpty())
  317. env->CallVoidMethod (info,
  318. nodeInfoSetTextSelection,
  319. selection.getStart(), selection.getEnd());
  320. if (nodeInfoSetLiveRegion != nullptr && accessibilityHandler.hasFocus (false))
  321. env->CallVoidMethod (info,
  322. nodeInfoSetLiveRegion,
  323. ACCESSIBILITY_LIVE_REGION_POLITE);
  324. env->CallVoidMethod (info,
  325. AndroidAccessibilityNodeInfo.setMovementGranularities,
  326. ALL_GRANULARITIES);
  327. env->CallVoidMethod (info,
  328. AndroidAccessibilityNodeInfo.addAction,
  329. ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
  330. env->CallVoidMethod (info,
  331. AndroidAccessibilityNodeInfo.addAction,
  332. ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
  333. env->CallVoidMethod (info,
  334. AndroidAccessibilityNodeInfo.addAction,
  335. ACTION_SET_SELECTION);
  336. if (! isReadOnly)
  337. env->CallVoidMethod (info, AndroidAccessibilityNodeInfo.addAction, ACTION_SET_TEXT);
  338. }
  339. if (auto* valueInterface = accessibilityHandler.getValueInterface())
  340. {
  341. if (! valueInterface->isReadOnly())
  342. {
  343. const auto range = valueInterface->getRange();
  344. if (range.isValid())
  345. {
  346. env->CallVoidMethod (info,
  347. AndroidAccessibilityNodeInfo.addAction,
  348. ACTION_SCROLL_FORWARD);
  349. env->CallVoidMethod (info,
  350. AndroidAccessibilityNodeInfo.addAction,
  351. ACTION_SCROLL_BACKWARD);
  352. }
  353. }
  354. }
  355. }
  356. bool performAction (int action, jobject arguments)
  357. {
  358. switch (action)
  359. {
  360. case ACTION_ACCESSIBILITY_FOCUS:
  361. {
  362. const WeakReference<Component> safeComponent (&accessibilityHandler.getComponent());
  363. accessibilityHandler.getActions().invoke (AccessibilityActionType::focus);
  364. if (safeComponent != nullptr)
  365. accessibilityHandler.grabFocus();
  366. return true;
  367. }
  368. case ACTION_CLEAR_ACCESSIBILITY_FOCUS:
  369. {
  370. accessibilityHandler.giveAwayFocus();
  371. return true;
  372. }
  373. case ACTION_FOCUS:
  374. case ACTION_CLEAR_FOCUS:
  375. {
  376. auto& component = accessibilityHandler.getComponent();
  377. if (component.getWantsKeyboardFocus())
  378. {
  379. const auto hasFocus = component.hasKeyboardFocus (false);
  380. if (hasFocus && action == ACTION_CLEAR_FOCUS)
  381. component.giveAwayKeyboardFocus();
  382. else if (! hasFocus && action == ACTION_FOCUS)
  383. component.grabKeyboardFocus();
  384. return true;
  385. }
  386. break;
  387. }
  388. case ACTION_CLICK:
  389. {
  390. if (accessibilityHandler.getActions().invoke (AccessibilityActionType::press))
  391. {
  392. sendAccessibilityEventImpl (accessibilityHandler, TYPE_VIEW_CLICKED);
  393. return true;
  394. }
  395. break;
  396. }
  397. case ACTION_SELECT:
  398. case ACTION_CLEAR_SELECTION:
  399. {
  400. const auto state = accessibilityHandler.getCurrentState();
  401. if (state.isSelectable() || state.isMultiSelectable())
  402. {
  403. const auto isSelected = state.isSelected();
  404. if ((isSelected && action == ACTION_CLEAR_SELECTION)
  405. || (! isSelected && action == ACTION_SELECT))
  406. {
  407. return accessibilityHandler.getActions().invoke (AccessibilityActionType::toggle);
  408. }
  409. }
  410. break;
  411. }
  412. case ACTION_EXPAND:
  413. case ACTION_COLLAPSE:
  414. {
  415. const auto state = accessibilityHandler.getCurrentState();
  416. if (state.isExpandable())
  417. {
  418. const auto isExpanded = state.isExpanded();
  419. if ((isExpanded && action == ACTION_COLLAPSE)
  420. || (! isExpanded && action == ACTION_EXPAND))
  421. {
  422. return accessibilityHandler.getActions().invoke (AccessibilityActionType::showMenu);
  423. }
  424. }
  425. break;
  426. }
  427. case ACTION_NEXT_AT_MOVEMENT_GRANULARITY: return moveCursor (arguments, true);
  428. case ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: return moveCursor (arguments, false);
  429. case ACTION_SET_SELECTION:
  430. {
  431. if (auto* textInterface = accessibilityHandler.getTextInterface())
  432. {
  433. auto* env = getEnv();
  434. const auto selection = [&]() -> Range<int>
  435. {
  436. const auto selectionStartKey = javaString ("ACTION_ARGUMENT_SELECTION_START_INT");
  437. const auto selectionEndKey = javaString ("ACTION_ARGUMENT_SELECTION_END_INT");
  438. const auto hasKey = [&env, &arguments] (const auto& key)
  439. {
  440. return env->CallBooleanMethod (arguments, AndroidBundle.containsKey, key.get());
  441. };
  442. if (hasKey (selectionStartKey) && hasKey (selectionEndKey))
  443. {
  444. const auto getKey = [&env, &arguments] (const auto& key)
  445. {
  446. return env->CallIntMethod (arguments, AndroidBundle.getInt, key.get());
  447. };
  448. return { getKey (selectionStartKey), getKey (selectionEndKey) };
  449. }
  450. return {};
  451. }();
  452. textInterface->setSelection (selection);
  453. return true;
  454. }
  455. break;
  456. }
  457. case ACTION_SET_TEXT:
  458. {
  459. if (auto* textInterface = accessibilityHandler.getTextInterface())
  460. {
  461. if (! textInterface->isReadOnly())
  462. {
  463. const auto charSequenceKey = javaString ("ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE");
  464. auto* env = getEnv();
  465. const auto text = [&]() -> String
  466. {
  467. if (env->CallBooleanMethod (arguments, AndroidBundle.containsKey, charSequenceKey.get()))
  468. {
  469. LocalRef<jobject> charSequence (env->CallObjectMethod (arguments,
  470. AndroidBundle.getCharSequence,
  471. charSequenceKey.get()));
  472. LocalRef<jstring> textStringRef ((jstring) env->CallObjectMethod (charSequence,
  473. JavaCharSequence.toString));
  474. return juceString (textStringRef.get());
  475. }
  476. return {};
  477. }();
  478. textInterface->setText (text);
  479. }
  480. }
  481. break;
  482. }
  483. case ACTION_SCROLL_BACKWARD:
  484. case ACTION_SCROLL_FORWARD:
  485. {
  486. if (auto* valueInterface = accessibilityHandler.getValueInterface())
  487. {
  488. if (! valueInterface->isReadOnly())
  489. {
  490. const auto range = valueInterface->getRange();
  491. if (range.isValid())
  492. {
  493. const auto interval = action == ACTION_SCROLL_BACKWARD ? -range.getInterval()
  494. : range.getInterval();
  495. valueInterface->setValue (jlimit (range.getMinimumValue(),
  496. range.getMaximumValue(),
  497. valueInterface->getCurrentValue() + interval));
  498. // required for Android to announce the new value
  499. sendAccessibilityEventImpl (accessibilityHandler, TYPE_VIEW_SELECTED);
  500. return true;
  501. }
  502. }
  503. }
  504. break;
  505. }
  506. }
  507. return false;
  508. }
  509. private:
  510. static std::unordered_map<int, AccessibilityHandler*> virtualViewIdMap;
  511. static int getVirtualViewIdForHandler (const AccessibilityHandler& handler)
  512. {
  513. static int counter = 0;
  514. if (handler.getComponent().isOnDesktop())
  515. return HOST_VIEW_ID;
  516. return counter++;
  517. }
  518. LocalRef<jstring> getDescriptionString() const
  519. {
  520. const auto valueString = [this]() -> String
  521. {
  522. if (auto* textInterface = accessibilityHandler.getTextInterface())
  523. return textInterface->getText ({ 0, textInterface->getTotalNumCharacters() });
  524. if (auto* valueInterface = accessibilityHandler.getValueInterface())
  525. return valueInterface->getCurrentValueAsString();
  526. return {};
  527. }();
  528. StringArray strings (accessibilityHandler.getTitle(),
  529. valueString,
  530. accessibilityHandler.getDescription(),
  531. accessibilityHandler.getHelp());
  532. strings.removeEmptyStrings();
  533. return javaString (strings.joinIntoString (","));
  534. }
  535. bool moveCursor (jobject arguments, bool forwards)
  536. {
  537. if (auto* textInterface = accessibilityHandler.getTextInterface())
  538. {
  539. const auto granularityKey = javaString ("ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT");
  540. const auto extendSelectionKey = javaString ("ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN");
  541. auto* env = getEnv();
  542. const auto boundaryType = [&]
  543. {
  544. const auto granularity = env->CallIntMethod (arguments,
  545. AndroidBundle.getInt,
  546. granularityKey.get());
  547. using BoundaryType = AccessibilityTextHelpers::BoundaryType;
  548. switch (granularity)
  549. {
  550. case MOVEMENT_GRANULARITY_CHARACTER: return BoundaryType::character;
  551. case MOVEMENT_GRANULARITY_WORD: return BoundaryType::word;
  552. case MOVEMENT_GRANULARITY_LINE: return BoundaryType::line;
  553. case MOVEMENT_GRANULARITY_PARAGRAPH:
  554. case MOVEMENT_GRANULARITY_PAGE: return BoundaryType::document;
  555. }
  556. jassertfalse;
  557. return BoundaryType::character;
  558. }();
  559. using Direction = AccessibilityTextHelpers::Direction;
  560. const auto cursorPos = AccessibilityTextHelpers::findTextBoundary (*textInterface,
  561. textInterface->getTextInsertionOffset(),
  562. boundaryType,
  563. forwards ? Direction::forwards
  564. : Direction::backwards);
  565. const auto newSelection = [&]() -> Range<int>
  566. {
  567. const auto currentSelection = textInterface->getSelection();
  568. const auto extendSelection = env->CallBooleanMethod (arguments,
  569. AndroidBundle.getBoolean,
  570. extendSelectionKey.get());
  571. if (! extendSelection)
  572. return { cursorPos, cursorPos };
  573. const auto start = currentSelection.getStart();
  574. const auto end = currentSelection.getEnd();
  575. if (forwards)
  576. return { start, jmax (start, cursorPos) };
  577. return { jmin (start, cursorPos), end };
  578. }();
  579. textInterface->setSelection (newSelection);
  580. return true;
  581. }
  582. return false;
  583. }
  584. AccessibilityHandler& accessibilityHandler;
  585. const int virtualViewId;
  586. //==============================================================================
  587. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityNativeHandle)
  588. };
  589. std::unordered_map<int, AccessibilityHandler*> AccessibilityNativeHandle::virtualViewIdMap;
  590. class AccessibilityHandler::AccessibilityNativeImpl : public AccessibilityNativeHandle
  591. {
  592. public:
  593. using AccessibilityNativeHandle::AccessibilityNativeHandle;
  594. };
  595. //==============================================================================
  596. AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const
  597. {
  598. return nativeImpl.get();
  599. }
  600. static bool areAnyAccessibilityClientsActive()
  601. {
  602. auto* env = getEnv();
  603. LocalRef<jobject> accessibilityManager (env->CallObjectMethod (getAppContext().get(), AndroidContext.getSystemService,
  604. javaString ("accessibility").get()));
  605. if (accessibilityManager != nullptr)
  606. return env->CallBooleanMethod (accessibilityManager.get(), AndroidAccessibilityManager.isEnabled);
  607. return false;
  608. }
  609. void sendAccessibilityEventImpl (const AccessibilityHandler& handler, int eventType)
  610. {
  611. if (! areAnyAccessibilityClientsActive())
  612. return;
  613. if (const auto sourceView = getSourceView (handler))
  614. {
  615. auto* env = getEnv();
  616. LocalRef<jobject> event (env->CallStaticObjectMethod (AndroidAccessibilityEvent,
  617. AndroidAccessibilityEvent.obtain,
  618. eventType));
  619. env->CallVoidMethod (event,
  620. AndroidAccessibilityEvent.setPackageName,
  621. env->CallObjectMethod (getAppContext().get(), AndroidContext.getPackageName));
  622. env->CallVoidMethod (event,
  623. AndroidAccessibilityEvent.setSource,
  624. sourceView,
  625. handler.getNativeImplementation()->getVirtualViewId());
  626. env->CallBooleanMethod (sourceView,
  627. AndroidViewGroup.requestSendAccessibilityEvent,
  628. sourceView,
  629. event.get());
  630. }
  631. }
  632. void notifyAccessibilityEventInternal (const AccessibilityHandler& handler,
  633. InternalAccessibilityEvent eventType)
  634. {
  635. auto notification = [&handler, eventType]
  636. {
  637. switch (eventType)
  638. {
  639. case InternalAccessibilityEvent::focusChanged:
  640. return handler.hasFocus (false) ? TYPE_VIEW_ACCESSIBILITY_FOCUSED
  641. : TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED;
  642. case InternalAccessibilityEvent::elementCreated:
  643. case InternalAccessibilityEvent::elementDestroyed:
  644. case InternalAccessibilityEvent::elementMovedOrResized:
  645. return handler.getComponent().isOnDesktop() ? 0
  646. : TYPE_WINDOW_CONTENT_CHANGED;
  647. case InternalAccessibilityEvent::windowOpened:
  648. case InternalAccessibilityEvent::windowClosed:
  649. break;
  650. }
  651. return 0;
  652. }();
  653. if (notification != 0)
  654. sendAccessibilityEventImpl (handler, notification);
  655. }
  656. void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent eventType) const
  657. {
  658. auto notification = [eventType]
  659. {
  660. switch (eventType)
  661. {
  662. case AccessibilityEvent::textSelectionChanged: return TYPE_VIEW_TEXT_SELECTION_CHANGED;
  663. case AccessibilityEvent::textChanged: return TYPE_VIEW_TEXT_CHANGED;
  664. case AccessibilityEvent::structureChanged: return TYPE_WINDOW_CONTENT_CHANGED;
  665. case AccessibilityEvent::rowSelectionChanged:
  666. case AccessibilityEvent::valueChanged:
  667. case AccessibilityEvent::titleChanged: break;
  668. }
  669. return 0;
  670. }();
  671. if (notification != 0)
  672. sendAccessibilityEventImpl (*this, notification);
  673. }
  674. void AccessibilityHandler::postAnnouncement (const String& announcementString,
  675. AnnouncementPriority)
  676. {
  677. if (! areAnyAccessibilityClientsActive())
  678. return;
  679. const auto rootView = getRootView();
  680. if (rootView != nullptr)
  681. getEnv()->CallVoidMethod (rootView.get(),
  682. AndroidView.announceForAccessibility,
  683. javaString (announcementString).get());
  684. }
  685. } // namespace juce