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.

1036 lines
45KB

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