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.

946 lines
39KB

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