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.

914 lines
38KB

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