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.

881 lines
36KB

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