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.

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