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.

921 lines
38KB

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