From ec990202b1bac7515a7e9df6b1309bcba4df3408 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 10 May 2021 09:38:00 +0100 Subject: [PATCH] Accessibility: Added VoiceOver (macOS) and Narrator (Windows) accessibility screen reader support to juce_gui_basics --- BREAKING-CHANGES.txt | 26 + docs/Accessibility.md | 51 + .../juce_core/native/juce_mac_ObjCHelpers.h | 10 + .../juce_core/system/juce_StandardHeader.h | 1 + .../native/juce_mac_CoreGraphicsHelpers.h | 28 + .../enums/juce_AccessibilityActions.h | 119 ++ .../enums/juce_AccessibilityEvent.h | 75 ++ .../enums/juce_AccessibilityRole.h | 70 ++ .../juce_AccessibilityCellInterface.h | 61 + .../juce_AccessibilityTableInterface.h | 54 + .../juce_AccessibilityTextInterface.h | 78 ++ .../juce_AccessibilityValueInterface.h | 222 ++++ .../juce_AccessibilityHandler.cpp | 346 ++++++ .../accessibility/juce_AccessibilityHandler.h | 325 +++++ .../accessibility/juce_AccessibilityState.h | 227 ++++ .../juce_ButtonAccessibilityHandler.h | 96 ++ .../juce_ComboBoxAccessibilityHandler.h | 66 + .../juce_LabelAccessibilityHandler.h | 62 + .../juce_SliderAccessibilityHandler.h | 100 ++ .../juce_TableListBoxAccessibilityHandler.h | 83 ++ .../juce_TextEditorAccessibilityHandler.h | 108 ++ .../juce_TreeViewAccessibilityHandler.h | 79 ++ .../juce_gui_basics/buttons/juce_Button.cpp | 13 + modules/juce_gui_basics/buttons/juce_Button.h | 2 + .../components/juce_Component.cpp | 357 ++++-- .../components/juce_Component.h | 425 +++++-- .../components/juce_ComponentTraverser.h | 72 ++ .../components/juce_FocusTraverser.cpp | 359 ++++++ .../components/juce_FocusTraverser.h | 93 ++ .../drawables/juce_Drawable.cpp | 6 + .../juce_gui_basics/drawables/juce_Drawable.h | 2 + .../drawables/juce_DrawableImage.cpp | 6 + .../drawables/juce_DrawableImage.h | 2 + .../drawables/juce_DrawableText.cpp | 21 + .../drawables/juce_DrawableText.h | 2 + .../filebrowser/juce_FileBrowserComponent.cpp | 6 + .../filebrowser/juce_FileBrowserComponent.h | 2 + .../filebrowser/juce_FileListComponent.cpp | 13 +- .../filebrowser/juce_FileListComponent.h | 1 + .../filebrowser/juce_FileTreeComponent.cpp | 5 + .../filebrowser/juce_FilenameComponent.cpp | 4 +- .../filebrowser/juce_FilenameComponent.h | 2 +- .../juce_ImagePreviewComponent.cpp | 6 + .../filebrowser/juce_ImagePreviewComponent.h | 2 + modules/juce_gui_basics/juce_gui_basics.cpp | 11 + modules/juce_gui_basics/juce_gui_basics.h | 22 +- .../keyboard/juce_KeyboardFocusTraverser.cpp | 293 +++-- .../keyboard/juce_KeyboardFocusTraverser.h | 75 +- .../layout/juce_ComponentAnimator.cpp | 1 + .../layout/juce_ConcertinaPanel.cpp | 6 + .../layout/juce_ConcertinaPanel.h | 4 + .../layout/juce_GroupComponent.cpp | 6 + .../layout/juce_GroupComponent.h | 2 + .../juce_gui_basics/layout/juce_ScrollBar.cpp | 44 +- .../juce_gui_basics/layout/juce_ScrollBar.h | 7 + .../juce_gui_basics/layout/juce_SidePanel.cpp | 6 + .../juce_gui_basics/layout/juce_SidePanel.h | 2 + .../layout/juce_TabbedButtonBar.cpp | 8 +- .../layout/juce_TabbedButtonBar.h | 2 + .../layout/juce_TabbedComponent.cpp | 6 + .../layout/juce_TabbedComponent.h | 2 + .../menus/juce_BurgerMenuComponent.cpp | 6 + .../menus/juce_BurgerMenuComponent.h | 2 + .../menus/juce_MenuBarComponent.cpp | 256 ++-- .../menus/juce_MenuBarComponent.h | 30 +- .../juce_gui_basics/menus/juce_PopupMenu.cpp | 180 ++- .../misc/juce_BubbleComponent.h | 8 +- .../misc/juce_DropShadower.cpp | 1 + .../misc/juce_JUCESplashScreen.cpp | 6 + .../misc/juce_JUCESplashScreen.h | 4 + .../accessibility/juce_mac_Accessibility.mm | 1078 ++++++++++++++++ .../juce_win32_Accessibility.cpp | 270 ++++ .../juce_win32_AccessibilityElement.cpp | 558 +++++++++ .../juce_win32_AccessibilityElement.h | 80 ++ .../juce_win32_UIAExpandCollapseProvider.h | 86 ++ .../juce_win32_UIAGridItemProvider.h | 101 ++ .../juce_win32_UIAGridProvider.h | 90 ++ .../accessibility/juce_win32_UIAHelpers.h | 103 ++ .../juce_win32_UIAInvokeProvider.h | 62 + .../juce_win32_UIAProviderBase.h | 58 + .../accessibility/juce_win32_UIAProviders.h | 43 + .../juce_win32_UIARangeValueProvider.h | 140 +++ .../juce_win32_UIASelectionProvider.h | 252 ++++ .../juce_win32_UIATextProvider.h | 664 ++++++++++ .../juce_win32_UIAToggleProvider.h | 80 ++ .../juce_win32_UIATransformProvider.h | 125 ++ .../juce_win32_UIAValueProvider.h | 121 ++ .../juce_win32_UIAWindowProvider.h | 197 +++ .../juce_win32_WindowsUIAWrapper.h | 158 +++ .../native/juce_mac_NSViewComponentPeer.mm | 244 ++-- .../native/juce_win32_Windowing.cpp | 42 +- .../properties/juce_PropertyPanel.cpp | 2 +- .../juce_gui_basics/widgets/juce_ComboBox.cpp | 7 + .../juce_gui_basics/widgets/juce_ComboBox.h | 2 + .../widgets/juce_ImageComponent.cpp | 6 + .../widgets/juce_ImageComponent.h | 2 + .../juce_gui_basics/widgets/juce_Label.cpp | 64 +- modules/juce_gui_basics/widgets/juce_Label.h | 4 +- .../juce_gui_basics/widgets/juce_ListBox.cpp | 134 +- .../juce_gui_basics/widgets/juce_ListBox.h | 8 + .../widgets/juce_ProgressBar.cpp | 29 + .../widgets/juce_ProgressBar.h | 2 + .../juce_gui_basics/widgets/juce_Slider.cpp | 48 +- modules/juce_gui_basics/widgets/juce_Slider.h | 2 + .../widgets/juce_TableHeaderComponent.cpp | 6 + .../widgets/juce_TableHeaderComponent.h | 2 + .../widgets/juce_TableListBox.cpp | 86 +- .../widgets/juce_TableListBox.h | 3 +- .../widgets/juce_TextEditor.cpp | 134 +- .../juce_gui_basics/widgets/juce_TextEditor.h | 22 +- .../juce_gui_basics/widgets/juce_Toolbar.cpp | 6 + .../juce_gui_basics/widgets/juce_Toolbar.h | 2 + .../widgets/juce_ToolbarItemComponent.cpp | 11 + .../widgets/juce_ToolbarItemComponent.h | 2 + .../widgets/juce_ToolbarItemPalette.cpp | 6 + .../widgets/juce_ToolbarItemPalette.h | 2 + .../juce_gui_basics/widgets/juce_TreeView.cpp | 1086 +++++++++-------- .../juce_gui_basics/widgets/juce_TreeView.h | 213 ++-- .../windows/juce_AlertWindow.cpp | 19 +- .../windows/juce_AlertWindow.h | 3 + .../windows/juce_CallOutBox.cpp | 6 + .../juce_gui_basics/windows/juce_CallOutBox.h | 2 + .../windows/juce_ComponentPeer.cpp | 26 +- .../windows/juce_ComponentPeer.h | 6 +- .../windows/juce_DialogWindow.cpp | 6 + .../windows/juce_DialogWindow.h | 2 + .../windows/juce_TooltipWindow.cpp | 15 + .../windows/juce_TooltipWindow.h | 4 + .../windows/juce_TopLevelWindow.cpp | 7 + .../windows/juce_TopLevelWindow.h | 2 + .../code_editor/juce_CodeEditorComponent.cpp | 211 +++- .../code_editor/juce_CodeEditorComponent.h | 7 +- .../misc/juce_KeyMappingEditorComponent.cpp | 18 +- 133 files changed, 10158 insertions(+), 1297 deletions(-) create mode 100644 docs/Accessibility.md create mode 100644 modules/juce_gui_basics/accessibility/enums/juce_AccessibilityActions.h create mode 100644 modules/juce_gui_basics/accessibility/enums/juce_AccessibilityEvent.h create mode 100644 modules/juce_gui_basics/accessibility/enums/juce_AccessibilityRole.h create mode 100644 modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityCellInterface.h create mode 100644 modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTableInterface.h create mode 100644 modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTextInterface.h create mode 100644 modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityValueInterface.h create mode 100644 modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.cpp create mode 100644 modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.h create mode 100644 modules/juce_gui_basics/accessibility/juce_AccessibilityState.h create mode 100644 modules/juce_gui_basics/accessibility/widget_handlers/juce_ButtonAccessibilityHandler.h create mode 100644 modules/juce_gui_basics/accessibility/widget_handlers/juce_ComboBoxAccessibilityHandler.h create mode 100644 modules/juce_gui_basics/accessibility/widget_handlers/juce_LabelAccessibilityHandler.h create mode 100644 modules/juce_gui_basics/accessibility/widget_handlers/juce_SliderAccessibilityHandler.h create mode 100644 modules/juce_gui_basics/accessibility/widget_handlers/juce_TableListBoxAccessibilityHandler.h create mode 100644 modules/juce_gui_basics/accessibility/widget_handlers/juce_TextEditorAccessibilityHandler.h create mode 100644 modules/juce_gui_basics/accessibility/widget_handlers/juce_TreeViewAccessibilityHandler.h create mode 100644 modules/juce_gui_basics/components/juce_ComponentTraverser.h create mode 100644 modules/juce_gui_basics/components/juce_FocusTraverser.cpp create mode 100644 modules/juce_gui_basics/components/juce_FocusTraverser.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_Accessibility.cpp create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_AccessibilityElement.cpp create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_AccessibilityElement.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_UIAExpandCollapseProvider.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridItemProvider.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridProvider.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_UIAHelpers.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_UIAInvokeProvider.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_UIAProviderBase.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_UIAProviders.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_UIARangeValueProvider.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_UIASelectionProvider.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_UIATextProvider.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_UIAToggleProvider.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_UIATransformProvider.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_UIAValueProvider.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_UIAWindowProvider.h create mode 100644 modules/juce_gui_basics/native/accessibility/juce_win32_WindowsUIAWrapper.h diff --git a/BREAKING-CHANGES.txt b/BREAKING-CHANGES.txt index 2268fe9e9e..1c3f2b365b 100644 --- a/BREAKING-CHANGES.txt +++ b/BREAKING-CHANGES.txt @@ -4,6 +4,32 @@ JUCE breaking changes Develop ======= +Change +------ +`Component::createFocusTraverser()` has been renamed to +`Component::createKeyboardFocusTraverser()` and now returns a `std::unique_ptr` +instead of a raw pointer. `Component::createFocusTraverser()` is a new method +for controlling basic focus traversal and not keyboard focus traversal. + +Possible Issues +--------------- +Derived Components that override the old method will no longer compile. + +Workaround +---------- +Override the new method. Be careful to override +`createKeyboardFocusTraverser()` and not `createFocusTraverser()` to ensure +that the behaviour is the same. + +Rationale +--------- +The ownership of this method is now clearer as the previous code relied on the +caller deleting the object. The name has changed to accomodate the new +`Component::createFocusTraverser()` method that returns an object for +determining basic focus traversal, of which keyboard focus is generally a +subset. + + Change ------ PluginDescription::uid has been deprecated and replaced with a new 'uniqueId' diff --git a/docs/Accessibility.md b/docs/Accessibility.md new file mode 100644 index 0000000000..ab003604dd --- /dev/null +++ b/docs/Accessibility.md @@ -0,0 +1,51 @@ +# JUCE Accessibility + +## What is supported? + +Currently JUCE supports VoiceOver on macOS and Narrator on Windows. The JUCE +accessibility API exposes the following to these clients: + + - Title, description, and help text for UI elements + - Programmatic access to UI elements and text + - Interaction with UI elements + - Full UI keyboard navigation + - Posting notifications to listening clients + +## Customising Behaviour + +By default any visible and enabled `Component` is accessible to screen reader +clients and exposes some basic information such as title, description, help +text and its position in the hierarchy of UI elements. + +The `setTitle()`, `setDescription()` and `setHelpText()` methods can be used +to customise the text that will be read out by accessibility clients when +interacting with UI elements and the `setExplicitFocusOrder()`, +`setFocusContainer()` and `createFocusTraverser()` methods can be used to +control the parent/child relationships and the order of navigation between UI +elements. + +## Custom Components + +For further customisation of accessibility behaviours the `AccessibilityHandler` +class provides a unified API to the underlying native accessibility libraries. + +This class wraps a component with a given role specified by the +`AccessibilityRole` enum and takes a list of optional actions and interfaces to +provide programmatic access and control over the UI element. Its state is used +to convey further information to accessibility clients via the +`getCurrentState()` method. + +To implement the desired behaviours for a custom component, subclass +`AccessibilityHandler` and return an instance of this from the +`Component::createAccessibilityHandler()` method. + +Examples of some common UI element handlers for existing JUCE widgets can be +found in the [`widget_handlers`](/modules/juce_gui_basics/accessibility/widget_handlers) directory. + +## Further Reading + + - [NSAccessibility protocol](https://developer.apple.com/documentation/appkit/nsaccessibility?language=objc) + - [UI Automation for Win32 applications](https://docs.microsoft.com/en-us/windows/win32/winauto/entry-uiauto-win32) + - A talk giving an overview of this feature from ADC 2020 can be found on + YouTube at https://youtu.be/BqrEv4ApH3U + diff --git a/modules/juce_core/native/juce_mac_ObjCHelpers.h b/modules/juce_core/native/juce_mac_ObjCHelpers.h index 3d2787918b..1e199453b8 100644 --- a/modules/juce_core/native/juce_mac_ObjCHelpers.h +++ b/modules/juce_core/native/juce_mac_ObjCHelpers.h @@ -29,6 +29,16 @@ namespace juce { //============================================================================== +inline Range nsRangeToJuce (NSRange range) +{ + return { (int) range.location, (int) (range.location + range.length) }; +} + +inline NSRange juceRangeToNS (Range range) +{ + return NSMakeRange ((NSUInteger) range.getStart(), (NSUInteger) range.getLength()); +} + inline String nsStringToJuce (NSString* s) { return CharPointer_UTF8 ([s UTF8String]); diff --git a/modules/juce_core/system/juce_StandardHeader.h b/modules/juce_core/system/juce_StandardHeader.h index 226b91030c..e473404663 100644 --- a/modules/juce_core/system/juce_StandardHeader.h +++ b/modules/juce_core/system/juce_StandardHeader.h @@ -60,6 +60,7 @@ #include #include #include +#include #include #include diff --git a/modules/juce_graphics/native/juce_mac_CoreGraphicsHelpers.h b/modules/juce_graphics/native/juce_mac_CoreGraphicsHelpers.h index d9b2930db8..c16710560d 100644 --- a/modules/juce_graphics/native/juce_mac_CoreGraphicsHelpers.h +++ b/modules/juce_graphics/native/juce_mac_CoreGraphicsHelpers.h @@ -64,6 +64,34 @@ namespace { return CGPointMake ((CGFloat) p.x, (CGFloat) p.y); } + + #if JUCE_MAC + inline CGFloat getMainScreenHeight() noexcept + { + if ([[NSScreen screens] count] == 0) + return 0.0f; + + return [[[NSScreen screens] objectAtIndex: 0] frame].size.height; + } + + inline NSRect flippedScreenRect (NSRect r) noexcept + { + r.origin.y = getMainScreenHeight() - (r.origin.y + r.size.height); + return r; + } + + inline NSPoint flippedScreenPoint (NSPoint p) noexcept + { + p.y = getMainScreenHeight() - p.y; + return p; + } + + template + Point convertToIntPoint (PointType p) noexcept + { + return Point (roundToInt (p.x), roundToInt (p.y)); + } + #endif } CGImageRef juce_createCoreGraphicsImage (const Image&, CGColorSpaceRef, bool mustOutliveSource); diff --git a/modules/juce_gui_basics/accessibility/enums/juce_AccessibilityActions.h b/modules/juce_gui_basics/accessibility/enums/juce_AccessibilityActions.h new file mode 100644 index 0000000000..f3ad844492 --- /dev/null +++ b/modules/juce_gui_basics/accessibility/enums/juce_AccessibilityActions.h @@ -0,0 +1,119 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** An action that can be performed by an accessible UI element. + + @tags{Accessibility} +*/ +enum class AccessibilityActionType +{ + /** Represents a "press" action. + + This will be called when the user "clicks" the UI element using an + accessibility client. + */ + press, + + /** Represents a "toggle" action. + + This will be called when the user toggles the state of a UI element, + for example a toggle button or the selection of a list item. + */ + toggle, + + /** Indicates that the UI element has received focus. + + This will be called when a UI element receives focus from an accessibility + client, or keyboard focus from the application. + */ + focus, + + /** Represents the user showing a contextual menu for a UI element. + + This will be called for UI elements which expand and collapse to + show contextual information or menus, or show a popup. + */ + showMenu +}; + +/** A simple wrapper for building a collection of supported accessibility actions + and corresponding callbacks for a UI element. + + Pass one of these when constructing an `AccessibilityHandler` to enable users + to interact with a UI element via the supported actions. + + @tags{Accessibility} +*/ +class JUCE_API AccessibilityActions +{ +public: + /** Constructor. + + Creates a default AccessibilityActions object with no action callbacks. + */ + AccessibilityActions() = default; + + /** Adds an action. + + When the user performs this action with an accessibility client + `actionCallback` will be called. + + Returns a reference to itself so that several calls can be chained. + */ + AccessibilityActions& addAction (AccessibilityActionType type, + std::function actionCallback) + { + actionMap[type] = std::move (actionCallback); + return *this; + } + + /** Returns true if the specified action is supported. */ + bool contains (AccessibilityActionType type) const + { + return actionMap.find (type) != actionMap.end(); + } + + /** If an action has been registered for the provided action type, invokes the + action and returns true. Otherwise, returns false. + */ + bool invoke (AccessibilityActionType type) const + { + auto iter = actionMap.find (type); + + if (iter == actionMap.end()) + return false; + + iter->second(); + return true; + } + +private: + std::map> actionMap; +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/accessibility/enums/juce_AccessibilityEvent.h b/modules/juce_gui_basics/accessibility/enums/juce_AccessibilityEvent.h new file mode 100644 index 0000000000..cbcb36a41b --- /dev/null +++ b/modules/juce_gui_basics/accessibility/enums/juce_AccessibilityEvent.h @@ -0,0 +1,75 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** A list of events that can be notified to any subscribed accessibility clients. + + To post a notification, call `AccessibilityHandler::notifyAccessibilityEvent` + on the associated handler with the appropriate `AccessibilityEvent` type and + listening clients will be notified. + + @tags{Accessibility} +*/ +enum class AccessibilityEvent +{ + /** Indicates that the UI element's value has changed. + + This should be called on the handler that implements `AccessibilityValueInterface` + for the UI element that has changed. + */ + valueChanged, + + /** Indicates that the structure of the UI elements has changed in a + significant way. + + This should be posted on the top-level handler whose structure has changed. + */ + structureChanged, + + /** Indicates that the selection of a text element has changed. + + This should be called on the handler that implements `AccessibilityTextInterface` + for the text element that has changed. + */ + textSelectionChanged, + + /** Indicates that the visible text of a text element has changed. + + This should be called on the handler that implements `AccessibilityTextInterface` + for the text element that has changed. + */ + textChanged, + + /** Indicates that the selection of rows in a list or table has changed. + + This should be called on the handler that implements `AccessibilityTableInterface` + for the UI element that has changed. + */ + rowSelectionChanged +}; + +} diff --git a/modules/juce_gui_basics/accessibility/enums/juce_AccessibilityRole.h b/modules/juce_gui_basics/accessibility/enums/juce_AccessibilityRole.h new file mode 100644 index 0000000000..b4354934a1 --- /dev/null +++ b/modules/juce_gui_basics/accessibility/enums/juce_AccessibilityRole.h @@ -0,0 +1,70 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** The list of available roles for an AccessibilityHandler object. + + When creating a custom AccessibilityHandler you should select the role that + best describes the UI element being represented. + + @tags{Accessibility} +*/ +enum class AccessibilityRole +{ + button, + toggleButton, + radioButton, + comboBox, + image, + slider, + staticText, + editableText, + menuItem, + menuBar, + popupMenu, + table, + tableHeader, + column, + row, + cell, + hyperlink, + list, + listItem, + tree, + treeItem, + progressBar, + group, + dialogWindow, + window, + scrollBar, + tooltip, + splashScreen, + ignored, + unspecified +}; + +} diff --git a/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityCellInterface.h b/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityCellInterface.h new file mode 100644 index 0000000000..f562372091 --- /dev/null +++ b/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityCellInterface.h @@ -0,0 +1,61 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** An abstract interface which represents a UI element that supports a cell interface. + + This typically represents a single cell inside of a UI element which implements an + AccessibilityTableInterface. + + @tags{Accessibility} +*/ +class JUCE_API AccessibilityCellInterface +{ +public: + /** Destructor. */ + virtual ~AccessibilityCellInterface() = default; + + /** Returns the column index of the cell in the table. */ + virtual int getColumnIndex() const = 0; + + /** Returns the number of columns occupied by the cell in the table. */ + virtual int getColumnSpan() const = 0; + + /** Returns the row index of the cell in the table. */ + virtual int getRowIndex() const = 0; + + /** Returns the number of rows occupied by the cell in the table. */ + virtual int getRowSpan() const = 0; + + /** Returns the indentation level for the cell. */ + virtual int getDisclosureLevel() const = 0; + + /** Returns the AccessibilityHandler of the table which contains the cell. */ + virtual const AccessibilityHandler* getTableHandler() const = 0; +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTableInterface.h b/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTableInterface.h new file mode 100644 index 0000000000..05afe40b51 --- /dev/null +++ b/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTableInterface.h @@ -0,0 +1,54 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** An abstract interface which represents a UI element that supports a table interface. + + Examples of UI elements which typically support a table interface are lists, tables, + and trees. + + @tags{Accessibility} +*/ +class JUCE_API AccessibilityTableInterface +{ +public: + /** Destructor. */ + virtual ~AccessibilityTableInterface() = default; + + /** Returns the total number of rows in the table. */ + virtual int getNumRows() const = 0; + + /** Returns the total number of columns in the table. */ + virtual int getNumColumns() const = 0; + + /** Returns the AccessibilityHandler for one of the cells in the table, or + nullptr if there is no cell at the specified position. + */ + virtual const AccessibilityHandler* getCellHandler (int row, int column) const = 0; +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTextInterface.h b/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTextInterface.h new file mode 100644 index 0000000000..b77d6ca8b1 --- /dev/null +++ b/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTextInterface.h @@ -0,0 +1,78 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** An abstract interface which represents a UI element that supports a text interface. + + A UI element can use this interface to provide extended textual information which + cannot be conveyed using just the title, description, and help text properties of + AccessibilityHandler. This is typically for text that an accessibility client might + want to read line-by-line, or provide text selection and input for. + + @tags{Accessibility} +*/ +class JUCE_API AccessibilityTextInterface +{ +public: + /** Destructor. */ + virtual ~AccessibilityTextInterface() = default; + + /** Returns true if the text being displayed is protected and should not be + exposed to the user, for example a password entry field. + */ + virtual bool isDisplayingProtectedText() const = 0; + + /** Returns the total number of characters in the text element. */ + virtual int getTotalNumCharacters() const = 0; + + /** Returns the range of characters that are currently selected, or an empty + range if nothing is selected. + */ + virtual Range getSelection() const = 0; + + /** Selects a section of the text. */ + virtual void setSelection (Range newRange) = 0; + + /** Gets the current text insertion position, if supported. */ + virtual int getTextInsertionOffset() const = 0; + + /** Returns a section of text. */ + virtual String getText (Range range) const = 0; + + /** Replaces the text with a new string. */ + virtual void setText (const String& newText) = 0; + + /** Returns the bounding box in screen coordinates for a range of text. + As the range may span multiple lines, this method returns a RectangleList. + */ + virtual RectangleList getTextBounds (Range textRange) const = 0; + + /** Returns the index of the character at a given position in screen coordinates. */ + virtual int getOffsetAtPoint (Point point) const = 0; +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityValueInterface.h b/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityValueInterface.h new file mode 100644 index 0000000000..b8a9b7d138 --- /dev/null +++ b/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityValueInterface.h @@ -0,0 +1,222 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** An abstract interface representing the value of an accessibility element. + + Values should be used when information needs to be conveyed which cannot + be represented by the accessibility element's label alone. For example, a + gain slider with the label "Gain" needs to also provide a value for its + position whereas a "Save" button does not. + + This class allows for full control over the value text/numeric conversion, + ranged, and read-only properties but in most cases you'll want to use one + of the derived classes below which handle some of this for you. + + @see AccessibilityTextValueInterface, AccessibilityNumericValueInterface, + AccessibilityRangedNumericValueInterface + + @tags{Accessibility} +*/ +class JUCE_API AccessibilityValueInterface +{ +public: + /** Destructor. */ + virtual ~AccessibilityValueInterface() = default; + + /** Returns true if the value is read-only and cannot be modified by an + accessibility client. + + @see setValue, setValueAsString + */ + virtual bool isReadOnly() const = 0; + + /** Returns the current value as a double. */ + virtual double getCurrentValue() const = 0; + + /** Returns the current value as a String. */ + virtual String getCurrentValueAsString() const = 0; + + /** Sets the current value to a new double value. */ + virtual void setValue (double newValue) = 0; + + /** Sets the current value to a new String value. */ + virtual void setValueAsString (const String& newValue) = 0; + + /** Represents the range of this value, if supported. + + Return one of these from the `getRange()` method, providing a minimum, + maximum, and interval value for the range to indicate that this is a + ranged value. + + The default state is an "invalid" range, indicating that the accessibility + element does not support ranged values. + + @see AccessibilityRangedNumericValueInterface + + @tags{Accessibility} + */ + class JUCE_API AccessibleValueRange + { + public: + /** Constructor. + + Creates a default, "invalid" range that can be returned from + `AccessibilityValueInterface::getRange()` to indicate that the value + interface does not support ranged values. + */ + AccessibleValueRange() = default; + + /** The minimum and maximum values for this range, inclusive. */ + struct JUCE_API MinAndMax { double min, max; }; + + /** Constructor. + + Creates a valid AccessibleValueRange with the provided minimum, maximum, + and interval values. + */ + AccessibleValueRange (MinAndMax valueRange, double interval) + : valid (true), + range (valueRange), + stepSize (interval) + { + jassert (range.min < range.max); + } + + /** Returns true if this represents a valid range. */ + bool isValid() const noexcept { return valid; } + + /** Returns the minimum value for this range. */ + double getMinimumValue() const noexcept { return range.min; } + + /** Returns the maxiumum value for this range. */ + double getMaximumValue() const noexcept { return range.max; } + + /** Returns the interval for this range. */ + double getInterval() const noexcept { return stepSize; } + + private: + bool valid = false; + MinAndMax range {}; + double stepSize = 0.0; + }; + + /** If this is a ranged value, this should return a valid AccessibleValueRange + object representing the supported numerical range. + */ + virtual AccessibleValueRange getRange() const = 0; +}; + +//============================================================================== +/** A value interface that represents a text value. + + @tags{Accessibility} +*/ +class JUCE_API AccessibilityTextValueInterface : public AccessibilityValueInterface +{ +public: + /** Returns true if the value is read-only and cannot be modified by an + accessibility client. + + @see setValueAsString + */ + bool isReadOnly() const override = 0; + + /** Returns the current value. */ + String getCurrentValueAsString() const override = 0; + + /** Sets the current value to a new value. */ + void setValueAsString (const String& newValue) override = 0; + + /** @internal */ + double getCurrentValue() const final { return getCurrentValueAsString().getDoubleValue(); } + /** @internal */ + void setValue (double newValue) final { setValueAsString (String (newValue)); } + /** @internal */ + AccessibleValueRange getRange() const final { return {}; } +}; + +//============================================================================== +/** A value interface that represents a non-ranged numeric value. + + @tags{Accessibility} +*/ +class JUCE_API AccessibilityNumericValueInterface : public AccessibilityValueInterface +{ +public: + /** Returns true if the value is read-only and cannot be modified by an + accessibility client. + + @see setValue + */ + bool isReadOnly() const override = 0; + + /** Returns the current value. */ + double getCurrentValue() const override = 0; + + /** Sets the current value to a new value. */ + void setValue (double newValue) override = 0; + + /** @internal */ + String getCurrentValueAsString() const final { return String (getCurrentValue()); } + /** @internal */ + void setValueAsString (const String& newValue) final { setValue (newValue.getDoubleValue()); } + /** @internal */ + AccessibleValueRange getRange() const final { return {}; } +}; + +//============================================================================== +/** A value interface that represents a ranged numeric value. + + @tags{Accessibility} +*/ +class JUCE_API AccessibilityRangedNumericValueInterface : public AccessibilityValueInterface +{ +public: + /** Returns true if the value is read-only and cannot be modified by an + accessibility client. + + @see setValueAsString + */ + bool isReadOnly() const override = 0; + + /** Returns the current value. */ + double getCurrentValue() const override = 0; + + /** Sets the current value to a new value. */ + void setValue (double newValue) override = 0; + + /** Returns the range. */ + AccessibleValueRange getRange() const override = 0; + + /** @internal */ + String getCurrentValueAsString() const final { return String (getCurrentValue()); } + /** @internal */ + void setValueAsString (const String& newValue) final { setValue (newValue.getDoubleValue()); } +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.cpp b/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.cpp new file mode 100644 index 0000000000..fe464bc880 --- /dev/null +++ b/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.cpp @@ -0,0 +1,346 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +AccessibilityHandler* AccessibilityHandler::currentlyFocusedHandler = nullptr; + +enum class InternalAccessibilityEvent +{ + elementCreated, + elementDestroyed, + focusChanged, + windowOpened, + windowClosed +}; + +void notifyAccessibilityEventInternal (const AccessibilityHandler& handler, InternalAccessibilityEvent event); + +inline String getAccessibleApplicationOrPluginName() +{ + #if defined (JucePlugin_Name) + return JucePlugin_Name; + #else + if (auto* app = JUCEApplicationBase::getInstance()) + return app->getApplicationName(); + + return "JUCE Application"; + #endif +} + +AccessibilityHandler::AccessibilityHandler (Component& comp, + AccessibilityRole accessibilityRole, + AccessibilityActions accessibilityActions, + Interfaces interfacesIn) + : component (comp), + typeIndex (typeid (component)), + role (accessibilityRole), + actions (std::move (accessibilityActions)), + interfaces (std::move (interfacesIn)), + nativeImpl (createNativeImpl (*this)) +{ + notifyAccessibilityEventInternal (*this, InternalAccessibilityEvent::elementCreated); +} + +AccessibilityHandler::~AccessibilityHandler() +{ + giveAwayFocus(); + notifyAccessibilityEventInternal (*this, InternalAccessibilityEvent::elementDestroyed); +} + +//============================================================================== +AccessibleState AccessibilityHandler::getCurrentState() const +{ + auto state = AccessibleState().withFocusable(); + + return hasFocus (false) ? state.withFocused() : state; +} + +static bool isComponentVisibleWithinWindow (const Component& comp) +{ + if (auto* peer = comp.getPeer()) + return ! peer->getAreaCoveredBy (comp).getIntersection (peer->getComponent().getLocalBounds()).isEmpty(); + + return false; +} + +static bool isComponentVisibleWithinParent (Component* comp) +{ + if (auto* parent = comp->getParentComponent()) + { + if (comp->getBoundsInParent().getIntersection (parent->getLocalBounds()).isEmpty()) + return false; + + return isComponentVisibleWithinParent (parent); + } + + return true; +} + +bool AccessibilityHandler::isIgnored() const +{ + const auto state = getCurrentState(); + + return role == AccessibilityRole::ignored + || state.isIgnored() + || ! component.isShowing() + || (! state.isAccessibleOffscreen() + && (! isComponentVisibleWithinParent (&component) + || ! isComponentVisibleWithinWindow (component))); +} + +//============================================================================== +const AccessibilityActions& AccessibilityHandler::getActions() const noexcept +{ + return actions; +} + +AccessibilityValueInterface* AccessibilityHandler::getValueInterface() const +{ + return interfaces.value.get(); +} + +AccessibilityTableInterface* AccessibilityHandler::getTableInterface() const +{ + return interfaces.table.get(); +} + +AccessibilityCellInterface* AccessibilityHandler::getCellInterface() const +{ + return interfaces.cell.get(); +} + +AccessibilityTextInterface* AccessibilityHandler::getTextInterface() const +{ + return interfaces.text.get(); +} + +//============================================================================== +static AccessibilityHandler* findEnclosingHandler (Component* comp) +{ + if (comp != nullptr) + { + if (auto* handler = comp->getAccessibilityHandler()) + return handler; + + return findEnclosingHandler (comp->getParentComponent()); + } + + return nullptr; +} + +static AccessibilityHandler* getUnignoredAncestor (AccessibilityHandler* handler) +{ + while (handler != nullptr + && handler->isIgnored() + && handler->getParent() != nullptr) + { + handler = handler->getParent(); + } + + return handler; +} + +static AccessibilityHandler* findFirstUnignoredChild (const std::vector& handlers) +{ + if (! handlers.empty()) + { + const auto iter = std::find_if (handlers.cbegin(), handlers.cend(), + [] (const AccessibilityHandler* handler) { return ! handler->isIgnored(); }); + + if (iter != handlers.cend()) + return *iter; + + for (auto* handler : handlers) + if (auto* unignored = findFirstUnignoredChild (handler->getChildren())) + return unignored; + } + + return nullptr; +} + +static AccessibilityHandler* getFirstUnignoredDescendant (AccessibilityHandler* handler) +{ + if (handler != nullptr && handler->isIgnored()) + return findFirstUnignoredChild (handler->getChildren()); + + return handler; +} + +AccessibilityHandler* AccessibilityHandler::getParent() const +{ + if (auto* focusContainer = component.findFocusContainer()) + return getUnignoredAncestor (findEnclosingHandler (focusContainer)); + + return nullptr; +} + +std::vector AccessibilityHandler::getChildren() const +{ + if (! component.isFocusContainer() && component.getParentComponent() != nullptr) + return {}; + + std::vector children; + + if (auto traverser = component.createFocusTraverser()) + { + for (auto* focusableChild : traverser->getAllComponents (&component)) + { + if (auto* handler = findEnclosingHandler (focusableChild)) + { + if (! isParentOf (handler)) + continue; + + if (auto* unignored = getFirstUnignoredDescendant (handler)) + if (std::find (children.cbegin(), children.cend(), unignored) == children.cend()) + children.push_back (unignored); + } + } + } + + return children; +} + +bool AccessibilityHandler::isParentOf (const AccessibilityHandler* possibleChild) const noexcept +{ + while (possibleChild != nullptr) + { + possibleChild = possibleChild->getParent(); + + if (possibleChild == this) + return true; + } + + return false; +} + +AccessibilityHandler* AccessibilityHandler::getChildAt (Point screenPoint) +{ + if (auto* comp = Desktop::getInstance().findComponentAt (screenPoint)) + if (isParentOf (comp->getAccessibilityHandler())) + return getUnignoredAncestor (findEnclosingHandler (comp)); + + return nullptr; +} + +AccessibilityHandler* AccessibilityHandler::getChildFocus() +{ + return hasFocus (true) ? getUnignoredAncestor (currentlyFocusedHandler) + : nullptr; +} + +bool AccessibilityHandler::hasFocus (bool trueIfChildFocused) const +{ + return currentlyFocusedHandler != nullptr + && (currentlyFocusedHandler == this + || (trueIfChildFocused && isParentOf (currentlyFocusedHandler))); +} + +void AccessibilityHandler::grabFocus() +{ + if (! hasFocus (false)) + grabFocusInternal (true); +} + +void AccessibilityHandler::giveAwayFocus() const +{ + if (hasFocus (true)) + giveAwayFocusInternal(); +} + +void AccessibilityHandler::grabFocusInternal (bool canTryParent) +{ + if (getCurrentState().isFocusable() && ! isIgnored()) + { + takeFocus(); + return; + } + + if (isParentOf (currentlyFocusedHandler) && ! currentlyFocusedHandler->isIgnored()) + return; + + if (component.isFocusContainer() || component.getParentComponent() == nullptr) + { + if (auto traverser = component.createFocusTraverser()) + { + if (auto* defaultComp = traverser->getDefaultComponent (&component)) + { + if (auto* handler = getUnignoredAncestor (findEnclosingHandler (defaultComp))) + { + if (isParentOf (handler)) + { + handler->grabFocusInternal (false); + return; + } + } + } + } + } + + if (canTryParent) + if (auto* parent = getParent()) + parent->grabFocusInternal (true); +} + +void AccessibilityHandler::giveAwayFocusInternal() const +{ + currentlyFocusedHandler = nullptr; + notifyAccessibilityEventInternal (*this, InternalAccessibilityEvent::focusChanged); + + if (auto* focusedComponent = Component::getCurrentlyFocusedComponent()) + if (auto* handler = focusedComponent->getAccessibilityHandler()) + handler->grabFocus(); +} + +void AccessibilityHandler::takeFocus() +{ + currentlyFocusedHandler = this; + + WeakReference weakComponent (&component); + actions.invoke (AccessibilityActionType::focus); + + if (weakComponent != nullptr + && component.getWantsKeyboardFocus() + && ! component.hasKeyboardFocus (true)) + { + component.grabKeyboardFocus(); + } + + notifyAccessibilityEventInternal (*this, InternalAccessibilityEvent::focusChanged); +} + +//============================================================================== +#if ! (JUCE_MAC || JUCE_WINDOWS) +class AccessibilityHandler::AccessibilityNativeImpl { public: AccessibilityNativeImpl (AccessibilityHandler&) {} }; +void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent) const {} +void AccessibilityHandler::postAnnouncement (const String&, AnnouncementPriority) {} +AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const { return nullptr; } +AccessibilityHandler::AccessibilityNativeImpl* AccessibilityHandler::createNativeImpl (AccessibilityHandler&) { return nullptr; } +void AccessibilityHandler::DestroyNativeImpl::operator() (AccessibilityHandler::AccessibilityNativeImpl*) const noexcept {} +void notifyAccessibilityEventInternal (const AccessibilityHandler&, InternalAccessibilityEvent) {} +#endif + +} // namespace juce diff --git a/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.h b/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.h new file mode 100644 index 0000000000..111c7f87b3 --- /dev/null +++ b/modules/juce_gui_basics/accessibility/juce_AccessibilityHandler.h @@ -0,0 +1,325 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +class AccessibilityNativeHandle; + +/** Base class for accessible Components. + + This class wraps a Component and provides methods that allow an accessibility client, + such as VoiceOver on macOS, or Narrator on Windows, to control it. + + It handles hierarchical navigation, properties, state, and various interfaces. + + @tags{Accessibility} +*/ +class JUCE_API AccessibilityHandler +{ +public: + /** Utility struct which holds one or more accessibility interfaces. + + The main purpose of this class is to provide convenience constructors from each + of the four types of accessibility interface. + */ + struct JUCE_API Interfaces + { + Interfaces() = default; + + Interfaces (std::unique_ptr ptr) : value (std::move (ptr)) {} + Interfaces (std::unique_ptr ptr) : text (std::move (ptr)) {} + Interfaces (std::unique_ptr ptr) : table (std::move (ptr)) {} + Interfaces (std::unique_ptr ptr) : cell (std::move (ptr)) {} + + Interfaces (std::unique_ptr valueIn, + std::unique_ptr textIn, + std::unique_ptr tableIn, + std::unique_ptr cellIn) + : value (std::move (valueIn)), + text (std::move (textIn)), + table (std::move (tableIn)), + cell (std::move (cellIn)) + { + } + + std::unique_ptr value; + std::unique_ptr text; + std::unique_ptr table; + std::unique_ptr cell; + }; + + /** Constructor. + + This will create a AccessibilityHandler which wraps the provided Component and makes + it visible to accessibility clients. You must also specify a role for the UI element + from the `AccessibilityRole` list which best describes it. + + To enable users to interact with the UI element you should provide the set of supported + actions and their associated callbacks via the `accessibilityActions` parameter. + + For UI elements that support more complex interaction the value, text, table, and cell + interfaces should be implemented as required and passed as the final argument of this + constructor. See the documentation of these classes for more information about the + types of control they represent and which methods need to be implemented. + */ + AccessibilityHandler (Component& componentToWrap, + AccessibilityRole accessibilityRole, + AccessibilityActions actions = {}, + Interfaces interfaces = {}); + + /** Destructor. */ + virtual ~AccessibilityHandler(); + + //============================================================================== + /** Returns the Component that this handler represents. */ + const Component& getComponent() const noexcept { return component; } + + /** Returns the Component that this handler represents. */ + Component& getComponent() noexcept { return component; } + + //============================================================================== + /** The type of UI element that this accessibility handler represents. + + @see AccessibilityRole + */ + AccessibilityRole getRole() const noexcept { return role; } + + /** The title of the UI element. + + This will be read out by the system and should be concise, preferably matching + the visible title of the UI element (if any). For example, this might be the + text of a button or a simple label. + + The default implementation will call `Component::getTitle()`, but you can override + this to return a different string if required. + + If neither a name nor a description is provided then the UI element may be + ignored by accessibility clients. + + This must be a localised string. + */ + virtual String getTitle() const { return component.getTitle(); } + + /** A short description of the UI element. + + This may be read out by the system. It should not include the type of the UI + element and should ideally be a single word, for example "Open" for a button + that opens a window. + + The default implementation will call `Component::getDescription()`, but you + can override this to return a different string if required. + + If neither a name nor a description is provided then the UI element may be + ignored by accessibility clients. + + This must be a localised string. + */ + virtual String getDescription() const { return component.getDescription(); } + + /** Some help text for the UI element (if required). + + This may be read out by the system. This string functions in a similar way to + a tooltip, for example "Click to open window." for a button which opens a window. + + The default implementation will call `Component::getHelpText()`, but you can + override this to return a different string if required. + + This must be a localised string. + */ + virtual String getHelp() const { return component.getHelpText(); } + + /** Returns the current state of the UI element. + + The default implementation of this method will set the focusable flag and, if + this UI element is currently focused, will also set the focused flag. + */ + virtual AccessibleState getCurrentState() const; + + /** Returns true if this UI element should be ignored by accessibility clients. */ + bool isIgnored() const; + + //============================================================================== + /** Returns the set of actions that the UI element supports and the associated + callbacks. + */ + const AccessibilityActions& getActions() const noexcept; + + /** Returns the value interface for this UI element, or nullptr if it is not supported. + + @see AccessibilityValueInterface + */ + AccessibilityValueInterface* getValueInterface() const; + + /** Returns the table interface for this UI element, or nullptr if it is not supported. + + @see AccessibilityTableInterface + */ + AccessibilityTableInterface* getTableInterface() const; + + /** Returns the cell interface for this UI element, or nullptr if it is not supported. + + @see AccessibilityCellInterface + */ + AccessibilityCellInterface* getCellInterface() const; + + /** Returns the text interface for this UI element, or nullptr if it is not supported. + + @see AccessibilityTextInterface + */ + AccessibilityTextInterface* getTextInterface() const; + + //============================================================================== + /** Returns the first unignored parent of this UI element in the accessibility hierarchy, + or nullptr if this is a root element without a parent. + */ + AccessibilityHandler* getParent() const; + + /** Returns the unignored children of this UI element in the accessibility hierarchy. */ + std::vector getChildren() const; + + /** Checks whether a given UI element is a child of this one in the accessibility + hierarchy. + */ + bool isParentOf (const AccessibilityHandler* possibleChild) const noexcept; + + /** Returns the deepest child of this UI element in the accessibility hierarchy that + contains the given screen point, or nullptr if there is no child at this point. + */ + AccessibilityHandler* getChildAt (Point screenPoint); + + /** Returns the deepest UI element which currently has focus. + + This can be a child of this UI element or, if no child is focused, + this element itself. + + Note that this can be different to the value of the Component with keyboard + focus returned by Component::getCurrentlyFocusedComponent(). + + @see hasFocus + */ + AccessibilityHandler* getChildFocus(); + + /** Returns true if this UI element has the focus. + + @param trueIfChildFocused if this is true, this method will also return true + if any child of this UI element in the accessibility + hierarchy has focus + */ + bool hasFocus (bool trueIfChildFocused) const; + + /** Tries to give focus to this UI element. + + If the UI element is focusable, as indicated by AccessibleState::isFocusable(), + this will perform its AccessibilityActionType::focus action, try to give keyboard + focus to the Component it represents, and notify any listening accessibility + clients that the current focus has changed. + + @see hasFocus, giveAwayFocus + */ + void grabFocus(); + + /** If this UI element or any of its children in the accessibility hierarchy currently + have focus, this will defocus it. + + This will also give away the keyboard focus from the Component it represents, and + notify any listening accessibility clients that the current focus has changed. + + @see hasFocus, grabFocus + */ + void giveAwayFocus() const; + + //============================================================================== + /** Used to send a notification to any observing accessibility clients that something + has changed in the UI element. + + @see AccessibilityEvent + */ + void notifyAccessibilityEvent (AccessibilityEvent event) const; + + /** A priority level that can help an accessibility client determine how to handle + an announcement request. + + Exactly what this controls is platform-specific, but generally a low priority + announcement will be read when the screen reader is free, whereas a high priority + announcement will interrupt the current speech. + */ + enum class AnnouncementPriority + { + low, + medium, + high + }; + + /** Posts an announcement to be made to the user. + + @param announcementString a localised string containing the announcement to be read out + @param priority the appropriate priority level for the announcement + */ + static void postAnnouncement (const String& announcementString, AnnouncementPriority priority); + + //============================================================================== + /** @internal */ + AccessibilityNativeHandle* getNativeImplementation() const; + /** @internal */ + std::type_index getTypeIndex() const { return typeIndex; } + +private: + //============================================================================== + friend class AccessibilityNativeHandle; + + //============================================================================== + void grabFocusInternal (bool); + void giveAwayFocusInternal() const; + void takeFocus(); + + static AccessibilityHandler* currentlyFocusedHandler; + + //============================================================================== + Component& component; + std::type_index typeIndex; + + const AccessibilityRole role; + AccessibilityActions actions; + + Interfaces interfaces; + + //============================================================================== + class AccessibilityNativeImpl; + + struct DestroyNativeImpl + { + void operator() (AccessibilityNativeImpl*) const noexcept; + }; + + static AccessibilityNativeImpl* createNativeImpl (AccessibilityHandler&); + + std::unique_ptr nativeImpl; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityHandler) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/accessibility/juce_AccessibilityState.h b/modules/juce_gui_basics/accessibility/juce_AccessibilityState.h new file mode 100644 index 0000000000..769c605508 --- /dev/null +++ b/modules/juce_gui_basics/accessibility/juce_AccessibilityState.h @@ -0,0 +1,227 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** Represents the state of an accessible UI element. + + An instance of this class is returned by `AccessibilityHandler::getCurrentState()` + to convey its current state to an accessibility client. + + @see AccessibilityHandler + + @tags{Accessibility} +*/ +class JUCE_API AccessibleState +{ +public: + /** Constructor. + + Represents a "default" state with no flags set. To set a flag, use one of the + `withX()` methods - these can be chained together to set multiple flags. + */ + AccessibleState() = default; + + //============================================================================== + /** Sets the checkable flag and returns the new state. + + @see isCheckable + */ + AccessibleState withCheckable() const noexcept { return withFlag (Flags::checkable); } + + /** Sets the checked flag and returns the new state. + + @see isChecked + */ + AccessibleState withChecked() const noexcept { return withFlag (Flags::checked); } + + /** Sets the collapsed flag and returns the new state. + + @see isCollapsed + */ + AccessibleState withCollapsed() const noexcept { return withFlag (Flags::collapsed); } + + /** Sets the expandable flag and returns the new state. + + @see isExpandable + */ + AccessibleState withExpandable() const noexcept { return withFlag (Flags::expandable); } + + /** Sets the expanded flag and returns the new state. + + @see isExpanded + */ + AccessibleState withExpanded() const noexcept { return withFlag (Flags::expanded); } + + /** Sets the focusable flag and returns the new state. + + @see isFocusable + */ + AccessibleState withFocusable() const noexcept { return withFlag (Flags::focusable); } + + /** Sets the focused flag and returns the new state. + + @see isFocused + */ + AccessibleState withFocused() const noexcept { return withFlag (Flags::focused); } + + /** Sets the ignored flag and returns the new state. + + @see isIgnored + */ + AccessibleState withIgnored() const noexcept { return withFlag (Flags::ignored); } + + /** Sets the selectable flag and returns the new state. + + @see isSelectable + */ + AccessibleState withSelectable() const noexcept { return withFlag (Flags::selectable); } + + /** Sets the multiSelectable flag and returns the new state. + + @see isMultiSelectable + */ + AccessibleState withMultiSelectable() const noexcept { return withFlag (Flags::multiSelectable); } + + /** Sets the selected flag and returns the new state. + + @see isSelected + */ + AccessibleState withSelected() const noexcept { return withFlag (Flags::selected); } + + /** Sets the accessible offscreen flag and returns the new state. + + @see isSelected + */ + AccessibleState withAccessibleOffscreen() const noexcept { return withFlag (Flags::accessibleOffscreen); } + + //============================================================================== + /** Returns true if the UI element is checkable. + + @see withCheckable + */ + bool isCheckable() const noexcept { return isFlagSet (Flags::checkable); } + + /** Returns true if the UI element is checked. + + @see withChecked + */ + bool isChecked() const noexcept { return isFlagSet (Flags::checked); } + + /** Returns true if the UI element is collapsed. + + @see withCollapsed + */ + bool isCollapsed() const noexcept { return isFlagSet (Flags::collapsed); } + + /** Returns true if the UI element is expandable. + + @see withExpandable + */ + bool isExpandable() const noexcept { return isFlagSet (Flags::expandable); } + + /** Returns true if the UI element is expanded. + + @see withExpanded + */ + bool isExpanded() const noexcept { return isFlagSet (Flags::expanded); } + + /** Returns true if the UI element is focusable. + + @see withFocusable + */ + bool isFocusable() const noexcept { return isFlagSet (Flags::focusable); } + + /** Returns true if the UI element is focused. + + @see withFocused + */ + bool isFocused() const noexcept { return isFlagSet (Flags::focused); } + + /** Returns true if the UI element is ignored. + + @see withIgnored + */ + bool isIgnored() const noexcept { return isFlagSet (Flags::ignored); } + + /** Returns true if the UI element supports multiple item selection. + + @see withMultiSelectable + */ + bool isMultiSelectable() const noexcept { return isFlagSet (Flags::multiSelectable); } + + /** Returns true if the UI element is selectable. + + @see withSelectable + */ + bool isSelectable() const noexcept { return isFlagSet (Flags::selectable); } + + /** Returns true if the UI element is selected. + + @see withSelected + */ + bool isSelected() const noexcept { return isFlagSet (Flags::selected); } + + /** Returns true if the UI element is accessible offscreen. + + @see withSelected + */ + bool isAccessibleOffscreen() const noexcept { return isFlagSet (Flags::accessibleOffscreen); } + +private: + enum Flags + { + checkable = (1 << 0), + checked = (1 << 1), + collapsed = (1 << 2), + expandable = (1 << 3), + expanded = (1 << 4), + focusable = (1 << 5), + focused = (1 << 6), + ignored = (1 << 7), + multiSelectable = (1 << 8), + selectable = (1 << 9), + selected = (1 << 10), + accessibleOffscreen = (1 << 11) + }; + + AccessibleState withFlag (int flag) const noexcept + { + auto copy = *this; + copy.flags |= flag; + + return copy; + } + + bool isFlagSet (int flag) const noexcept + { + return (flags & flag) != 0; + } + + int flags = 0; +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/accessibility/widget_handlers/juce_ButtonAccessibilityHandler.h b/modules/juce_gui_basics/accessibility/widget_handlers/juce_ButtonAccessibilityHandler.h new file mode 100644 index 0000000000..db7e025213 --- /dev/null +++ b/modules/juce_gui_basics/accessibility/widget_handlers/juce_ButtonAccessibilityHandler.h @@ -0,0 +1,96 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** Basic accessible interface for a Button that can be clicked or toggled. + + @tags{Accessibility} +*/ +class JUCE_API ButtonAccessibilityHandler : public AccessibilityHandler +{ +public: + explicit ButtonAccessibilityHandler (Button& buttonToWrap) + : AccessibilityHandler (buttonToWrap, + getButtonRole (buttonToWrap), + getAccessibilityActions (buttonToWrap)), + button (buttonToWrap) + { + } + + AccessibleState getCurrentState() const override + { + auto state = AccessibilityHandler::getCurrentState(); + + if (button.getClickingTogglesState()) + { + state = state.withCheckable(); + + if (button.getToggleState()) + state = state.withChecked(); + } + + return state; + } + + String getTitle() const override + { + auto title = AccessibilityHandler::getTitle(); + + if (title.isEmpty()) + return button.getButtonText(); + + return title; + } + +private: + static AccessibilityRole getButtonRole (const Button& b) + { + if (b.getRadioGroupId() != 0) return AccessibilityRole::radioButton; + if (b.getClickingTogglesState()) return AccessibilityRole::toggleButton; + + return AccessibilityRole::button; + } + + static AccessibilityActions getAccessibilityActions (Button& button) + { + auto actions = AccessibilityActions().addAction (AccessibilityActionType::press, + [&button] { button.triggerClick(); }); + + if (button.getClickingTogglesState()) + actions = actions.addAction (AccessibilityActionType::toggle, + [&button] { button.setToggleState (! button.getToggleState(), sendNotification); }); + + return actions; + } + + Button& button; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ButtonAccessibilityHandler) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/accessibility/widget_handlers/juce_ComboBoxAccessibilityHandler.h b/modules/juce_gui_basics/accessibility/widget_handlers/juce_ComboBoxAccessibilityHandler.h new file mode 100644 index 0000000000..fda95013ba --- /dev/null +++ b/modules/juce_gui_basics/accessibility/widget_handlers/juce_ComboBoxAccessibilityHandler.h @@ -0,0 +1,66 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** Basic accessible interface for a ComboBox that can show a menu. + + @tags{Accessibility} +*/ +class JUCE_API ComboBoxAccessibilityHandler : public AccessibilityHandler +{ +public: + explicit ComboBoxAccessibilityHandler (ComboBox& comboBoxToWrap) + : AccessibilityHandler (comboBoxToWrap, + AccessibilityRole::comboBox, + getAccessibilityActions (comboBoxToWrap)), + comboBox (comboBoxToWrap) + { + } + + AccessibleState getCurrentState() const override + { + auto state = AccessibilityHandler::getCurrentState().withExpandable(); + + return comboBox.isPopupActive() ? state.withExpanded() : state.withCollapsed(); + } + + String getTitle() const override { return comboBox.getText(); } + +private: + static AccessibilityActions getAccessibilityActions (ComboBox& comboBox) + { + return AccessibilityActions().addAction (AccessibilityActionType::press, [&comboBox] { comboBox.showPopup(); }) + .addAction (AccessibilityActionType::showMenu, [&comboBox] { comboBox.showPopup(); }); + } + + ComboBox& comboBox; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComboBoxAccessibilityHandler) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/accessibility/widget_handlers/juce_LabelAccessibilityHandler.h b/modules/juce_gui_basics/accessibility/widget_handlers/juce_LabelAccessibilityHandler.h new file mode 100644 index 0000000000..2d4d6b1aa9 --- /dev/null +++ b/modules/juce_gui_basics/accessibility/widget_handlers/juce_LabelAccessibilityHandler.h @@ -0,0 +1,62 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** Basic accessible interface for a Label that can also show a TextEditor + when clicked. + + @tags{Accessibility} +*/ +class JUCE_API LabelAccessibilityHandler : public AccessibilityHandler +{ +public: + explicit LabelAccessibilityHandler (Label& labelToWrap) + : AccessibilityHandler (labelToWrap, + AccessibilityRole::staticText, + getAccessibilityActions (labelToWrap)), + label (labelToWrap) + { + } + + String getTitle() const override { return label.getText(); } + +private: + static AccessibilityActions getAccessibilityActions (Label& label) + { + if (label.isEditable()) + return AccessibilityActions().addAction (AccessibilityActionType::press, [&label] { label.showEditor(); }); + + return {}; + } + + Label& label; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LabelAccessibilityHandler) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/accessibility/widget_handlers/juce_SliderAccessibilityHandler.h b/modules/juce_gui_basics/accessibility/widget_handlers/juce_SliderAccessibilityHandler.h new file mode 100644 index 0000000000..3ee1535f37 --- /dev/null +++ b/modules/juce_gui_basics/accessibility/widget_handlers/juce_SliderAccessibilityHandler.h @@ -0,0 +1,100 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** Basic accessible interface for a Slider. + + @tags{Accessibility} +*/ +class JUCE_API SliderAccessibilityHandler : public AccessibilityHandler +{ +public: + explicit SliderAccessibilityHandler (Slider& sliderToWrap) + : AccessibilityHandler (sliderToWrap, + AccessibilityRole::slider, + {}, + { std::make_unique (sliderToWrap) }) + { + } + +private: + class SliderValueInterface : public AccessibilityValueInterface + { + public: + explicit SliderValueInterface (Slider& sliderToWrap) + : slider (sliderToWrap) + { + } + + bool isReadOnly() const override { return false; } + + double getCurrentValue() const override + { + return slider.isTwoValue() ? slider.getMaxValue() : slider.getValue(); + } + + void setValue (double newValue) override + { + if (slider.isTwoValue()) + slider.setMaxValue (newValue, sendNotification); + else + slider.setValue (newValue, sendNotification); + } + + String getCurrentValueAsString() const override + { + return slider.getTextFromValue (getCurrentValue()); + } + + void setValueAsString (const String& newValue) override + { + setValue (slider.getValueFromText (newValue)); + } + + AccessibleValueRange getRange() const override + { + return { { slider.getMinimum(), slider.getMaximum() }, + getStepSize() }; + } + + private: + double getStepSize() const + { + auto interval = slider.getInterval(); + + return interval != 0.0 ? interval + : slider.proportionOfLengthToValue (0.01); + } + + Slider& slider; + }; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SliderAccessibilityHandler) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/accessibility/widget_handlers/juce_TableListBoxAccessibilityHandler.h b/modules/juce_gui_basics/accessibility/widget_handlers/juce_TableListBoxAccessibilityHandler.h new file mode 100644 index 0000000000..955a6bd96e --- /dev/null +++ b/modules/juce_gui_basics/accessibility/widget_handlers/juce_TableListBoxAccessibilityHandler.h @@ -0,0 +1,83 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** Basic accessible interface for a TableListBox. + + @tags{Accessibility} +*/ +class JUCE_API TableListBoxAccessibilityHandler : public AccessibilityHandler +{ +public: + explicit TableListBoxAccessibilityHandler (TableListBox& tableListBoxToWrap) + : AccessibilityHandler (tableListBoxToWrap, + AccessibilityRole::list, + {}, + { std::make_unique (tableListBoxToWrap) }) + { + } + +private: + class TableListBoxTableInterface : public AccessibilityTableInterface + { + public: + explicit TableListBoxTableInterface (TableListBox& tableListBoxToWrap) + : tableListBox (tableListBoxToWrap) + { + } + + int getNumRows() const override + { + if (auto* model = tableListBox.getModel()) + return model->getNumRows(); + + return 0; + } + + int getNumColumns() const override + { + return tableListBox.getHeader().getNumColumns (false); + } + + const AccessibilityHandler* getCellHandler (int row, int column) const override + { + if (isPositiveAndBelow (row, getNumRows()) && isPositiveAndBelow (column, getNumColumns())) + if (auto* cellComponent = tableListBox.getCellComponent (tableListBox.getHeader().getColumnIdOfIndex (column, false), row)) + return cellComponent->getAccessibilityHandler(); + + return nullptr; + } + + private: + TableListBox& tableListBox; + }; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TableListBoxAccessibilityHandler) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/accessibility/widget_handlers/juce_TextEditorAccessibilityHandler.h b/modules/juce_gui_basics/accessibility/widget_handlers/juce_TextEditorAccessibilityHandler.h new file mode 100644 index 0000000000..83ea7b2858 --- /dev/null +++ b/modules/juce_gui_basics/accessibility/widget_handlers/juce_TextEditorAccessibilityHandler.h @@ -0,0 +1,108 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** Basic accessible interface for a TextEditor. + + @tags{Accessibility} +*/ +class JUCE_API TextEditorAccessibilityHandler : public AccessibilityHandler +{ +public: + explicit TextEditorAccessibilityHandler (TextEditor& textEditorToWrap) + : AccessibilityHandler (textEditorToWrap, + textEditorToWrap.isReadOnly() ? AccessibilityRole::staticText : AccessibilityRole::editableText, + {}, + { textEditorToWrap.isReadOnly() ? nullptr : std::make_unique (textEditorToWrap) }), + textEditor (textEditorToWrap) + { + } + + String getTitle() const override + { + return textEditor.isReadOnly() ? textEditor.getText() : textEditor.getTitle(); + } + +private: + class TextEditorTextInterface : public AccessibilityTextInterface + { + public: + explicit TextEditorTextInterface (TextEditor& editor) + : textEditor (editor) + { + } + + bool isDisplayingProtectedText() const override { return textEditor.getPasswordCharacter() != 0; } + + int getTotalNumCharacters() const override { return textEditor.getText().length(); } + Range getSelection() const override { return textEditor.getHighlightedRegion(); } + void setSelection (Range r) override { textEditor.setHighlightedRegion (r); } + + String getText (Range r) const override + { + if (isDisplayingProtectedText()) + return String::repeatedString (String::charToString (textEditor.getPasswordCharacter()), + getTotalNumCharacters()); + + return textEditor.getTextInRange (r); + } + + void setText (const String& newText) override + { + textEditor.setText (newText); + } + + int getTextInsertionOffset() const override { return textEditor.getCaretPosition(); } + + RectangleList getTextBounds (Range textRange) const override + { + auto localRects = textEditor.getTextBounds (textRange); + RectangleList globalRects; + + std::for_each (localRects.begin(), localRects.end(), + [&] (const Rectangle& r) { globalRects.add (textEditor.localAreaToGlobal (r)); }); + + return globalRects; + } + + int getOffsetAtPoint (Point point) const override + { + auto localPoint = textEditor.getLocalPoint (nullptr, point); + return textEditor.getTextIndexAt (localPoint.x, localPoint.y); + } + + private: + TextEditor& textEditor; + }; + + TextEditor& textEditor; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TextEditorAccessibilityHandler) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/accessibility/widget_handlers/juce_TreeViewAccessibilityHandler.h b/modules/juce_gui_basics/accessibility/widget_handlers/juce_TreeViewAccessibilityHandler.h new file mode 100644 index 0000000000..6f828e0569 --- /dev/null +++ b/modules/juce_gui_basics/accessibility/widget_handlers/juce_TreeViewAccessibilityHandler.h @@ -0,0 +1,79 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** Basic accessible interface for a TreeView. + + @tags{Accessibility} +*/ +class JUCE_API TreeViewAccessibilityHandler : public AccessibilityHandler +{ +public: + explicit TreeViewAccessibilityHandler (TreeView& treeViewToWrap) + : AccessibilityHandler (treeViewToWrap, + AccessibilityRole::tree, + {}, + { std::make_unique (treeViewToWrap) }) + { + } + +private: + class TreeViewTableInterface : public AccessibilityTableInterface + { + public: + explicit TreeViewTableInterface (TreeView& treeViewToWrap) + : treeView (treeViewToWrap) + { + } + + int getNumRows() const override + { + return treeView.getNumRowsInTree(); + } + + int getNumColumns() const override + { + return 1; + } + + const AccessibilityHandler* getCellHandler (int row, int) const override + { + if (auto* itemComp = treeView.getItemComponent (treeView.getItemOnRow (row))) + return itemComp->getAccessibilityHandler(); + + return nullptr; + } + + private: + TreeView& treeView; + }; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TreeViewAccessibilityHandler) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/buttons/juce_Button.cpp b/modules/juce_gui_basics/buttons/juce_Button.cpp index 1df2d6e205..9978d96723 100644 --- a/modules/juce_gui_basics/buttons/juce_Button.cpp +++ b/modules/juce_gui_basics/buttons/juce_Button.cpp @@ -188,6 +188,9 @@ void Button::setToggleState (bool shouldBeOn, NotificationType clickNotification sendStateMessage(); else buttonStateChanged(); + + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::valueChanged); } } @@ -205,6 +208,8 @@ void Button::setClickingTogglesState (bool shouldToggle) noexcept // it is that this button represents, and the button will update its state to reflect this // in the applicationCommandListChanged() method. jassert (commandManagerToUse == nullptr || ! clickTogglesState); + + invalidateAccessibilityHandler(); } bool Button::getClickingTogglesState() const noexcept @@ -220,6 +225,8 @@ void Button::setRadioGroupId (int newGroupId, NotificationType notification) if (lastToggleState) turnOffOtherButtonsInGroup (notification, notification); + + invalidateAccessibilityHandler(); } } @@ -692,4 +699,10 @@ void Button::repeatTimerCallback() } } +//============================================================================== +std::unique_ptr Button::createAccessibilityHandler() +{ + return std::make_unique (*this); +} + } // namespace juce diff --git a/modules/juce_gui_basics/buttons/juce_Button.h b/modules/juce_gui_basics/buttons/juce_Button.h index 341076004c..ba506c57d4 100644 --- a/modules/juce_gui_basics/buttons/juce_Button.h +++ b/modules/juce_gui_basics/buttons/juce_Button.h @@ -470,6 +470,8 @@ protected: void focusLost (FocusChangeType) override; /** @internal */ void enablementChanged() override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: //============================================================================== diff --git a/modules/juce_gui_basics/components/juce_Component.cpp b/modules/juce_gui_basics/components/juce_Component.cpp index 028eac61ef..9b89b4bdbb 100644 --- a/modules/juce_gui_basics/components/juce_Component.cpp +++ b/modules/juce_gui_basics/components/juce_Component.cpp @@ -482,15 +482,15 @@ Component::~Component() componentListeners.call ([this] (ComponentListener& l) { l.componentBeingDeleted (*this); }); - masterReference.clear(); - while (childComponentList.size() > 0) removeChildComponent (childComponentList.size() - 1, false, true); + masterReference.clear(); + if (parentComponent != nullptr) parentComponent->removeChildComponent (parentComponent->childComponentList.indexOf (this), true, false); - else if (hasKeyboardFocus (true)) - giveAwayFocus (currentlyFocusedComponent != this); + else + giveAwayKeyboardFocusInternal (isParentOf (currentlyFocusedComponent)); if (flags.hasHeavyweightPeerFlag) removeFromDesktop(); @@ -551,8 +551,8 @@ void Component::setVisible (bool shouldBeVisible) if (parentComponent != nullptr) parentComponent->grabKeyboardFocus(); - if (hasKeyboardFocus (true)) - giveAwayFocus (true); + // ensure that keyboard focus is given away if it wasn't taken by parent + giveAwayKeyboardFocus(); } } @@ -704,6 +704,9 @@ void Component::addToDesktop (int styleWanted, void* nativeWindowToAttachTo) repaint(); internalHierarchyChanged(); + + if (auto* handler = getAccessibilityHandler()) + notifyAccessibilityEventInternal (*handler, InternalAccessibilityEvent::windowOpened); } } } @@ -716,6 +719,9 @@ void Component::removeFromDesktop() if (flags.hasHeavyweightPeerFlag) { + if (auto* handler = getAccessibilityHandler()) + notifyAccessibilityEventInternal (*handler, InternalAccessibilityEvent::windowClosed); + ComponentHelpers::releaseAllCachedImageResources (*this); auto* peer = ComponentPeer::getPeerFor (this); @@ -886,7 +892,7 @@ void Component::reorderChildInternal (int sourceIndex, int destIndex) } } -void Component::toFront (bool setAsForeground) +void Component::toFront (bool shouldGrabKeyboardFocus) { // if component methods are being called from threads other than the message // thread, you'll need to use a MessageManagerLock object to make sure it's thread-safe. @@ -896,9 +902,9 @@ void Component::toFront (bool setAsForeground) { if (auto* peer = getPeer()) { - peer->toFront (setAsForeground); + peer->toFront (shouldGrabKeyboardFocus); - if (setAsForeground && ! hasKeyboardFocus (true)) + if (shouldGrabKeyboardFocus && ! hasKeyboardFocus (true)) grabKeyboardFocus(); } } @@ -926,7 +932,7 @@ void Component::toFront (bool setAsForeground) } } - if (setAsForeground) + if (shouldGrabKeyboardFocus) { internalBroughtToFront(); @@ -1498,9 +1504,7 @@ Component* Component::removeChildComponent (int index, bool sendParentEvents, bo // thread, you'll need to use a MessageManagerLock object to make sure it's thread-safe. JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED_OR_OFFSCREEN - auto* child = childComponentList [index]; - - if (child != nullptr) + if (auto* child = childComponentList [index]) { sendParentEvents = sendParentEvents && child->isShowing(); @@ -1518,23 +1522,19 @@ Component* Component::removeChildComponent (int index, bool sendParentEvents, bo ComponentHelpers::releaseAllCachedImageResources (*child); // (NB: there are obscure situations where child->isShowing() = false, but it still has the focus) - if (currentlyFocusedComponent == child || child->isParentOf (currentlyFocusedComponent)) + if (child->hasKeyboardFocus (true)) { + const WeakReference safeThis (this); + + child->giveAwayKeyboardFocusInternal (sendChildEvents || currentlyFocusedComponent != child); + if (sendParentEvents) { - const WeakReference thisPointer (this); - - giveAwayFocus (sendChildEvents || currentlyFocusedComponent != child); - - if (thisPointer == nullptr) + if (safeThis == nullptr) return child; grabKeyboardFocus(); } - else - { - giveAwayFocus (sendChildEvents || currentlyFocusedComponent != child); - } } if (sendChildEvents) @@ -1542,9 +1542,11 @@ Component* Component::removeChildComponent (int index, bool sendParentEvents, bo if (sendParentEvents) internalChildrenChanged(); + + return child; } - return child; + return nullptr; } //============================================================================== @@ -1656,6 +1658,10 @@ void Component::internalHierarchyChanged() i = jmin (i, childComponentList.size()); } + + if (flags.hasHeavyweightPeerFlag) + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::structureChanged); } //============================================================================== @@ -2416,7 +2422,7 @@ void Component::internalMouseDown (MouseInputSource source, Point relativ if (! flags.dontFocusOnMouseClickFlag) { - grabFocusInternal (focusChangedByMouseClick, true); + grabKeyboardFocusInternal (focusChangedByMouseClick, true); if (checker.shouldBailOut()) return; @@ -2644,36 +2650,48 @@ void Component::focusGained (FocusChangeType) {} void Component::focusLost (FocusChangeType) {} void Component::focusOfChildComponentChanged (FocusChangeType) {} -void Component::internalFocusGain (FocusChangeType cause) +void Component::internalKeyboardFocusGain (FocusChangeType cause) { - internalFocusGain (cause, WeakReference (this)); + internalKeyboardFocusGain (cause, WeakReference (this)); } -void Component::internalFocusGain (FocusChangeType cause, const WeakReference& safePointer) +void Component::internalKeyboardFocusGain (FocusChangeType cause, + const WeakReference& safePointer) { focusGained (cause); if (safePointer != nullptr) - internalChildFocusChange (cause, safePointer); + { + if (auto* handler = getAccessibilityHandler()) + handler->grabFocus(); + + internalChildKeyboardFocusChange (cause, safePointer); + } } -void Component::internalFocusLoss (FocusChangeType cause) +void Component::internalKeyboardFocusLoss (FocusChangeType cause) { const WeakReference safePointer (this); focusLost (cause); if (safePointer != nullptr) - internalChildFocusChange (cause, safePointer); + { + if (auto* handler = getAccessibilityHandler()) + handler->giveAwayFocus(); + + internalChildKeyboardFocusChange (cause, safePointer); + } } -void Component::internalChildFocusChange (FocusChangeType cause, const WeakReference& safePointer) +void Component::internalChildKeyboardFocusChange (FocusChangeType cause, + const WeakReference& safePointer) { - const bool childIsNowFocused = hasKeyboardFocus (true); + const bool childIsNowKeyboardFocused = hasKeyboardFocus (true); - if (flags.childCompFocusedFlag != childIsNowFocused) + if (flags.childKeyboardFocusedFlag != childIsNowKeyboardFocused) { - flags.childCompFocusedFlag = childIsNowFocused; + flags.childKeyboardFocusedFlag = childIsNowKeyboardFocused; focusOfChildComponentChanged (cause); @@ -2682,12 +2700,12 @@ void Component::internalChildFocusChange (FocusChangeType cause, const WeakRefer } if (parentComponent != nullptr) - parentComponent->internalChildFocusChange (cause, WeakReference (parentComponent)); + parentComponent->internalChildKeyboardFocusChange (cause, parentComponent); } void Component::setWantsKeyboardFocus (bool wantsFocus) noexcept { - flags.wantsFocusFlag = wantsFocus; + flags.wantsKeyboardFocusFlag = wantsFocus; } void Component::setMouseClickGrabsKeyboardFocus (bool shouldGrabFocus) @@ -2702,12 +2720,15 @@ bool Component::getMouseClickGrabsKeyboardFocus() const noexcept bool Component::getWantsKeyboardFocus() const noexcept { - return flags.wantsFocusFlag && ! flags.isDisabledFlag; + return flags.wantsKeyboardFocusFlag && ! flags.isDisabledFlag; } -void Component::setFocusContainer (bool shouldBeFocusContainer) noexcept +void Component::setFocusContainerType (FocusContainerType containerType) noexcept { - flags.isFocusContainerFlag = shouldBeFocusContainer; + flags.isFocusContainerFlag = (containerType == FocusContainerType::focusContainer + || containerType == FocusContainerType::keyboardFocusContainer); + + flags.isKeyboardFocusContainerFlag = (containerType == FocusContainerType::keyboardFocusContainer); } bool Component::isFocusContainer() const noexcept @@ -2715,6 +2736,35 @@ bool Component::isFocusContainer() const noexcept return flags.isFocusContainerFlag; } +bool Component::isKeyboardFocusContainer() const noexcept +{ + return flags.isKeyboardFocusContainerFlag; +} + +template +static Component* findContainer (const Component* child, FocusContainerFn isFocusContainer) +{ + if (auto* parent = child->getParentComponent()) + { + if ((parent->*isFocusContainer)() || parent->getParentComponent() == nullptr) + return parent; + + return findContainer (parent, isFocusContainer); + } + + return nullptr; +} + +Component* Component::findFocusContainer() const +{ + return findContainer (this, &Component::isFocusContainer); +} + +Component* Component::findKeyboardFocusContainer() const +{ + return findContainer (this, &Component::isKeyboardFocusContainer); +} + static const Identifier juce_explicitFocusOrderId ("_jexfo"); int Component::getExplicitFocusOrder() const @@ -2727,85 +2777,78 @@ void Component::setExplicitFocusOrder (int newFocusOrderIndex) properties.set (juce_explicitFocusOrderId, newFocusOrderIndex); } -KeyboardFocusTraverser* Component::createFocusTraverser() +std::unique_ptr Component::createFocusTraverser() { if (flags.isFocusContainerFlag || parentComponent == nullptr) - return new KeyboardFocusTraverser(); + return std::make_unique(); return parentComponent->createFocusTraverser(); } +std::unique_ptr Component::createKeyboardFocusTraverser() +{ + if (flags.isKeyboardFocusContainerFlag || parentComponent == nullptr) + return std::make_unique(); + + return parentComponent->createKeyboardFocusTraverser(); +} + void Component::takeKeyboardFocus (FocusChangeType cause) { - // give the focus to this component - if (currentlyFocusedComponent != this) + if (currentlyFocusedComponent == this) + return; + + if (auto* peer = getPeer()) { - // get the focus onto our desktop window - if (auto* peer = getPeer()) - { - const WeakReference safePointer (this); - peer->grabFocus(); + const WeakReference safePointer (this); + peer->grabFocus(); - if (peer->isFocused() && currentlyFocusedComponent != this) - { - WeakReference componentLosingFocus (currentlyFocusedComponent); - currentlyFocusedComponent = this; + if (! peer->isFocused() || currentlyFocusedComponent == this) + return; - Desktop::getInstance().triggerFocusCallback(); + WeakReference componentLosingFocus (currentlyFocusedComponent); + currentlyFocusedComponent = this; - // call this after setting currentlyFocusedComponent so that the one that's - // losing it has a chance to see where focus is going - if (componentLosingFocus != nullptr) - componentLosingFocus->internalFocusLoss (cause); + Desktop::getInstance().triggerFocusCallback(); - if (currentlyFocusedComponent == this) - internalFocusGain (cause, safePointer); - } - } + // call this after setting currentlyFocusedComponent so that the one that's + // losing it has a chance to see where focus is going + if (componentLosingFocus != nullptr) + componentLosingFocus->internalKeyboardFocusLoss (cause); + + if (currentlyFocusedComponent == this) + internalKeyboardFocusGain (cause, safePointer); } } -void Component::grabFocusInternal (FocusChangeType cause, bool canTryParent) +void Component::grabKeyboardFocusInternal (FocusChangeType cause, bool canTryParent) { - if (isShowing()) + if (! isShowing()) + return; + + if (flags.wantsKeyboardFocusFlag + && (isEnabled() || parentComponent == nullptr)) { - if (flags.wantsFocusFlag && (isEnabled() || parentComponent == nullptr)) + takeKeyboardFocus (cause); + return; + } + + if (isParentOf (currentlyFocusedComponent) && currentlyFocusedComponent->isShowing()) + return; + + if (auto traverser = createKeyboardFocusTraverser()) + { + if (auto* defaultComp = traverser->getDefaultComponent (this)) { - takeKeyboardFocus (cause); - } - else - { - if (isParentOf (currentlyFocusedComponent) - && currentlyFocusedComponent->isShowing()) - { - // do nothing if the focused component is actually a child of ours.. - } - else - { - // find the default child component.. - std::unique_ptr traverser (createFocusTraverser()); - - if (traverser != nullptr) - { - auto* defaultComp = traverser->getDefaultComponent (this); - traverser.reset(); - - if (defaultComp != nullptr) - { - defaultComp->grabFocusInternal (cause, false); - return; - } - } - - if (canTryParent && parentComponent != nullptr) - { - // if no children want it and we're allowed to try our parent comp, - // then pass up to parent, which will try our siblings. - parentComponent->grabFocusInternal (cause, true); - } - } + defaultComp->grabKeyboardFocusInternal (cause, false); + return; } } + + // if no children want it and we're allowed to try our parent comp, + // then pass up to parent, which will try our siblings. + if (canTryParent && parentComponent != nullptr) + parentComponent->grabKeyboardFocusInternal (cause, true); } void Component::grabKeyboardFocus() @@ -2814,7 +2857,7 @@ void Component::grabKeyboardFocus() // thread, you'll need to use a MessageManagerLock object to make sure it's thread-safe. JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED - grabFocusInternal (focusChangedDirectly, true); + grabKeyboardFocusInternal (focusChangedDirectly, true); // A component can only be focused when it's actually on the screen! // If this fails then you're probably trying to grab the focus before you've @@ -2823,6 +2866,31 @@ void Component::grabKeyboardFocus() jassert (isShowing() || isOnDesktop()); } +void Component::giveAwayKeyboardFocusInternal (bool sendFocusLossEvent) +{ + if (hasKeyboardFocus (true)) + { + if (auto* componentLosingFocus = currentlyFocusedComponent) + { + currentlyFocusedComponent = nullptr; + + if (sendFocusLossEvent && componentLosingFocus != nullptr) + componentLosingFocus->internalKeyboardFocusLoss (focusChangedDirectly); + + Desktop::getInstance().triggerFocusCallback(); + } + } +} + +void Component::giveAwayKeyboardFocus() +{ + // if component methods are being called from threads other than the message + // thread, you'll need to use a MessageManagerLock object to make sure it's thread-safe. + JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED + + giveAwayKeyboardFocusInternal (true); +} + void Component::moveKeyboardFocusToSibling (bool moveToNext) { // if component methods are being called from threads other than the message @@ -2831,15 +2899,27 @@ void Component::moveKeyboardFocusToSibling (bool moveToNext) if (parentComponent != nullptr) { - std::unique_ptr traverser (createFocusTraverser()); - - if (traverser != nullptr) + if (auto traverser = createKeyboardFocusTraverser()) { - auto* nextComp = moveToNext ? traverser->getNextComponent (this) - : traverser->getPreviousComponent (this); - traverser.reset(); + auto findComponentToFocus = [&]() -> Component* + { + if (auto* comp = (moveToNext ? traverser->getNextComponent (this) + : traverser->getPreviousComponent (this))) + return comp; - if (nextComp != nullptr) + if (auto* focusContainer = findKeyboardFocusContainer()) + { + auto allFocusableComponents = traverser->getAllComponents (focusContainer); + + if (! allFocusableComponents.empty()) + return moveToNext ? allFocusableComponents.front() + : allFocusableComponents.back(); + } + + return nullptr; + }; + + if (auto* nextComp = findComponentToFocus()) { if (nextComp->isCurrentlyBlockedByAnotherModalComponent()) { @@ -2850,7 +2930,7 @@ void Component::moveKeyboardFocusToSibling (bool moveToNext) return; } - nextComp->grabFocusInternal (focusChangedByTabKey, true); + nextComp->grabKeyboardFocusInternal (focusChangedByTabKey, true); return; } } @@ -2872,19 +2952,8 @@ Component* JUCE_CALLTYPE Component::getCurrentlyFocusedComponent() noexcept void JUCE_CALLTYPE Component::unfocusAllComponents() { - if (auto* c = getCurrentlyFocusedComponent()) - c->giveAwayFocus (true); -} - -void Component::giveAwayFocus (bool sendFocusLossEvent) -{ - auto* componentLosingFocus = currentlyFocusedComponent; - currentlyFocusedComponent = nullptr; - - if (sendFocusLossEvent && componentLosingFocus != nullptr) - componentLosingFocus->internalFocusLoss (focusChangedDirectly); - - Desktop::getInstance().triggerFocusCallback(); + if (currentlyFocusedComponent != nullptr) + currentlyFocusedComponent->giveAwayKeyboardFocus(); } //============================================================================== @@ -3029,4 +3098,52 @@ bool Component::BailOutChecker::shouldBailOut() const noexcept return safePointer == nullptr; } +//============================================================================== +void Component::setTitle (const String& newTitle) +{ + componentTitle = newTitle; +} + +void Component::setDescription (const String& newDescription) +{ + componentDescription = newDescription; +} + +void Component::setHelpText (const String& newHelpText) +{ + componentHelpText = newHelpText; +} + +void Component::setAccessible (bool shouldBeAccessible) +{ + flags.accessibilityIgnoredFlag = ! shouldBeAccessible; + + if (flags.accessibilityIgnoredFlag) + invalidateAccessibilityHandler(); +} + +std::unique_ptr Component::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::unspecified); +} + +void Component::invalidateAccessibilityHandler() +{ + accessibilityHandler = nullptr; +} + +AccessibilityHandler* Component::getAccessibilityHandler() +{ + if (flags.accessibilityIgnoredFlag) + return nullptr; + + if (accessibilityHandler == nullptr + || accessibilityHandler->getTypeIndex() != std::type_index (typeid (*this))) + { + accessibilityHandler = createAccessibilityHandler(); + } + + return accessibilityHandler.get(); +} + } // namespace juce diff --git a/modules/juce_gui_basics/components/juce_Component.h b/modules/juce_gui_basics/components/juce_Component.h index acff23b15d..03a0f1614c 100644 --- a/modules/juce_gui_basics/components/juce_Component.h +++ b/modules/juce_gui_basics/components/juce_Component.h @@ -73,7 +73,7 @@ public: /** Returns the name of this component. @see setName */ - const String& getName() const noexcept { return componentName; } + String getName() const noexcept { return componentName; } /** Sets the name of this component. @@ -87,7 +87,7 @@ public: /** Returns the ID string that was set by setComponentID(). @see setComponentID, findChildWithID */ - const String& getComponentID() const noexcept { return componentID; } + String getComponentID() const noexcept { return componentID; } /** Sets the component's ID string. You can retrieve the ID using getComponentID(). @@ -217,11 +217,12 @@ public: then they will still be kept in front of this one (unless of course this one is also 'always-on-top'). - @param shouldAlsoGainFocus if true, this will also try to assign keyboard focus - to the component (see grabKeyboardFocus() for more details) + @param shouldAlsoGainKeyboardFocus if true, this will also try to assign + keyboard focus to the component (see + grabKeyboardFocus() for more details) @see toBack, toBehind, setAlwaysOnTop */ - void toFront (bool shouldAlsoGainFocus); + void toFront (bool shouldAlsoGainKeyboardFocus); /** Changes this component's z-order to be at the back of all its siblings. @@ -1203,55 +1204,156 @@ public: bool isBroughtToFrontOnMouseClick() const noexcept; //============================================================================== - // Keyboard focus methods + // Focus methods - /** Sets a flag to indicate whether this component needs keyboard focus or not. + /** Sets the focus order of this component. - By default components aren't actually interested in gaining the + The focus order is used by the default traverser implementation returned by + createFocusTraverser() as part of its algorithm for deciding the order in + which components should be traversed. A value of 0 or less is taken to mean + that no explicit order is wanted, and that traversal should use other + factors, like the component's position. + + @see getExplicitFocusOrder, FocusTraverser, createFocusTraverser + */ + void setExplicitFocusOrder (int newFocusOrderIndex); + + /** Returns the focus order of this component, if one has been specified. + + By default components don't have a focus order - in that case, this will + return 0. + + @see setExplicitFocusOrder + */ + int getExplicitFocusOrder() const; + + /** A focus container type that can be passed to setFocusContainer(). + + If a component is marked as a focus container or keyboard focus container then + it will act as the top-level component within which focus or keyboard focus is + passed around. By default components are considered "focusable" if they are visible + and enabled and "keyboard focusable" if `getWantsKeyboardFocus() == true`. + + The order of traversal within a focus container is determined by the objects + returned by createFocusTraverser() and createKeyboardFocusTraverser(), + respectively - see the documentation of the default FocusContainer and + KeyboardFocusContainer implementations for more information. + */ + enum class FocusContainerType + { + /** The component will not act as a focus container. + + This is the default setting for non top-level components and means that it and any + sub-components are navigable within their containing focus container. + */ + none, + + /** The component will act as a top-level component within which focus is passed around. + + The default traverser implementation returned by createFocusTraverser() will use this + flag to find the first parent component (of the currently focused one) that wants to + be a focus container. + + This is currently used when determining the hierarchy of accessible UI elements presented + to screen reader clients on supported platforms. See the AccessibilityHandler class for + more information. + */ + focusContainer, + + /** The component will act as a top-level component within which keyboard focus is passed around. + + The default traverser implementation returned by createKeyboardFocusTraverser() will + use this flag to find the first parent component (of the currently focused one) that + wants to be a keyboard focus container. + + This is currently used when determining how keyboard focus is passed between components + that have been marked as keyboard focusable with setWantsKeyboardFocus() when clicking + on components and navigating with the tab key. + */ + keyboardFocusContainer + }; + + /** Sets whether this component is a container for components that can have + their focus traversed, and the type of focus traversal that it supports. + + @see FocusContainerType, isFocusContainer, isKeyboardFocusContainer, + FocusTraverser, createFocusTraverser, + KeyboardFocusTraverser, createKeyboardFocusTraverser + */ + void setFocusContainerType (FocusContainerType containerType) noexcept; + + /** Returns true if this component has been marked as a focus container. + + @see setFocusContainer + */ + bool isFocusContainer() const noexcept; + + /** Returns true if this component has been marked as a keyboard focus container. + + @see setFocusContainer + */ + bool isKeyboardFocusContainer() const noexcept; + + /** Returns the focus container for this component. + + @see isFocusContainer, setFocusContainer + */ + Component* findFocusContainer() const; + + /** Returns the keyboard focus container for this component. + + @see isFocusContainer, setFocusContainer + */ + Component* findKeyboardFocusContainer() const; + + //============================================================================== + /** Sets a flag to indicate whether this component wants keyboard focus or not. + + By default components aren't actually interested in gaining the keyboard focus, but this method can be used to turn this on. See the grabKeyboardFocus() method for details about the way a component is chosen to receive the focus. - @see grabKeyboardFocus, getWantsKeyboardFocus + @see grabKeyboardFocus, giveAwayKeyboardFocus, getWantsKeyboardFocus */ void setWantsKeyboardFocus (bool wantsFocus) noexcept; /** Returns true if the component is interested in getting keyboard focus. - This returns the flag set by setWantsKeyboardFocus(). The default - setting is false. + This returns the flag set by setWantsKeyboardFocus(). The default setting + is false. @see setWantsKeyboardFocus */ bool getWantsKeyboardFocus() const noexcept; - //============================================================================== /** Chooses whether a click on this component automatically grabs the focus. By default this is set to true, but you might want a component which can - be focused, but where you don't want the user to be able to affect it directly - by clicking. + be focused, but where you don't want the user to be able to affect it + directly by clicking. */ void setMouseClickGrabsKeyboardFocus (bool shouldGrabFocus); /** Returns the last value set with setMouseClickGrabsKeyboardFocus(). - See setMouseClickGrabsKeyboardFocus() for more info. + + @see setMouseClickGrabsKeyboardFocus */ bool getMouseClickGrabsKeyboardFocus() const noexcept; - //============================================================================== /** Tries to give keyboard focus to this component. - When the user clicks on a component or its grabKeyboardFocus() - method is called, the following procedure is used to work out which - component should get it: + When the user clicks on a component or its grabKeyboardFocus() method is + called, the following procedure is used to work out which component should + get it: - if the component that was clicked on actually wants focus (as indicated by calling getWantsKeyboardFocus), it gets it. - if the component itself doesn't want focus, it will try to pass it on to whichever of its children is the default component, as determined by - KeyboardFocusTraverser::getDefaultComponent() + the getDefaultComponent() implemetation of the ComponentTraverser returned + by createKeyboardFocusTraverser(). - if none of its children want focus at all, it will pass it up to its parent instead, unless it's a top-level component without a parent, in which case it just takes the focus itself. @@ -1261,12 +1363,21 @@ public: visible. So there's no point trying to call this in the component's own constructor or before all of its parent hierarchy has been fully instantiated. - @see setWantsKeyboardFocus, getWantsKeyboardFocus, hasKeyboardFocus, - getCurrentlyFocusedComponent, focusGained, focusLost, + @see giveAwayKeyboardFocus, setWantsKeyboardFocus, getWantsKeyboardFocus, + hasKeyboardFocus, getCurrentlyFocusedComponent, focusGained, focusLost, keyPressed, keyStateChanged */ void grabKeyboardFocus(); + /** If this component or any of its children currently have the keyboard focus, + this will defocus it, send a focus change notification, and try to pass the + focus to the next component. + + @see grabKeyboardFocus, setWantsKeyboardFocus, getCurrentlyFocusedComponent, + focusGained, focusLost + */ + void giveAwayKeyboardFocus(); + /** Returns true if this component currently has the keyboard focus. @param trueIfChildIsFocused if this is true, then the method returns true if @@ -1274,13 +1385,28 @@ public: have the focus. If false, the method only returns true if this component has the focus. - @see grabKeyboardFocus, setWantsKeyboardFocus, getCurrentlyFocusedComponent, - focusGained, focusLost + @see grabKeyboardFocus, giveAwayKeyboardFocus, setWantsKeyboardFocus, + getCurrentlyFocusedComponent, focusGained, focusLost */ bool hasKeyboardFocus (bool trueIfChildIsFocused) const; + /** Tries to move the keyboard focus to one of this component's siblings. + + This will try to move focus to either the next or previous component, as + determined by the getNextComponent() and getPreviousComponent() implemetations + of the ComponentTraverser returned by createKeyboardFocusTraverser(). + + This is the method that is used when shifting focus by pressing the tab key. + + @param moveToNext if true, the focus will move forwards; if false, it will + move backwards + @see grabKeyboardFocus, giveAwayKeyboardFocus, setFocusContainer, setWantsKeyboardFocus + */ + void moveKeyboardFocusToSibling (bool moveToNext); + /** Returns the component that currently has the keyboard focus. - @returns the focused component, or null if nothing is focused. + + @returns the focused component, or nullptr if nothing is focused. */ static Component* JUCE_CALLTYPE getCurrentlyFocusedComponent() noexcept; @@ -1288,83 +1414,32 @@ public: static void JUCE_CALLTYPE unfocusAllComponents(); //============================================================================== - /** Tries to move the keyboard focus to one of this component's siblings. + /** Creates a ComponentTraverser object to determine the logic by which focus should be + passed from this component. - This will try to move focus to either the next or previous component. (This - is the method that is used when shifting focus by pressing the tab key). + The default implementation of this method will return an instance of FocusTraverser + if this component is a focus container (as determined by the setFocusContainer() method). + If the component isn't a focus container, then it will recursively call + createFocusTraverser() on its parents. - Components for which getWantsKeyboardFocus() returns false are not looked at. - - @param moveToNext if true, the focus will move forwards; if false, it will - move backwards - @see grabKeyboardFocus, setFocusContainer, setWantsKeyboardFocus + If you override this to return a custom traverser object, then this component and + all its sub-components will use the new object to make their focusing decisions. */ - void moveKeyboardFocusToSibling (bool moveToNext); + virtual std::unique_ptr createFocusTraverser(); - /** Creates a KeyboardFocusTraverser object to use to determine the logic by - which focus should be passed from this component. + /** Creates a ComponentTraverser object to use to determine the logic by which keyboard + focus should be passed from this component. - The default implementation of this method will return a default - KeyboardFocusTraverser if this component is a focus container (as determined - by the setFocusContainer() method). If the component isn't a focus - container, then it will recursively ask its parents for a KeyboardFocusTraverser. + The default implementation of this method will return an instance of + KeyboardFocusTraverser if this component is a keyboard focus container (as determined by + the setFocusContainer() method). If the component isn't a keyboard focus container, then + it will recursively call createKeyboardFocusTraverser() on its parents. - If you override this to return a custom KeyboardFocusTraverser, then - this component and all its sub-components will use the new object to - make their focusing decisions. - - The method should return a new object, which the caller is required to - delete when no longer needed. + If you override this to return a custom traverser object, then this component and + all its sub-components will use the new object to make their keyboard focusing + decisions. */ - virtual KeyboardFocusTraverser* createFocusTraverser(); - - /** Returns the focus order of this component, if one has been specified. - - By default components don't have a focus order - in that case, this - will return 0. Lower numbers indicate that the component will be - earlier in the focus traversal order. - - To change the order, call setExplicitFocusOrder(). - - The focus order may be used by the KeyboardFocusTraverser class as part of - its algorithm for deciding the order in which components should be traversed. - See the KeyboardFocusTraverser class for more details on this. - - @see moveKeyboardFocusToSibling, createFocusTraverser, KeyboardFocusTraverser - */ - int getExplicitFocusOrder() const; - - /** Sets the index used in determining the order in which focusable components - should be traversed. - - A value of 0 or less is taken to mean that no explicit order is wanted, and - that traversal should use other factors, like the component's position. - - @see getExplicitFocusOrder, moveKeyboardFocusToSibling - */ - void setExplicitFocusOrder (int newFocusOrderIndex); - - /** Indicates whether this component is a parent for components that can have - their focus traversed. - - This flag is used by the default implementation of the createFocusTraverser() - method, which uses the flag to find the first parent component (of the currently - focused one) which wants to be a focus container. - - So using this method to set the flag to 'true' causes this component to - act as the top level within which focus is passed around. - - @see isFocusContainer, createFocusTraverser, moveKeyboardFocusToSibling - */ - void setFocusContainer (bool shouldBeFocusContainer) noexcept; - - /** Returns true if this component has been marked as a focus container. - - See setFocusContainer() for more details. - - @see setFocusContainer, moveKeyboardFocusToSibling, createFocusTraverser - */ - bool isFocusContainer() const noexcept; + virtual std::unique_ptr createKeyboardFocusTraverser(); //============================================================================== /** Returns true if the component (and all its parents) are enabled. @@ -2284,7 +2359,109 @@ public: */ bool getViewportIgnoreDragFlag() const noexcept { return flags.viewportIgnoreDragFlag; } + //============================================================================== + /** Returns the title text for this component. + + @see setTitle + */ + String getTitle() const noexcept { return componentTitle; } + + /** Sets the title for this component. + + If this component supports accessibility using the default AccessibilityHandler + implementation, this string will be passed to accessibility clients requesting a + title and may be read out by a screen reader. + + @see getTitle, getAccessibilityHandler + */ + void setTitle (const String& newTitle); + + /** Returns the description for this component. + + @see setDescription + */ + String getDescription() const noexcept { return componentDescription; } + + /** Sets the description for this component. + + If this component supports accessibility using the default AccessibilityHandler + implementation, this string will be passed to accessibility clients requesting a + description and may be read out by a screen reader. + + @see getDescription, getAccessibilityHandler + */ + void setDescription (const String& newDescription); + + /** Returns the help text for this component. + + @see setHelpText + */ + String getHelpText() const noexcept { return componentHelpText; } + + /** Sets the help text for this component. + + If this component supports accessibility using the default AccessibilityHandler + implementation, this string will be passed to accessibility clients requesting help text + and may be read out by a screen reader. + + @see getHelpText, getAccessibilityHandler + */ + void setHelpText (const String& newHelpText); + + /** Sets whether this component is visible to accessibility clients. + + If this flag is set to false then the getAccessibilityHandler() method will return nullptr + and this component will not be visible to any accessibility clients. + + By default this is set to true. + + @see getAccessibilityHandler, createAccessibilityHandler + */ + void setAccessible (bool shouldBeAccessible); + + /** Returns the accessibility handler for this component, or nullptr if this component is not + accessible. + + @see createAccessibilityHandler, setAccessible + */ + AccessibilityHandler* getAccessibilityHandler(); + + /** Invalidates the AccessibilityHandler that is currently being used for this component. + + Use this to indicate that something in the accessible component has changed + and its handler needs to be updated. This will trigger a call to + createAccessibilityHandler(). + */ + void invalidateAccessibilityHandler(); + + //============================================================================== + // This method has been deprecated in favour of the setFocusContainerType() method + // that takes a more descriptive enum. + JUCE_DEPRECATED_WITH_BODY (void setFocusContainer (bool shouldBeFocusContainer) noexcept, + { + setFocusContainerType (shouldBeFocusContainer ? FocusContainerType::keyboardFocusContainer + : FocusContainerType::none); + }) + private: + //============================================================================== + /** Override this method to return a custom AccessibilityHandler for this component. + + The default implementation creates and returns a AccessibilityHandler object with an + unspecified role, meaning that it will be visible to accessibility clients but + without a specific role, action callbacks or interfaces. To control how accessibility + clients see and interact with your component subclass AccessibilityHandler, implement + the desired behaviours, and return an instance of it from this method in your + component subclass. + + The accessibility handler you return here is guaranteed to be destroyed before + its Component, so it's safe to store and use a reference back to the Component + inside the AccessibilityHandler if necessary. + + @see getAccessibilityHandler + */ + virtual std::unique_ptr createAccessibilityHandler(); + //============================================================================== friend class ComponentPeer; friend class MouseInputSource; @@ -2294,7 +2471,7 @@ private: static Component* currentlyFocusedComponent; //============================================================================== - String componentName, componentID; + String componentName, componentID, componentTitle, componentDescription, componentHelpText; Component* parentComponent = nullptr; Rectangle boundsRelativeToParent; std::unique_ptr positioner; @@ -2314,29 +2491,33 @@ private: friend class WeakReference; WeakReference::Master masterReference; + std::unique_ptr accessibilityHandler; + struct ComponentFlags { - bool hasHeavyweightPeerFlag : 1; - bool visibleFlag : 1; - bool opaqueFlag : 1; - bool ignoresMouseClicksFlag : 1; - bool allowChildMouseClicksFlag : 1; - bool wantsFocusFlag : 1; - bool isFocusContainerFlag : 1; - bool dontFocusOnMouseClickFlag : 1; - bool alwaysOnTopFlag : 1; - bool bufferToImageFlag : 1; - bool bringToFrontOnClickFlag : 1; - bool repaintOnMouseActivityFlag : 1; - bool isDisabledFlag : 1; - bool childCompFocusedFlag : 1; - bool dontClipGraphicsFlag : 1; - bool mouseDownWasBlocked : 1; - bool isMoveCallbackPending : 1; - bool isResizeCallbackPending : 1; - bool viewportIgnoreDragFlag : 1; + bool hasHeavyweightPeerFlag : 1; + bool visibleFlag : 1; + bool opaqueFlag : 1; + bool ignoresMouseClicksFlag : 1; + bool allowChildMouseClicksFlag : 1; + bool wantsKeyboardFocusFlag : 1; + bool isFocusContainerFlag : 1; + bool isKeyboardFocusContainerFlag : 1; + bool childKeyboardFocusedFlag : 1; + bool dontFocusOnMouseClickFlag : 1; + bool alwaysOnTopFlag : 1; + bool bufferToImageFlag : 1; + bool bringToFrontOnClickFlag : 1; + bool repaintOnMouseActivityFlag : 1; + bool isDisabledFlag : 1; + bool dontClipGraphicsFlag : 1; + bool mouseDownWasBlocked : 1; + bool isMoveCallbackPending : 1; + bool isResizeCallbackPending : 1; + bool viewportIgnoreDragFlag : 1; + bool accessibilityIgnoredFlag : 1; #if JUCE_DEBUG - bool isInsidePaintCall : 1; + bool isInsidePaintCall : 1; #endif }; @@ -2358,10 +2539,10 @@ private: void internalMouseWheel (MouseInputSource, Point, Time, const MouseWheelDetails&); void internalMagnifyGesture (MouseInputSource, Point, Time, float); void internalBroughtToFront(); - void internalFocusGain (FocusChangeType, const WeakReference&); - void internalFocusGain (FocusChangeType); - void internalFocusLoss (FocusChangeType); - void internalChildFocusChange (FocusChangeType, const WeakReference&); + void internalKeyboardFocusGain (FocusChangeType, const WeakReference&); + void internalKeyboardFocusGain (FocusChangeType); + void internalKeyboardFocusLoss (FocusChangeType); + void internalChildKeyboardFocusChange (FocusChangeType, const WeakReference&); void internalModalInputAttempt(); void internalModifierKeysChanged(); void internalChildrenChanged(); @@ -2377,8 +2558,8 @@ private: void repaintParent(); void sendFakeMouseMove() const; void takeKeyboardFocus (FocusChangeType); - void grabFocusInternal (FocusChangeType, bool canTryParent); - static void giveAwayFocus (bool sendFocusLossEvent); + void grabKeyboardFocusInternal (FocusChangeType, bool canTryParent); + void giveAwayKeyboardFocusInternal (bool sendFocusLossEvent); void sendEnablementChangeMessage(); void sendVisibilityChangeMessage(); diff --git a/modules/juce_gui_basics/components/juce_ComponentTraverser.h b/modules/juce_gui_basics/components/juce_ComponentTraverser.h new file mode 100644 index 0000000000..1b37a0bb6f --- /dev/null +++ b/modules/juce_gui_basics/components/juce_ComponentTraverser.h @@ -0,0 +1,72 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + Base class for traversing components. + + If you need custom focus or keyboard focus traversal for a component you can + create a subclass of ComponentTraverser and return it from + Component::createFocusTraverser() or Component::createKeyboardFocusTraverser(). + + @see Component::createFocusTraverser, Component::createKeyboardFocusTraverser + + @tags{GUI} +*/ +class JUCE_API ComponentTraverser +{ +public: + /** Destructor. */ + virtual ~ComponentTraverser() = default; + + /** Returns the component that should be used as the traversal entry point + within the given parent component. + + This must return nullptr if there is no default component. + */ + virtual Component* getDefaultComponent (Component* parentComponent) = 0; + + /** Returns the component that comes after the specified one when moving "forwards". + + This must return nullptr if there is no next component. + */ + virtual Component* getNextComponent (Component* current) = 0; + + /** Returns the component that comes after the specified one when moving "backwards". + + This must return nullptr if there is no previous component. + */ + virtual Component* getPreviousComponent (Component* current) = 0; + + /** Returns all of the traversable components within the given parent component in + traversal order. + */ + virtual std::vector getAllComponents (Component* parentComponent) = 0; +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/components/juce_FocusTraverser.cpp b/modules/juce_gui_basics/components/juce_FocusTraverser.cpp new file mode 100644 index 0000000000..6dfa92f831 --- /dev/null +++ b/modules/juce_gui_basics/components/juce_FocusTraverser.cpp @@ -0,0 +1,359 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +namespace FocusHelpers +{ + static int getOrder (const Component* c) + { + auto order = c->getExplicitFocusOrder(); + return order > 0 ? order : std::numeric_limits::max(); + } + + template + static void findAllComponents (Component* parent, + std::vector& components, + FocusContainerFn isFocusContainer) + { + if (parent == nullptr || parent->getNumChildComponents() == 0) + return; + + std::vector localComponents; + + for (auto* c : parent->getChildren()) + if (c->isVisible() && c->isEnabled()) + localComponents.push_back (c); + + const auto compareComponents = [&] (const Component* a, const Component* b) + { + const auto getComponentOrderAttributes = [] (const Component* c) + { + return std::make_tuple (getOrder (c), + c->isAlwaysOnTop() ? 0 : 1, + c->getY(), + c->getX()); + }; + + return getComponentOrderAttributes (a) < getComponentOrderAttributes (b); + }; + + // This will sort so that they are ordered in terms of explicit focus, + // always on top, left-to-right, and then top-to-bottom. + std::stable_sort (localComponents.begin(), localComponents.end(), compareComponents); + + for (auto* c : localComponents) + { + components.push_back (c); + + if (! (c->*isFocusContainer)()) + findAllComponents (c, components, isFocusContainer); + } + } + + enum class NavigationDirection { forwards, backwards }; + + template + static Component* navigateFocus (Component* current, + Component* focusContainer, + NavigationDirection direction, + FocusContainerFn isFocusContainer) + { + if (focusContainer != nullptr) + { + std::vector components; + findAllComponents (focusContainer, components, isFocusContainer); + + const auto iter = std::find (components.cbegin(), components.cend(), current); + + if (iter == components.cend()) + return nullptr; + + switch (direction) + { + case NavigationDirection::forwards: + if (iter != std::prev (components.cend())) + return *std::next (iter); + + break; + + case NavigationDirection::backwards: + if (iter != components.cbegin()) + return *std::prev (iter); + + break; + } + } + + return nullptr; + } +} + +//============================================================================== +Component* FocusTraverser::getNextComponent (Component* current) +{ + jassert (current != nullptr); + + return FocusHelpers::navigateFocus (current, + current->findFocusContainer(), + FocusHelpers::NavigationDirection::forwards, + &Component::isFocusContainer); +} + +Component* FocusTraverser::getPreviousComponent (Component* current) +{ + jassert (current != nullptr); + + return FocusHelpers::navigateFocus (current, + current->findFocusContainer(), + FocusHelpers::NavigationDirection::backwards, + &Component::isFocusContainer); +} + +Component* FocusTraverser::getDefaultComponent (Component* parentComponent) +{ + if (parentComponent != nullptr) + { + std::vector components; + FocusHelpers::findAllComponents (parentComponent, + components, + &Component::isFocusContainer); + + if (! components.empty()) + return components.front(); + } + + return nullptr; +} + +std::vector FocusTraverser::getAllComponents (Component* parentComponent) +{ + std::vector components; + FocusHelpers::findAllComponents (parentComponent, + components, + &Component::isFocusContainer); + + return components; +} + +//============================================================================== +//============================================================================== +#if JUCE_UNIT_TESTS + +struct FocusTraverserTests : public UnitTest +{ + FocusTraverserTests() + : UnitTest ("FocusTraverser", UnitTestCategories::gui) + {} + + void runTest() override + { + ScopedJuceInitialiser_GUI libraryInitialiser; + + beginTest ("Basic traversal"); + { + TestComponent parent; + + expect (traverser.getDefaultComponent (&parent) == &parent.children.front()); + + for (auto iter = parent.children.begin(); iter != parent.children.end(); ++iter) + expect (traverser.getNextComponent (&(*iter)) == (iter == std::prev (parent.children.cend()) ? nullptr + : &(*std::next (iter)))); + + for (auto iter = parent.children.rbegin(); iter != parent.children.rend(); ++iter) + expect (traverser.getPreviousComponent (&(*iter)) == (iter == std::prev (parent.children.rend()) ? nullptr + : &(*std::next (iter)))); + auto allComponents = traverser.getAllComponents (&parent); + + expect (std::equal (allComponents.cbegin(), allComponents.cend(), parent.children.cbegin(), + [] (const Component* c1, const Component& c2) { return c1 == &c2; })); + } + + beginTest ("Disabled components are ignored"); + { + checkIgnored ([] (Component& c) { c.setEnabled (false); }); + } + + beginTest ("Invisible components are ignored"); + { + checkIgnored ([] (Component& c) { c.setVisible (false); }); + } + + beginTest ("Explicit focus order comes before unspecified"); + { + TestComponent parent; + + auto& explicitFocusComponent = parent.children[2]; + + explicitFocusComponent.setExplicitFocusOrder (1); + expect (traverser.getDefaultComponent (&parent) == &explicitFocusComponent); + + expect (traverser.getAllComponents (&parent).front() == &explicitFocusComponent); + } + + beginTest ("Explicit focus order comparison"); + { + checkComponentProperties ([this] (Component& child) { child.setExplicitFocusOrder (getRandom().nextInt ({ 1, 100 })); }, + [] (const Component& c1, const Component& c2) { return c1.getExplicitFocusOrder() + <= c2.getExplicitFocusOrder(); }); + } + + beginTest ("Left to right"); + { + checkComponentProperties ([this] (Component& child) { child.setTopLeftPosition (getRandom().nextInt ({ 0, 100 }), 0); }, + [] (const Component& c1, const Component& c2) { return c1.getX() <= c2.getX(); }); + } + + beginTest ("Top to bottom"); + { + checkComponentProperties ([this] (Component& child) { child.setTopLeftPosition (0, getRandom().nextInt ({ 0, 100 })); }, + [] (const Component& c1, const Component& c2) { return c1.getY() <= c2.getY(); }); + } + + beginTest ("Focus containers have their own focus"); + { + Component root; + + TestComponent container; + container.setFocusContainerType (Component::FocusContainerType::focusContainer); + + root.addAndMakeVisible (container); + + expect (traverser.getDefaultComponent (&root) == &container); + expect (traverser.getNextComponent (&container) == nullptr); + expect (traverser.getPreviousComponent (&container) == nullptr); + + expect (traverser.getDefaultComponent (&container) == &container.children.front()); + + for (auto iter = container.children.begin(); iter != container.children.end(); ++iter) + expect (traverser.getNextComponent (&(*iter)) == (iter == std::prev (container.children.cend()) ? nullptr + : &(*std::next (iter)))); + + for (auto iter = container.children.rbegin(); iter != container.children.rend(); ++iter) + expect (traverser.getPreviousComponent (&(*iter)) == (iter == std::prev (container.children.rend()) ? nullptr + : &(*std::next (iter)))); + + expect (traverser.getAllComponents (&root).size() == 1); + + auto allContainerComponents = traverser.getAllComponents (&container); + + expect (std::equal (allContainerComponents.cbegin(), allContainerComponents.cend(), container.children.cbegin(), + [] (const Component* c1, const Component& c2) { return c1 == &c2; })); + } + + beginTest ("Non-focus containers pass-through focus"); + { + Component root; + + TestComponent container; + container.setFocusContainerType (Component::FocusContainerType::none); + + root.addAndMakeVisible (container); + + expect (traverser.getDefaultComponent (&root) == &container); + expect (traverser.getNextComponent (&container) == &container.children.front()); + expect (traverser.getPreviousComponent (&container) == nullptr); + + expect (traverser.getDefaultComponent (&container) == &container.children.front()); + + for (auto iter = container.children.begin(); iter != container.children.end(); ++iter) + expect (traverser.getNextComponent (&(*iter)) == (iter == std::prev (container.children.cend()) ? nullptr + : &(*std::next (iter)))); + + for (auto iter = container.children.rbegin(); iter != container.children.rend(); ++iter) + expect (traverser.getPreviousComponent (&(*iter)) == (iter == std::prev (container.children.rend()) ? &container + : &(*std::next (iter)))); + + expect (traverser.getAllComponents (&root).size() == container.children.size() + 1); + } + } + +private: + struct TestComponent : public Component + { + TestComponent() + { + for (auto& child : children) + addAndMakeVisible (child); + } + + std::array children; + }; + + void checkComponentProperties (std::function&& childFn, + std::function&& testProperty) + { + TestComponent parent; + + for (auto& child : parent.children) + childFn (child); + + auto* comp = traverser.getDefaultComponent (&parent); + + for (const auto& child : parent.children) + if (&child != comp) + expect (testProperty (*comp, child)); + + for (;;) + { + auto* next = traverser.getNextComponent (comp); + + if (next == nullptr) + break; + + expect (testProperty (*comp, *next)); + comp = next; + } + } + + void checkIgnored (const std::function& makeIgnored) + { + TestComponent parent; + + auto iter = parent.children.begin(); + + makeIgnored (*iter); + expect (traverser.getDefaultComponent (&parent) == std::addressof (*std::next (iter))); + + iter += 5; + makeIgnored (*iter); + expect (traverser.getNextComponent (std::addressof (*std::prev (iter))) == std::addressof (*std::next (iter))); + expect (traverser.getPreviousComponent (std::addressof (*std::next (iter))) == std::addressof (*std::prev (iter))); + + auto allComponents = traverser.getAllComponents (&parent); + + expect (std::find (allComponents.cbegin(), allComponents.cend(), &parent.children.front()) == allComponents.cend()); + expect (std::find (allComponents.cbegin(), allComponents.cend(), std::addressof (*iter)) == allComponents.cend()); + } + + FocusTraverser traverser; +}; + +static FocusTraverserTests focusTraverserTests; + +#endif + +} // namespace juce diff --git a/modules/juce_gui_basics/components/juce_FocusTraverser.h b/modules/juce_gui_basics/components/juce_FocusTraverser.h new file mode 100644 index 0000000000..d2b43568eb --- /dev/null +++ b/modules/juce_gui_basics/components/juce_FocusTraverser.h @@ -0,0 +1,93 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + Controls the order in which focus moves between components. + + The algorithm used by this class to work out the order of traversal is as + follows: + - Only visible and enabled components are considered focusable. + - If two components both have an explicit focus order specified then the + one with the lowest number comes first (see the + Component::setExplicitFocusOrder() method). + - Any component with an explicit focus order greater than 0 comes before ones + that don't have an order specified. + - Components with their 'always on top' flag set come before those without. + - Any unspecified components are traversed in a left-to-right, then + top-to-bottom order. + + If you need focus traversal in a more customised way you can create a + ComponentTraverser subclass that uses your own algorithm and return it + from Component::createFocusTraverser(). + + @see ComponentTraverser, Component::createFocusTraverser + + @tags{GUI} +*/ +class JUCE_API FocusTraverser : public ComponentTraverser +{ +public: + /** Destructor. */ + ~FocusTraverser() override = default; + + /** Returns the component that should receive focus by default within the given + parent component. + + The default implementation will just return the foremost visible and enabled + child component, and will return nullptr if there is no suitable component. + */ + Component* getDefaultComponent (Component* parentComponent) override; + + /** Returns the component that should be given focus after the specified one when + moving "forwards". + + The default implementation will return the next visible and enabled component + which is to the right of or below this one, and will return nullptr if there + is no suitable component. + */ + Component* getNextComponent (Component* current) override; + + /** Returns the component that should be given focus after the specified one when + moving "backwards". + + The default implementation will return the previous visible and enabled component + which is to the left of or above this one, and will return nullptr if there + is no suitable component. + */ + Component* getPreviousComponent (Component* current) override; + + /** Returns all of the components that can receive focus within the given parent + component in traversal order. + + The default implementation will return all visible and enabled child components. + */ + std::vector getAllComponents (Component* parentComponent) override; +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/drawables/juce_Drawable.cpp b/modules/juce_gui_basics/drawables/juce_Drawable.cpp index a9bcbb18d3..59df9fdd93 100644 --- a/modules/juce_gui_basics/drawables/juce_Drawable.cpp +++ b/modules/juce_gui_basics/drawables/juce_Drawable.cpp @@ -204,4 +204,10 @@ std::unique_ptr Drawable::createFromImageFile (const File& file) return {}; } +//============================================================================== +std::unique_ptr Drawable::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::ignored); +} + } // namespace juce diff --git a/modules/juce_gui_basics/drawables/juce_Drawable.h b/modules/juce_gui_basics/drawables/juce_Drawable.h index 2cddd85d40..2798218952 100644 --- a/modules/juce_gui_basics/drawables/juce_Drawable.h +++ b/modules/juce_gui_basics/drawables/juce_Drawable.h @@ -199,6 +199,8 @@ protected: void setBoundsToEnclose (Rectangle); /** @internal */ void applyDrawableClipPath (Graphics&); + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; Point originRelativeToComponent; std::unique_ptr drawableClipPath; diff --git a/modules/juce_gui_basics/drawables/juce_DrawableImage.cpp b/modules/juce_gui_basics/drawables/juce_DrawableImage.cpp index 958444d4eb..4bd229a30b 100644 --- a/modules/juce_gui_basics/drawables/juce_DrawableImage.cpp +++ b/modules/juce_gui_basics/drawables/juce_DrawableImage.cpp @@ -133,4 +133,10 @@ Path DrawableImage::getOutlineAsPath() const return {}; // not applicable for images } +//============================================================================== +std::unique_ptr DrawableImage::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::image); +} + } // namespace juce diff --git a/modules/juce_gui_basics/drawables/juce_DrawableImage.h b/modules/juce_gui_basics/drawables/juce_DrawableImage.h index f2c7f4cca2..4f73490701 100644 --- a/modules/juce_gui_basics/drawables/juce_DrawableImage.h +++ b/modules/juce_gui_basics/drawables/juce_DrawableImage.h @@ -94,6 +94,8 @@ public: Rectangle getDrawableBounds() const override; /** @internal */ Path getOutlineAsPath() const override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: //============================================================================== diff --git a/modules/juce_gui_basics/drawables/juce_DrawableText.cpp b/modules/juce_gui_basics/drawables/juce_DrawableText.cpp index e444ea3344..3d1f6237fc 100644 --- a/modules/juce_gui_basics/drawables/juce_DrawableText.cpp +++ b/modules/juce_gui_basics/drawables/juce_DrawableText.cpp @@ -208,4 +208,25 @@ bool DrawableText::replaceColour (Colour originalColour, Colour replacementColou return true; } +//============================================================================== +std::unique_ptr DrawableText::createAccessibilityHandler() +{ + class DrawableTextAccessibilityHandler : public AccessibilityHandler + { + public: + DrawableTextAccessibilityHandler (DrawableText& drawableTextToWrap) + : AccessibilityHandler (drawableTextToWrap, AccessibilityRole::staticText), + drawableText (drawableTextToWrap) + { + } + + String getTitle() const override { return drawableText.getText(); } + + private: + DrawableText& drawableText; + }; + + return std::make_unique (*this); +} + } // namespace juce diff --git a/modules/juce_gui_basics/drawables/juce_DrawableText.h b/modules/juce_gui_basics/drawables/juce_DrawableText.h index 6eb84f935d..c29d02e9c5 100644 --- a/modules/juce_gui_basics/drawables/juce_DrawableText.h +++ b/modules/juce_gui_basics/drawables/juce_DrawableText.h @@ -98,6 +98,8 @@ public: Path getOutlineAsPath() const override; /** @internal */ bool replaceColour (Colour originalColour, Colour replacementColour) override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: //============================================================================== diff --git a/modules/juce_gui_basics/filebrowser/juce_FileBrowserComponent.cpp b/modules/juce_gui_basics/filebrowser/juce_FileBrowserComponent.cpp index df2add3a06..96ff84f721 100644 --- a/modules/juce_gui_basics/filebrowser/juce_FileBrowserComponent.cpp +++ b/modules/juce_gui_basics/filebrowser/juce_FileBrowserComponent.cpp @@ -616,4 +616,10 @@ void FileBrowserComponent::timerCallback() } } +//============================================================================== +std::unique_ptr FileBrowserComponent::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::group); +} + } // namespace juce diff --git a/modules/juce_gui_basics/filebrowser/juce_FileBrowserComponent.h b/modules/juce_gui_basics/filebrowser/juce_FileBrowserComponent.h index 4d25ec3279..ba43b4da12 100644 --- a/modules/juce_gui_basics/filebrowser/juce_FileBrowserComponent.h +++ b/modules/juce_gui_basics/filebrowser/juce_FileBrowserComponent.h @@ -252,6 +252,8 @@ public: FilePreviewComponent* getPreviewComponent() const noexcept; /** @internal */ DirectoryContentsDisplayComponent* getDisplayComponent() const noexcept; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; protected: /** Returns a list of names and paths for the default places the user might want to look. diff --git a/modules/juce_gui_basics/filebrowser/juce_FileListComponent.cpp b/modules/juce_gui_basics/filebrowser/juce_FileListComponent.cpp index b3a85d4299..f9a60afa17 100644 --- a/modules/juce_gui_basics/filebrowser/juce_FileListComponent.cpp +++ b/modules/juce_gui_basics/filebrowser/juce_FileListComponent.cpp @@ -35,6 +35,7 @@ FileListComponent::FileListComponent (DirectoryContentsList& listToShow) DirectoryContentsDisplayComponent (listToShow), lastDirectory (listToShow.getDirectory()) { + setTitle ("Files"); setModel (this); directoryContentsList.addChangeListener (this); } @@ -68,7 +69,7 @@ void FileListComponent::setSelectedFile (const File& f) { for (int i = directoryContentsList.getNumFiles(); --i >= 0;) { - if (directoryContentsList.getFile(i) == f) + if (directoryContentsList.getFile (i) == f) { fileWaitingToBeSelected = File(); @@ -189,6 +190,11 @@ public: repaint(); } + std::unique_ptr createAccessibilityHandler() override + { + return nullptr; + } + private: //============================================================================== FileListComponent& owner; @@ -231,6 +237,11 @@ int FileListComponent::getNumRows() return directoryContentsList.getNumFiles(); } +String FileListComponent::getNameForRow (int rowNumber) +{ + return directoryContentsList.getFile (rowNumber).getFileName(); +} + void FileListComponent::paintListBoxItem (int, Graphics&, int, int, bool) { } diff --git a/modules/juce_gui_basics/filebrowser/juce_FileListComponent.h b/modules/juce_gui_basics/filebrowser/juce_FileListComponent.h index 450e6c8e86..47fb9a7ac2 100644 --- a/modules/juce_gui_basics/filebrowser/juce_FileListComponent.h +++ b/modules/juce_gui_basics/filebrowser/juce_FileListComponent.h @@ -82,6 +82,7 @@ private: void changeListenerCallback (ChangeBroadcaster*) override; int getNumRows() override; + String getNameForRow (int rowNumber) override; void paintListBoxItem (int, Graphics&, int, int, bool) override; Component* refreshComponentForRow (int rowNumber, bool isRowSelected, Component*) override; void selectedRowsChanged (int row) override; diff --git a/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.cpp b/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.cpp index b7c8136926..003ba94d9b 100644 --- a/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.cpp +++ b/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.cpp @@ -190,6 +190,11 @@ public: indexInContentsList, owner); } + String getAccessibilityName() override + { + return file.getFileName(); + } + void itemClicked (const MouseEvent& e) override { owner.sendMouseClickMessage (file, e); diff --git a/modules/juce_gui_basics/filebrowser/juce_FilenameComponent.cpp b/modules/juce_gui_basics/filebrowser/juce_FilenameComponent.cpp index 48512ba8c2..8912eb92ad 100644 --- a/modules/juce_gui_basics/filebrowser/juce_FilenameComponent.cpp +++ b/modules/juce_gui_basics/filebrowser/juce_FilenameComponent.cpp @@ -70,11 +70,11 @@ void FilenameComponent::resized() getLookAndFeel().layoutFilenameComponent (*this, &filenameBox, browseButton.get()); } -KeyboardFocusTraverser* FilenameComponent::createFocusTraverser() +std::unique_ptr FilenameComponent::createKeyboardFocusTraverser() { // This prevents the sub-components from grabbing focus if the // FilenameComponent has been set to refuse focus. - return getWantsKeyboardFocus() ? Component::createFocusTraverser() : nullptr; + return getWantsKeyboardFocus() ? Component::createKeyboardFocusTraverser() : nullptr; } void FilenameComponent::setBrowseButtonText (const String& newBrowseButtonText) diff --git a/modules/juce_gui_basics/filebrowser/juce_FilenameComponent.h b/modules/juce_gui_basics/filebrowser/juce_FilenameComponent.h index 68042f17bd..8faefa68cf 100644 --- a/modules/juce_gui_basics/filebrowser/juce_FilenameComponent.h +++ b/modules/juce_gui_basics/filebrowser/juce_FilenameComponent.h @@ -212,7 +212,7 @@ public: /** @internal */ void fileDragExit (const StringArray&) override; /** @internal */ - KeyboardFocusTraverser* createFocusTraverser() override; + std::unique_ptr createKeyboardFocusTraverser() override; private: //============================================================================== diff --git a/modules/juce_gui_basics/filebrowser/juce_ImagePreviewComponent.cpp b/modules/juce_gui_basics/filebrowser/juce_ImagePreviewComponent.cpp index 1b37973aaa..df40edc817 100644 --- a/modules/juce_gui_basics/filebrowser/juce_ImagePreviewComponent.cpp +++ b/modules/juce_gui_basics/filebrowser/juce_ImagePreviewComponent.cpp @@ -117,4 +117,10 @@ void ImagePreviewComponent::paint (Graphics& g) } } +//============================================================================== +std::unique_ptr ImagePreviewComponent::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::image); +} + } // namespace juce diff --git a/modules/juce_gui_basics/filebrowser/juce_ImagePreviewComponent.h b/modules/juce_gui_basics/filebrowser/juce_ImagePreviewComponent.h index d2e625b8ab..caf537dea6 100644 --- a/modules/juce_gui_basics/filebrowser/juce_ImagePreviewComponent.h +++ b/modules/juce_gui_basics/filebrowser/juce_ImagePreviewComponent.h @@ -53,6 +53,8 @@ public: void paint (Graphics&) override; /** @internal */ void timerCallback() override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: File fileToLoad; diff --git a/modules/juce_gui_basics/juce_gui_basics.cpp b/modules/juce_gui_basics/juce_gui_basics.cpp index 849629c9ee..3555570c94 100644 --- a/modules/juce_gui_basics/juce_gui_basics.cpp +++ b/modules/juce_gui_basics/juce_gui_basics.cpp @@ -66,6 +66,8 @@ #include #include #include + #include + #include #if JUCE_WEB_BROWSER #include @@ -103,8 +105,10 @@ namespace juce extern bool juce_areThereAnyAlwaysOnTopWindows(); } +#include "accessibility/juce_AccessibilityHandler.cpp" #include "components/juce_Component.cpp" #include "components/juce_ComponentListener.cpp" +#include "components/juce_FocusTraverser.cpp" #include "mouse/juce_MouseInputSource.cpp" #include "desktop/juce_Displays.cpp" #include "desktop/juce_Desktop.cpp" @@ -240,6 +244,7 @@ namespace juce #endif #else + #include "native/accessibility/juce_mac_Accessibility.mm" #include "native/juce_mac_NSViewComponentPeer.mm" #include "native/juce_mac_Windowing.mm" #include "native/juce_mac_MainMenu.mm" @@ -249,6 +254,12 @@ namespace juce #include "native/juce_mac_MouseCursor.mm" #elif JUCE_WINDOWS + #include "native/accessibility/juce_win32_WindowsUIAWrapper.h" + #include "native/accessibility/juce_win32_AccessibilityElement.h" + #include "native/accessibility/juce_win32_UIAHelpers.h" + #include "native/accessibility/juce_win32_UIAProviders.h" + #include "native/accessibility/juce_win32_AccessibilityElement.cpp" + #include "native/accessibility/juce_win32_Accessibility.cpp" #include "native/juce_win32_Windowing.cpp" #include "native/juce_win32_DragAndDrop.cpp" #include "native/juce_win32_FileChooser.cpp" diff --git a/modules/juce_gui_basics/juce_gui_basics.h b/modules/juce_gui_basics/juce_gui_basics.h index de622b2e5b..4589f6fc0e 100644 --- a/modules/juce_gui_basics/juce_gui_basics.h +++ b/modules/juce_gui_basics/juce_gui_basics.h @@ -155,6 +155,8 @@ namespace juce class ApplicationCommandManagerListener; class DrawableButton; class Displays; + class AccessibilityHandler; + class KeyboardFocusTraverser; class FlexBox; class Grid; @@ -167,7 +169,8 @@ namespace juce #include "mouse/juce_MouseEvent.h" #include "keyboard/juce_KeyPress.h" #include "keyboard/juce_KeyListener.h" -#include "keyboard/juce_KeyboardFocusTraverser.h" +#include "components/juce_ComponentTraverser.h" +#include "components/juce_FocusTraverser.h" #include "components/juce_ModalComponentManager.h" #include "components/juce_ComponentListener.h" #include "components/juce_CachedComponentImage.h" @@ -185,6 +188,7 @@ namespace juce #include "mouse/juce_TextDragAndDropTarget.h" #include "mouse/juce_TooltipClient.h" #include "keyboard/juce_CaretComponent.h" +#include "keyboard/juce_KeyboardFocusTraverser.h" #include "keyboard/juce_SystemClipboard.h" #include "keyboard/juce_TextEditorKeyMapper.h" #include "keyboard/juce_TextInputTarget.h" @@ -293,6 +297,22 @@ namespace juce #include "lookandfeel/juce_LookAndFeel_V3.h" #include "lookandfeel/juce_LookAndFeel_V4.h" #include "mouse/juce_LassoComponent.h" +#include "accessibility/interfaces/juce_AccessibilityCellInterface.h" +#include "accessibility/interfaces/juce_AccessibilityTableInterface.h" +#include "accessibility/interfaces/juce_AccessibilityTextInterface.h" +#include "accessibility/interfaces/juce_AccessibilityValueInterface.h" +#include "accessibility/enums/juce_AccessibilityActions.h" +#include "accessibility/enums/juce_AccessibilityEvent.h" +#include "accessibility/enums/juce_AccessibilityRole.h" +#include "accessibility/juce_AccessibilityState.h" +#include "accessibility/juce_AccessibilityHandler.h" +#include "accessibility/widget_handlers/juce_ButtonAccessibilityHandler.h" +#include "accessibility/widget_handlers/juce_ComboBoxAccessibilityHandler.h" +#include "accessibility/widget_handlers/juce_LabelAccessibilityHandler.h" +#include "accessibility/widget_handlers/juce_SliderAccessibilityHandler.h" +#include "accessibility/widget_handlers/juce_TableListBoxAccessibilityHandler.h" +#include "accessibility/widget_handlers/juce_TextEditorAccessibilityHandler.h" +#include "accessibility/widget_handlers/juce_TreeViewAccessibilityHandler.h" #if JUCE_LINUX || JUCE_BSD #if JUCE_GUI_BASICS_INCLUDE_XHEADERS diff --git a/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.cpp b/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.cpp index 73b558df8e..a59585429f 100644 --- a/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.cpp +++ b/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.cpp @@ -26,105 +26,248 @@ namespace juce { -namespace KeyboardFocusHelpers +//============================================================================== +namespace KeyboardFocusTraverserHelpers { - static int getOrder (const Component* c) + static bool isKeyboardFocusable (const Component* comp, const Component* container) { - auto order = c->getExplicitFocusOrder(); - return order > 0 ? order : (std::numeric_limits::max() / 2); + return comp->getWantsKeyboardFocus() && container->isParentOf (comp); } - static void findAllFocusableComponents (Component* parent, Array& comps) + static Component* traverse (Component* current, Component* container, + FocusHelpers::NavigationDirection direction) { - if (parent->getNumChildComponents() != 0) + if (auto* comp = FocusHelpers::navigateFocus (current, container, direction, + &Component::isKeyboardFocusContainer)) { - Array localComps; + if (isKeyboardFocusable (comp, container)) + return comp; - for (auto* c : parent->getChildren()) - if (c->isVisible() && c->isEnabled()) - localComps.add (c); - - // This will sort so that they are ordered in terms of left-to-right - // and then top-to-bottom. - std::stable_sort (localComps.begin(), localComps.end(), - [] (const Component* a, const Component* b) - { - auto explicitOrder1 = getOrder (a); - auto explicitOrder2 = getOrder (b); - - if (explicitOrder1 != explicitOrder2) - return explicitOrder1 < explicitOrder2; - - if (a->getY() != b->getY()) - return a->getY() < b->getY(); - - return a->getX() < b->getX(); - }); - - for (auto* c : localComps) - { - if (c->getWantsKeyboardFocus()) - comps.add (c); - - if (! c->isFocusContainer()) - findAllFocusableComponents (c, comps); - } - } - } - - static Component* findFocusContainer (Component* c) - { - c = c->getParentComponent(); - - if (c != nullptr) - while (c->getParentComponent() != nullptr && ! c->isFocusContainer()) - c = c->getParentComponent(); - - return c; - } - - static Component* getIncrementedComponent (Component* current, int delta) - { - if (auto* focusContainer = findFocusContainer (current)) - { - Array comps; - KeyboardFocusHelpers::findAllFocusableComponents (focusContainer, comps); - - if (! comps.isEmpty()) - { - auto index = comps.indexOf (current); - return comps [(index + comps.size() + delta) % comps.size()]; - } + return traverse (comp, container, direction); } return nullptr; } } -//============================================================================== -KeyboardFocusTraverser::KeyboardFocusTraverser() {} -KeyboardFocusTraverser::~KeyboardFocusTraverser() {} - Component* KeyboardFocusTraverser::getNextComponent (Component* current) { - jassert (current != nullptr); - return KeyboardFocusHelpers::getIncrementedComponent (current, 1); + return KeyboardFocusTraverserHelpers::traverse (current, current->findKeyboardFocusContainer(), + FocusHelpers::NavigationDirection::forwards); } Component* KeyboardFocusTraverser::getPreviousComponent (Component* current) { - jassert (current != nullptr); - return KeyboardFocusHelpers::getIncrementedComponent (current, -1); + return KeyboardFocusTraverserHelpers::traverse (current, current->findKeyboardFocusContainer(), + FocusHelpers::NavigationDirection::backwards); } Component* KeyboardFocusTraverser::getDefaultComponent (Component* parentComponent) { - Array comps; + for (auto* comp : getAllComponents (parentComponent)) + if (KeyboardFocusTraverserHelpers::isKeyboardFocusable (comp, parentComponent)) + return comp; - if (parentComponent != nullptr) - KeyboardFocusHelpers::findAllFocusableComponents (parentComponent, comps); - - return comps.getFirst(); + return nullptr; } +std::vector KeyboardFocusTraverser::getAllComponents (Component* parentComponent) +{ + std::vector components; + FocusHelpers::findAllComponents (parentComponent, + components, + &Component::isKeyboardFocusContainer); + + auto removePredicate = [parentComponent] (const Component* comp) + { + return ! KeyboardFocusTraverserHelpers::isKeyboardFocusable (comp, parentComponent); + }; + + components.erase (std::remove_if (std::begin (components), std::end (components), std::move (removePredicate)), + std::end (components)); + + return components; +} + + +//============================================================================== +//============================================================================== +#if JUCE_UNIT_TESTS + +struct KeyboardFocusTraverserTests : public UnitTest +{ + KeyboardFocusTraverserTests() + : UnitTest ("KeyboardFocusTraverser", UnitTestCategories::gui) + {} + + void runTest() override + { + ScopedJuceInitialiser_GUI libraryInitialiser; + + beginTest ("No child wants keyboard focus"); + { + TestComponent parent; + + expect (traverser.getDefaultComponent (&parent) == nullptr); + expect (traverser.getAllComponents (&parent).empty()); + } + + beginTest ("Single child wants keyboard focus"); + { + TestComponent parent; + + parent.children[5].setWantsKeyboardFocus (true); + + auto* defaultComponent = traverser.getDefaultComponent (&parent); + + expect (defaultComponent == &parent.children[5]); + expect (defaultComponent->getWantsKeyboardFocus()); + + expect (traverser.getNextComponent (defaultComponent) == nullptr); + expect (traverser.getPreviousComponent (defaultComponent) == nullptr); + expect (traverser.getAllComponents (&parent).size() == 1); + } + + beginTest ("Multiple children want keyboard focus"); + { + TestComponent parent; + + Component* focusChildren[] + { + &parent.children[1], + &parent.children[9], + &parent.children[3], + &parent.children[5], + &parent.children[8], + &parent.children[0] + }; + + for (auto* focusChild : focusChildren) + focusChild->setWantsKeyboardFocus (true); + + auto allComponents = traverser.getAllComponents (&parent); + + for (auto* focusChild : focusChildren) + expect (std::find (allComponents.cbegin(), allComponents.cend(), focusChild) != allComponents.cend()); + + auto* componentToTest = traverser.getDefaultComponent (&parent); + + for (;;) + { + expect (componentToTest->getWantsKeyboardFocus()); + expect (std::find (std::begin (focusChildren), std::end (focusChildren), componentToTest) != std::end (focusChildren)); + + componentToTest = traverser.getNextComponent (componentToTest); + + if (componentToTest == nullptr) + break; + } + + int focusOrder = 1; + for (auto* focusChild : focusChildren) + focusChild->setExplicitFocusOrder (focusOrder++); + + componentToTest = traverser.getDefaultComponent (&parent); + + for (auto* focusChild : focusChildren) + { + expect (componentToTest == focusChild); + expect (componentToTest->getWantsKeyboardFocus()); + + componentToTest = traverser.getNextComponent (componentToTest); + } + } + + beginTest ("Single nested child wants keyboard focus"); + { + TestComponent parent; + Component grandparent; + + grandparent.addAndMakeVisible (parent); + + auto& focusChild = parent.children[5]; + + focusChild.setWantsKeyboardFocus (true); + + expect (traverser.getDefaultComponent (&grandparent) == &focusChild); + expect (traverser.getDefaultComponent (&parent) == &focusChild); + expect (traverser.getNextComponent (&focusChild) == nullptr); + expect (traverser.getPreviousComponent (&focusChild) == nullptr); + expect (traverser.getAllComponents (&parent).size() == 1); + } + + beginTest ("Multiple nested children want keyboard focus"); + { + TestComponent parent; + Component grandparent; + + grandparent.addAndMakeVisible (parent); + + Component* focusChildren[] + { + &parent.children[1], + &parent.children[4], + &parent.children[5] + }; + + for (auto* focusChild : focusChildren) + focusChild->setWantsKeyboardFocus (true); + + auto allComponents = traverser.getAllComponents (&parent); + + expect (std::equal (allComponents.cbegin(), allComponents.cend(), focusChildren, + [] (const Component* c1, const Component* c2) { return c1 == c2; })); + + const auto front = *focusChildren; + const auto back = *std::prev (std::end (focusChildren)); + + expect (traverser.getDefaultComponent (&grandparent) == front); + expect (traverser.getDefaultComponent (&parent) == front); + expect (traverser.getNextComponent (front) == *std::next (std::begin (focusChildren))); + expect (traverser.getPreviousComponent (back) == *std::prev (std::end (focusChildren), 2)); + + std::array otherParents; + + for (auto& p : otherParents) + { + grandparent.addAndMakeVisible (p); + p.setWantsKeyboardFocus (true); + } + + expect (traverser.getDefaultComponent (&grandparent) == front); + expect (traverser.getDefaultComponent (&parent) == front); + expect (traverser.getNextComponent (back) == &otherParents.front()); + expect (traverser.getNextComponent (&otherParents.back()) == nullptr); + expect (traverser.getAllComponents (&grandparent).size() == numElementsInArray (focusChildren) + otherParents.size()); + expect (traverser.getAllComponents (&parent).size() == (size_t) numElementsInArray (focusChildren)); + + for (auto* focusChild : focusChildren) + focusChild->setWantsKeyboardFocus (false); + + expect (traverser.getDefaultComponent (&grandparent) == &otherParents.front()); + expect (traverser.getDefaultComponent (&parent) == nullptr); + expect (traverser.getAllComponents (&grandparent).size() == otherParents.size()); + expect (traverser.getAllComponents (&parent).empty()); + } + } + +private: + struct TestComponent : public Component + { + TestComponent() + { + for (auto& child : children) + addAndMakeVisible (child); + } + + std::array children; + }; + + KeyboardFocusTraverser traverser; +}; + +static KeyboardFocusTraverserTests keyboardFocusTraverserTests; + +#endif + } // namespace juce diff --git a/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.h b/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.h index eccfc259ab..99cbd86515 100644 --- a/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.h +++ b/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.h @@ -28,63 +28,60 @@ namespace juce //============================================================================== /** - Controls the order in which focus moves between components. + Controls the order in which keyboard focus moves between components. - The default algorithm used by this class to work out the order of traversal - is as follows: - - if two components both have an explicit focus order specified, then the - one with the lowest number comes first (see the Component::setExplicitFocusOrder() - method). - - any component with an explicit focus order greater than 0 comes before ones - that don't have an order specified. - - any unspecified components are traversed in a left-to-right, then top-to-bottom - order. + The default behaviour of this class uses a FocusTraverser object internally to + determine the default/next/previous component until it finds one which wants + keyboard focus, as set by the Component::setWantsKeyboardFocus() method. - If you need traversal in a more customised way, you can create a subclass - of KeyboardFocusTraverser that uses your own algorithm, and use - Component::createFocusTraverser() to create it. + If you need keyboard focus traversal in a more customised way, you can create + a subclass of ComponentTraverser that uses your own algorithm, and use + Component::createKeyboardFocusTraverser() to create it. - @see Component::setExplicitFocusOrder, Component::createFocusTraverser + @see FocusTraverser, ComponentTraverser, Component::createKeyboardFocusTraverser @tags{GUI} */ -class JUCE_API KeyboardFocusTraverser +class JUCE_API KeyboardFocusTraverser : public ComponentTraverser { public: - KeyboardFocusTraverser(); - /** Destructor. */ - virtual ~KeyboardFocusTraverser(); + ~KeyboardFocusTraverser() override = default; - /** Returns the component that should be given focus after the specified one - when moving "forwards". + /** Returns the component that should receive keyboard focus by default within the + given parent component. - The default implementation will return the next component which is to the - right of or below this one. - - This may return nullptr if there's no suitable candidate. + The default implementation will return the foremost focusable component (as + determined by FocusTraverser) that also wants keyboard focus, or nullptr if + there is no suitable component. */ - virtual Component* getNextComponent (Component* current); + Component* getDefaultComponent (Component* parentComponent) override; - /** Returns the component that should be given focus after the specified one - when moving "backwards". + /** Returns the component that should be given keyboard focus after the specified + one when moving "forwards". - The default implementation will return the next component which is to the - left of or above this one. - - This may return nullptr if there's no suitable candidate. + The default implementation will return the next focusable component (as + determined by FocusTraverser) that also wants keyboard focus, or nullptr if + there is no suitable component. */ - virtual Component* getPreviousComponent (Component* current); + Component* getNextComponent (Component* current) override; - /** Returns the component that should receive focus be default within the given - parent component. + /** Returns the component that should be given keyboard focus after the specified + one when moving "backwards". - The default implementation will just return the foremost child component that - wants focus. - - This may return nullptr if there's no suitable candidate. + The default implementation will return the previous focusable component (as + determined by FocusTraverser) that also wants keyboard focus, or nullptr if + there is no suitable component. */ - virtual Component* getDefaultComponent (Component* parentComponent); + Component* getPreviousComponent (Component* current) override; + + /** Returns all of the components that can receive keyboard focus within the given + parent component in traversal order. + + The default implementation will return all focusable child components (as + determined by FocusTraverser) that also wants keyboard focus. + */ + std::vector getAllComponents (Component* parentComponent) override; }; } // namespace juce diff --git a/modules/juce_gui_basics/layout/juce_ComponentAnimator.cpp b/modules/juce_gui_basics/layout/juce_ComponentAnimator.cpp index 0660305805..0a652577de 100644 --- a/modules/juce_gui_basics/layout/juce_ComponentAnimator.cpp +++ b/modules/juce_gui_basics/layout/juce_ComponentAnimator.cpp @@ -150,6 +150,7 @@ public: { ProxyComponent (Component& c) { + setAccessible (false); setWantsKeyboardFocus (false); setBounds (c.getBounds()); setTransform (c.getTransform()); diff --git a/modules/juce_gui_basics/layout/juce_ConcertinaPanel.cpp b/modules/juce_gui_basics/layout/juce_ConcertinaPanel.cpp index ed56282f4f..43d7975ac6 100644 --- a/modules/juce_gui_basics/layout/juce_ConcertinaPanel.cpp +++ b/modules/juce_gui_basics/layout/juce_ConcertinaPanel.cpp @@ -459,4 +459,10 @@ void ConcertinaPanel::panelHeaderDoubleClicked (Component* component) setPanelSize (component, 0, true); } +//============================================================================== +std::unique_ptr ConcertinaPanel::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::group); +} + } // namespace juce diff --git a/modules/juce_gui_basics/layout/juce_ConcertinaPanel.h b/modules/juce_gui_basics/layout/juce_ConcertinaPanel.h index 5bd717e036..81c0cf0029 100644 --- a/modules/juce_gui_basics/layout/juce_ConcertinaPanel.h +++ b/modules/juce_gui_basics/layout/juce_ConcertinaPanel.h @@ -119,6 +119,10 @@ public: ConcertinaPanel&, Component&) = 0; }; + //============================================================================== + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; + private: void resized() override; diff --git a/modules/juce_gui_basics/layout/juce_GroupComponent.cpp b/modules/juce_gui_basics/layout/juce_GroupComponent.cpp index 7ce52c77a4..6835b9bf7b 100644 --- a/modules/juce_gui_basics/layout/juce_GroupComponent.cpp +++ b/modules/juce_gui_basics/layout/juce_GroupComponent.cpp @@ -69,4 +69,10 @@ void GroupComponent::paint (Graphics& g) void GroupComponent::enablementChanged() { repaint(); } void GroupComponent::colourChanged() { repaint(); } +//============================================================================== +std::unique_ptr GroupComponent::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::group); +} + } // namespace juce diff --git a/modules/juce_gui_basics/layout/juce_GroupComponent.h b/modules/juce_gui_basics/layout/juce_GroupComponent.h index c966a3118b..d306c4966e 100644 --- a/modules/juce_gui_basics/layout/juce_GroupComponent.h +++ b/modules/juce_gui_basics/layout/juce_GroupComponent.h @@ -98,6 +98,8 @@ public: void enablementChanged() override; /** @internal */ void colourChanged() override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: String text; diff --git a/modules/juce_gui_basics/layout/juce_ScrollBar.cpp b/modules/juce_gui_basics/layout/juce_ScrollBar.cpp index 3d99248e7c..d540d0497d 100644 --- a/modules/juce_gui_basics/layout/juce_ScrollBar.cpp +++ b/modules/juce_gui_basics/layout/juce_ScrollBar.cpp @@ -61,7 +61,7 @@ private: ScrollBar::ScrollBar (bool shouldBeVertical) : vertical (shouldBeVertical) { setRepaintsOnMouseActivity (true); - setFocusContainer (true); + setFocusContainerType (FocusContainerType::keyboardFocusContainer); } ScrollBar::~ScrollBar() @@ -440,4 +440,46 @@ bool ScrollBar::getVisibility() const noexcept && visibleRange.getLength() > 0.0); } +//============================================================================== +std::unique_ptr ScrollBar::createAccessibilityHandler() +{ + class ScrollBarValueInterface : public AccessibilityRangedNumericValueInterface + { + public: + explicit ScrollBarValueInterface (ScrollBar& scrollBarToWrap) + : scrollBar (scrollBarToWrap) + { + } + + bool isReadOnly() const override { return false; } + + double getCurrentValue() const override + { + return scrollBar.getCurrentRangeStart(); + } + + void setValue (double newValue) override + { + scrollBar.setCurrentRangeStart (newValue); + } + + AccessibleValueRange getRange() const override + { + if (scrollBar.getRangeLimit().isEmpty()) + return {}; + + return { { scrollBar.getMinimumRangeLimit(), scrollBar.getMaximumRangeLimit() }, + scrollBar.getSingleStepSize() }; + } + + private: + ScrollBar& scrollBar; + }; + + return std::make_unique (*this, + AccessibilityRole::scrollBar, + AccessibilityActions{}, + AccessibilityHandler::Interfaces { std::make_unique (*this) }); +} + } // namespace juce diff --git a/modules/juce_gui_basics/layout/juce_ScrollBar.h b/modules/juce_gui_basics/layout/juce_ScrollBar.h index 1b5e9edbd2..e1c05f7861 100644 --- a/modules/juce_gui_basics/layout/juce_ScrollBar.h +++ b/modules/juce_gui_basics/layout/juce_ScrollBar.h @@ -211,6 +211,11 @@ public: */ void setSingleStepSize (double newSingleStepSize) noexcept; + /** Returns the current step size. + @see setSingleStepSize + */ + double getSingleStepSize() const noexcept { return singleStepSize; } + /** Moves the scrollbar by a number of single-steps. This will move the bar by a multiple of its single-step interval (as @@ -409,6 +414,8 @@ public: void parentHierarchyChanged() override; /** @internal */ void setVisible (bool) override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: //============================================================================== diff --git a/modules/juce_gui_basics/layout/juce_SidePanel.cpp b/modules/juce_gui_basics/layout/juce_SidePanel.cpp index 825995e2d5..5ff9c730c0 100644 --- a/modules/juce_gui_basics/layout/juce_SidePanel.cpp +++ b/modules/juce_gui_basics/layout/juce_SidePanel.cpp @@ -294,4 +294,10 @@ bool SidePanel::isMouseEventInThisOrChildren (Component* eventComponent) return false; } +//============================================================================== +std::unique_ptr SidePanel::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::group); +} + } // namespace juce diff --git a/modules/juce_gui_basics/layout/juce_SidePanel.h b/modules/juce_gui_basics/layout/juce_SidePanel.h index ba9703ea25..e5455bac9d 100644 --- a/modules/juce_gui_basics/layout/juce_SidePanel.h +++ b/modules/juce_gui_basics/layout/juce_SidePanel.h @@ -195,6 +195,8 @@ public: void mouseDrag (const MouseEvent&) override; /** @internal */ void mouseUp (const MouseEvent&) override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: //============================================================================== diff --git a/modules/juce_gui_basics/layout/juce_TabbedButtonBar.cpp b/modules/juce_gui_basics/layout/juce_TabbedButtonBar.cpp index 5c7cb9a762..bc01909355 100644 --- a/modules/juce_gui_basics/layout/juce_TabbedButtonBar.cpp +++ b/modules/juce_gui_basics/layout/juce_TabbedButtonBar.cpp @@ -202,7 +202,7 @@ TabbedButtonBar::TabbedButtonBar (Orientation orientationToUse) setInterceptsMouseClicks (false, true); behindFrontTab.reset (new BehindFrontTabComp (*this)); addAndMakeVisible (behindFrontTab.get()); - setFocusContainer (true); + setFocusContainerType (FocusContainerType::keyboardFocusContainer); } TabbedButtonBar::~TabbedButtonBar() @@ -574,4 +574,10 @@ void TabbedButtonBar::showExtraItemsMenu() void TabbedButtonBar::currentTabChanged (int, const String&) {} void TabbedButtonBar::popupMenuClickOnTab (int, const String&) {} +//============================================================================== +std::unique_ptr TabbedButtonBar::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::group); +} + } // namespace juce diff --git a/modules/juce_gui_basics/layout/juce_TabbedButtonBar.h b/modules/juce_gui_basics/layout/juce_TabbedButtonBar.h index 48b962f981..9cbf898193 100644 --- a/modules/juce_gui_basics/layout/juce_TabbedButtonBar.h +++ b/modules/juce_gui_basics/layout/juce_TabbedButtonBar.h @@ -334,6 +334,8 @@ public: void resized() override; /** @internal */ void lookAndFeelChanged() override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; protected: //============================================================================== diff --git a/modules/juce_gui_basics/layout/juce_TabbedComponent.cpp b/modules/juce_gui_basics/layout/juce_TabbedComponent.cpp index 4c602433cf..8c765e3d7f 100644 --- a/modules/juce_gui_basics/layout/juce_TabbedComponent.cpp +++ b/modules/juce_gui_basics/layout/juce_TabbedComponent.cpp @@ -310,4 +310,10 @@ void TabbedComponent::changeCallback (int newCurrentTabIndex, const String& newT void TabbedComponent::currentTabChanged (int, const String&) {} void TabbedComponent::popupMenuClickOnTab (int, const String&) {} +//============================================================================== +std::unique_ptr TabbedComponent::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::group); +} + } // namespace juce diff --git a/modules/juce_gui_basics/layout/juce_TabbedComponent.h b/modules/juce_gui_basics/layout/juce_TabbedComponent.h index e79c0b90d8..71cfb9ef1f 100644 --- a/modules/juce_gui_basics/layout/juce_TabbedComponent.h +++ b/modules/juce_gui_basics/layout/juce_TabbedComponent.h @@ -197,6 +197,8 @@ public: void resized() override; /** @internal */ void lookAndFeelChanged() override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; protected: //============================================================================== diff --git a/modules/juce_gui_basics/menus/juce_BurgerMenuComponent.cpp b/modules/juce_gui_basics/menus/juce_BurgerMenuComponent.cpp index 0ab18d7dce..7e07f4d61f 100644 --- a/modules/juce_gui_basics/menus/juce_BurgerMenuComponent.cpp +++ b/modules/juce_gui_basics/menus/juce_BurgerMenuComponent.cpp @@ -293,4 +293,10 @@ void BurgerMenuComponent::lookAndFeelChanged() listBox.setRowHeight (roundToInt (getLookAndFeel().getPopupMenuFont().getHeight() * 2.0f)); } +//============================================================================== +std::unique_ptr BurgerMenuComponent::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::menuBar); +} + } // namespace juce diff --git a/modules/juce_gui_basics/menus/juce_BurgerMenuComponent.h b/modules/juce_gui_basics/menus/juce_BurgerMenuComponent.h index 7812f46f17..2ee231e6c4 100644 --- a/modules/juce_gui_basics/menus/juce_BurgerMenuComponent.h +++ b/modules/juce_gui_basics/menus/juce_BurgerMenuComponent.h @@ -71,6 +71,8 @@ public: /** @internal */ void lookAndFeelChanged() override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: //============================================================================== diff --git a/modules/juce_gui_basics/menus/juce_MenuBarComponent.cpp b/modules/juce_gui_basics/menus/juce_MenuBarComponent.cpp index 0bdbea830b..6a3109db50 100644 --- a/modules/juce_gui_basics/menus/juce_MenuBarComponent.cpp +++ b/modules/juce_gui_basics/menus/juce_MenuBarComponent.cpp @@ -26,11 +26,63 @@ namespace juce { +class MenuBarComponent::AccessibleItemComponent : public Component +{ +public: + AccessibleItemComponent (MenuBarComponent& comp, const String& menuItemName) + : owner (comp), + name (menuItemName) + { + setInterceptsMouseClicks (false, false); + } + + const String& getName() const noexcept { return name; } + + std::unique_ptr createAccessibilityHandler() override + { + class ComponentHandler : public AccessibilityHandler + { + public: + explicit ComponentHandler (AccessibleItemComponent& item) + : AccessibilityHandler (item, + AccessibilityRole::menuItem, + getAccessibilityActions (item)), + itemComponent (item) + { + } + + AccessibleState getCurrentState() const override + { + auto state = AccessibilityHandler::getCurrentState().withSelectable(); + + return state.isFocused() ? state.withSelected() : state; + } + + String getTitle() const override { return itemComponent.name; } + + private: + static AccessibilityActions getAccessibilityActions (AccessibleItemComponent& item) + { + auto showMenu = [&item] { item.owner.showMenu (item.owner.indexOfItemComponent (&item)); }; + + return AccessibilityActions().addAction (AccessibilityActionType::focus, + [&item] { item.owner.setItemUnderMouse (item.owner.indexOfItemComponent (&item)); }) + .addAction (AccessibilityActionType::press, showMenu) + .addAction (AccessibilityActionType::showMenu, showMenu); + } + + AccessibleItemComponent& itemComponent; + }; + + return std::make_unique (*this); + } + +private: + MenuBarComponent& owner; + const String name; +}; + MenuBarComponent::MenuBarComponent (MenuBarModel* m) - : model (nullptr), - itemUnderMouse (-1), - currentPopupIndex (-1), - topLevelIndexClicked (0) { setRepaintsOnMouseActivity (true); setWantsKeyboardFocus (false); @@ -70,77 +122,83 @@ void MenuBarComponent::setModel (MenuBarModel* const newModel) //============================================================================== void MenuBarComponent::paint (Graphics& g) { - const bool isMouseOverBar = currentPopupIndex >= 0 || itemUnderMouse >= 0 || isMouseOver(); + const auto isMouseOverBar = (currentPopupIndex >= 0 || itemUnderMouse >= 0 || isMouseOver()); - getLookAndFeel().drawMenuBarBackground (g, - getWidth(), - getHeight(), - isMouseOverBar, - *this); + getLookAndFeel().drawMenuBarBackground (g, getWidth(), getHeight(), isMouseOverBar, *this); - if (model != nullptr) + if (model == nullptr) + return; + + for (size_t i = 0; i < itemComponents.size(); ++i) { - for (int i = 0; i < menuNames.size(); ++i) - { - Graphics::ScopedSaveState ss (g); + const auto& itemComponent = itemComponents[i]; + const auto itemBounds = itemComponent->getBounds(); - g.setOrigin (xPositions [i], 0); - g.reduceClipRegion (0, 0, xPositions[i + 1] - xPositions[i], getHeight()); + Graphics::ScopedSaveState ss (g); - getLookAndFeel().drawMenuBarItem (g, - xPositions[i + 1] - xPositions[i], - getHeight(), - i, - menuNames[i], - i == itemUnderMouse, - i == currentPopupIndex, - isMouseOverBar, - *this); - } + g.setOrigin (itemBounds.getX(), 0); + g.reduceClipRegion (0, 0, itemBounds.getWidth(), itemBounds.getHeight()); + + getLookAndFeel().drawMenuBarItem (g, + itemBounds.getWidth(), + itemBounds.getHeight(), + (int) i, + itemComponent->getName(), + (int) i == itemUnderMouse, + (int) i == currentPopupIndex, + isMouseOverBar, + *this); } } void MenuBarComponent::resized() { - xPositions.clear(); int x = 0; - xPositions.add (x); - for (int i = 0; i < menuNames.size(); ++i) + for (size_t i = 0; i < itemComponents.size(); ++i) { - x += getLookAndFeel().getMenuBarItemWidth (*this, i, menuNames[i]); - xPositions.add (x); + auto& itemComponent = itemComponents[i]; + + auto w = getLookAndFeel().getMenuBarItemWidth (*this, (int) i, itemComponent->getName()); + itemComponent->setBounds (x, 0, w, getHeight()); + x += w; } } int MenuBarComponent::getItemAt (Point p) { - for (int i = 0; i < xPositions.size(); ++i) - if (p.x >= xPositions[i] && p.x < xPositions[i + 1]) - return reallyContains (p, true) ? i : -1; + for (size_t i = 0; i < itemComponents.size(); ++i) + if (itemComponents[i]->getBounds().contains (p) && reallyContains (p, true)) + return (int) i; return -1; } void MenuBarComponent::repaintMenuItem (int index) { - if (isPositiveAndBelow (index, xPositions.size())) + if (isPositiveAndBelow (index, (int) itemComponents.size())) { - const int x1 = xPositions [index]; - const int x2 = xPositions [index + 1]; + auto itemBounds = itemComponents[(size_t) index]->getBounds(); - repaint (x1 - 2, 0, x2 - x1 + 4, getHeight()); + repaint (itemBounds.getX() - 2, + 0, + itemBounds.getWidth() + 4, + itemBounds.getHeight()); } } -void MenuBarComponent::setItemUnderMouse (const int index) +void MenuBarComponent::setItemUnderMouse (int index) { - if (itemUnderMouse != index) - { - repaintMenuItem (itemUnderMouse); - itemUnderMouse = index; - repaintMenuItem (itemUnderMouse); - } + if (itemUnderMouse == index) + return; + + repaintMenuItem (itemUnderMouse); + itemUnderMouse = index; + repaintMenuItem (itemUnderMouse); + + if (isPositiveAndBelow (itemUnderMouse, (int) itemComponents.size())) + if (auto* handler = itemComponents[(size_t) itemUnderMouse]->getAccessibilityHandler()) + handler->grabFocus(); } void MenuBarComponent::setOpenItem (int index) @@ -156,7 +214,7 @@ void MenuBarComponent::setOpenItem (int index) currentPopupIndex = index; repaintMenuItem (currentPopupIndex); - Desktop& desktop = Desktop::getInstance(); + auto& desktop = Desktop::getInstance(); if (index >= 0) desktop.addGlobalMouseListener (this); @@ -180,30 +238,24 @@ void MenuBarComponent::showMenu (int index) setOpenItem (index); setItemUnderMouse (index); - if (index >= 0) + if (isPositiveAndBelow (index, (int) itemComponents.size())) { - PopupMenu m (model->getMenuForIndex (itemUnderMouse, - menuNames [itemUnderMouse])); + const auto& itemComponent = itemComponents[(size_t) index]; + auto m = model->getMenuForIndex (itemUnderMouse, itemComponent->getName()); if (m.lookAndFeel == nullptr) m.setLookAndFeel (&getLookAndFeel()); - const Rectangle itemPos (xPositions [index], 0, xPositions [index + 1] - xPositions [index], getHeight()); + auto itemBounds = itemComponent->getBounds(); m.showMenuAsync (PopupMenu::Options().withTargetComponent (this) - .withTargetScreenArea (localAreaToGlobal (itemPos)) - .withMinimumWidth (itemPos.getWidth()), - ModalCallbackFunction::forComponent (menuBarMenuDismissedCallback, this, index)); + .withTargetScreenArea (localAreaToGlobal (itemBounds)) + .withMinimumWidth (itemBounds.getWidth()), + [this, index] (int result) { menuDismissed (index, result); }); } } } -void MenuBarComponent::menuBarMenuDismissedCallback (int result, MenuBarComponent* bar, int topLevelIndex) -{ - if (bar != nullptr) - bar->menuDismissed (topLevelIndex, result); -} - void MenuBarComponent::menuDismissed (int topLevelIndex, int itemId) { topLevelIndexClicked = topLevelIndex; @@ -212,8 +264,7 @@ void MenuBarComponent::menuDismissed (int topLevelIndex, int itemId) void MenuBarComponent::handleCommandMessage (int commandId) { - const Point mousePos (getMouseXYRelative()); - updateItemUnderMouse (mousePos); + updateItemUnderMouse (getMouseXYRelative()); if (currentPopupIndex == topLevelIndexClicked) setOpenItem (-1); @@ -239,8 +290,7 @@ void MenuBarComponent::mouseDown (const MouseEvent& e) { if (currentPopupIndex < 0) { - const MouseEvent e2 (e.getEventRelativeTo (this)); - updateItemUnderMouse (e2.getPosition()); + updateItemUnderMouse (e.getEventRelativeTo (this).getPosition()); currentPopupIndex = -2; showMenu (itemUnderMouse); @@ -249,8 +299,7 @@ void MenuBarComponent::mouseDown (const MouseEvent& e) void MenuBarComponent::mouseDrag (const MouseEvent& e) { - const MouseEvent e2 (e.getEventRelativeTo (this)); - const int item = getItemAt (e2.getPosition()); + const auto item = getItemAt (e.getEventRelativeTo (this).getPosition()); if (item >= 0) showMenu (item); @@ -258,7 +307,7 @@ void MenuBarComponent::mouseDrag (const MouseEvent& e) void MenuBarComponent::mouseUp (const MouseEvent& e) { - const MouseEvent e2 (e.getEventRelativeTo (this)); + const auto e2 = e.getEventRelativeTo (this); updateItemUnderMouse (e2.getPosition()); @@ -271,13 +320,13 @@ void MenuBarComponent::mouseUp (const MouseEvent& e) void MenuBarComponent::mouseMove (const MouseEvent& e) { - const MouseEvent e2 (e.getEventRelativeTo (this)); + const auto e2 = e.getEventRelativeTo (this); if (lastMousePos != e2.getPosition()) { if (currentPopupIndex >= 0) { - const int item = getItemAt (e2.getPosition()); + const auto item = getItemAt (e2.getPosition()); if (item >= 0) showMenu (item); @@ -293,11 +342,11 @@ void MenuBarComponent::mouseMove (const MouseEvent& e) bool MenuBarComponent::keyPressed (const KeyPress& key) { - const int numMenus = menuNames.size(); + const auto numMenus = (int) itemComponents.size(); if (numMenus > 0) { - const int currentIndex = jlimit (0, numMenus - 1, currentPopupIndex); + const auto currentIndex = jlimit (0, numMenus - 1, currentPopupIndex); if (key.isKeyCode (KeyPress::leftKey)) { @@ -315,34 +364,69 @@ bool MenuBarComponent::keyPressed (const KeyPress& key) return false; } -void MenuBarComponent::menuBarItemsChanged (MenuBarModel* /*menuBarModel*/) +void MenuBarComponent::menuBarItemsChanged (MenuBarModel*) { StringArray newNames; if (model != nullptr) newNames = model->getMenuBarNames(); - if (newNames != menuNames) + auto itemsHaveChanged = [this, &newNames] { - menuNames = newNames; + if ((int) itemComponents.size() != newNames.size()) + return true; + + for (size_t i = 0; i < itemComponents.size(); ++i) + if (itemComponents[i]->getName() != newNames[(int) i]) + return true; + + return false; + }(); + + if (itemsHaveChanged) + { + updateItemComponents (newNames); + repaint(); resized(); } } -void MenuBarComponent::menuCommandInvoked (MenuBarModel* /*menuBarModel*/, - const ApplicationCommandTarget::InvocationInfo& info) +void MenuBarComponent::updateItemComponents (const StringArray& menuNames) +{ + itemComponents.clear(); + + for (const auto& name : menuNames) + { + itemComponents.push_back (std::make_unique (*this, name)); + addAndMakeVisible (*itemComponents.back()); + } +} + +int MenuBarComponent::indexOfItemComponent (AccessibleItemComponent* itemComponent) const +{ + const auto iter = std::find_if (itemComponents.cbegin(), itemComponents.cend(), + [itemComponent] (const std::unique_ptr& c) { return c.get() == itemComponent; }); + + if (iter != itemComponents.cend()) + return (int) std::distance (itemComponents.cbegin(), iter); + + jassertfalse; + return -1; +} + +void MenuBarComponent::menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo& info) { if (model == nullptr || (info.commandFlags & ApplicationCommandInfo::dontTriggerVisualFeedback) != 0) return; - for (int i = 0; i < menuNames.size(); ++i) + for (size_t i = 0; i < itemComponents.size(); ++i) { - const PopupMenu menu (model->getMenuForIndex (i, menuNames [i])); + const auto menu = model->getMenuForIndex ((int) i, itemComponents[i]->getName()); if (menu.containsCommandItem (info.commandID)) { - setItemUnderMouse (i); + setItemUnderMouse ((int) i); startTimer (200); break; } @@ -355,4 +439,20 @@ void MenuBarComponent::timerCallback() updateItemUnderMouse (getMouseXYRelative()); } +//============================================================================== +std::unique_ptr MenuBarComponent::createAccessibilityHandler() +{ + struct MenuBarComponentAccessibilityHandler : public AccessibilityHandler + { + explicit MenuBarComponentAccessibilityHandler (MenuBarComponent& menuBarComponent) + : AccessibilityHandler (menuBarComponent, AccessibilityRole::menuBar) + { + } + + AccessibleState getCurrentState() const override { return AccessibleState().withIgnored(); } + }; + + return std::make_unique (*this); +} + } // namespace juce diff --git a/modules/juce_gui_basics/menus/juce_MenuBarComponent.h b/modules/juce_gui_basics/menus/juce_MenuBarComponent.h index ac3aef8a0a..f10b6e1b2f 100644 --- a/modules/juce_gui_basics/menus/juce_MenuBarComponent.h +++ b/modules/juce_gui_basics/menus/juce_MenuBarComponent.h @@ -95,24 +95,32 @@ public: void menuBarItemsChanged (MenuBarModel*) override; /** @internal */ void menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo&) override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: //============================================================================== - MenuBarModel* model; + class AccessibleItemComponent; - StringArray menuNames; - Array xPositions; - Point lastMousePos; - int itemUnderMouse, currentPopupIndex, topLevelIndexClicked; + //============================================================================== + void timerCallback() override; int getItemAt (Point); - void setItemUnderMouse (int index); - void setOpenItem (int index); + void setItemUnderMouse (int); + void setOpenItem (int); void updateItemUnderMouse (Point); - void timerCallback() override; - void repaintMenuItem (int index); - void menuDismissed (int topLevelIndex, int itemId); - static void menuBarMenuDismissedCallback (int, MenuBarComponent*, int); + void repaintMenuItem (int); + void menuDismissed (int, int); + + void updateItemComponents (const StringArray&); + int indexOfItemComponent (AccessibleItemComponent*) const; + + //============================================================================== + MenuBarModel* model = nullptr; + std::vector> itemComponents; + + Point lastMousePos; + int itemUnderMouse = -1, currentPopupIndex = -1, topLevelIndexClicked = 0; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuBarComponent) }; diff --git a/modules/juce_gui_basics/menus/juce_PopupMenu.cpp b/modules/juce_gui_basics/menus/juce_PopupMenu.cpp index 3ef7f4ab14..2e7efa3abe 100644 --- a/modules/juce_gui_basics/menus/juce_PopupMenu.cpp +++ b/modules/juce_gui_basics/menus/juce_PopupMenu.cpp @@ -84,7 +84,7 @@ struct ItemComponent : public Component ItemComponent (const PopupMenu::Item& i, const PopupMenu::Options& o, MenuWindow& parent) - : item (i), options (o), customComp (i.customComponent) + : item (i), parentWindow (parent), options (o), customComp (i.customComponent) { if (item.isSectionHeader) customComp = *new HeaderItemComponent (item.text, options); @@ -156,13 +156,99 @@ struct ItemComponent : public Component if (customComp != nullptr) customComp->setHighlighted (shouldBeHighlighted); + if (isHighlighted) + if (auto* handler = getAccessibilityHandler()) + handler->grabFocus(); + repaint(); } } + std::unique_ptr createAccessibilityHandler() override + { + return item.isSeparator ? nullptr : std::make_unique (*this); + } + PopupMenu::Item item; private: + //============================================================================== + class ItemAccessibilityHandler : public AccessibilityHandler + { + public: + explicit ItemAccessibilityHandler (ItemComponent& itemComponentToWrap) + : AccessibilityHandler (itemComponentToWrap, + AccessibilityRole::menuItem, + getAccessibilityActions (*this, itemComponentToWrap)), + itemComponent (itemComponentToWrap) + { + } + + String getTitle() const override + { + return itemComponent.item.text; + } + + AccessibleState getCurrentState() const override + { + auto state = AccessibilityHandler::getCurrentState().withSelectable() + .withAccessibleOffscreen(); + + if (hasActiveSubMenu (itemComponent.item)) + { + state = itemComponent.parentWindow.isSubMenuVisible() ? state.withExpandable().withExpanded() + : state.withExpandable().withCollapsed(); + } + + return state.isFocused() ? state.withSelected() : state; + } + + private: + static AccessibilityActions getAccessibilityActions (ItemAccessibilityHandler& handler, + ItemComponent& item) + { + auto onFocus = [&item] + { + item.parentWindow.disableTimerUntilMouseMoves(); + item.parentWindow.ensureItemComponentIsVisible (item, -1); + item.parentWindow.setCurrentlyHighlightedChild (&item); + }; + + auto onPress = [&item] + { + item.parentWindow.setCurrentlyHighlightedChild (&item); + item.parentWindow.triggerCurrentlyHighlightedItem(); + }; + + auto onToggle = [&handler, &item, onFocus] + { + if (handler.getCurrentState().isSelected()) + item.parentWindow.setCurrentlyHighlightedChild (nullptr); + else + onFocus(); + }; + + auto actions = AccessibilityActions().addAction (AccessibilityActionType::focus, std::move (onFocus)) + .addAction (AccessibilityActionType::press, std::move (onPress)) + .addAction (AccessibilityActionType::toggle, std::move (onToggle)); + + if (hasActiveSubMenu (item.item)) + actions.addAction (AccessibilityActionType::showMenu, [&item] + { + item.parentWindow.showSubMenuFor (&item); + + if (auto* subMenu = item.parentWindow.activeSubMenu.get()) + subMenu->setCurrentlyHighlightedChild (subMenu->items.getFirst()); + }); + + return actions; + } + + ItemComponent& itemComponent; + }; + + //============================================================================== + MenuWindow& parentWindow; const PopupMenu::Options& options; // NB: we use a copy of the one from the item info in case we're using our own section comp ReferenceCountedObjectPtr customComp; @@ -223,6 +309,7 @@ struct MenuWindow : public Component setWantsKeyboardFocus (false); setMouseClickGrabsKeyboardFocus (false); setAlwaysOnTop (true); + setFocusContainerType (FocusContainerType::focusContainer); setLookAndFeel (parent != nullptr ? &(parent->getLookAndFeel()) : menu.lookAndFeel.get()); @@ -275,11 +362,19 @@ struct MenuWindow : public Component if (auto visibleID = options.getItemThatMustBeVisible()) { - auto targetPosition = parentComponent != nullptr ? parentComponent->getLocalPoint (nullptr, targetArea.getTopLeft()) - : targetArea.getTopLeft(); + for (auto* item : items) + { + if (item->item.itemID == visibleID) + { + auto targetPosition = parentComponent != nullptr ? parentComponent->getLocalPoint (nullptr, targetArea.getTopLeft()) + : targetArea.getTopLeft(); - auto y = targetPosition.getY() - windowPos.getY(); - ensureItemIsVisible (visibleID, isPositiveAndBelow (y, windowPos.getHeight()) ? y : -1); + auto y = targetPosition.getY() - windowPos.getY(); + ensureItemComponentIsVisible (*item, isPositiveAndBelow (y, windowPos.getHeight()) ? y : -1); + + break; + } + } } resizeToBestWindowPos(); @@ -887,47 +982,36 @@ struct MenuWindow : public Component return correctColumnWidths (maxMenuW); } - void ensureItemIsVisible (const int itemID, int wantedY) + void ensureItemComponentIsVisible (const ItemComponent& itemComp, int wantedY) { - jassert (itemID != 0); - - for (int i = items.size(); --i >= 0;) + if (windowPos.getHeight() > PopupMenuSettings::scrollZone * 4) { - if (auto* m = items.getUnchecked (i)) + auto currentY = itemComp.getY(); + + if (wantedY > 0 || currentY < 0 || itemComp.getBottom() > windowPos.getHeight()) { - if (m->item.itemID == itemID - && windowPos.getHeight() > PopupMenuSettings::scrollZone * 4) - { - auto currentY = m->getY(); + if (wantedY < 0) + wantedY = jlimit (PopupMenuSettings::scrollZone, + jmax (PopupMenuSettings::scrollZone, + windowPos.getHeight() - (PopupMenuSettings::scrollZone + itemComp.getHeight())), + currentY); - if (wantedY > 0 || currentY < 0 || m->getBottom() > windowPos.getHeight()) - { - if (wantedY < 0) - wantedY = jlimit (PopupMenuSettings::scrollZone, - jmax (PopupMenuSettings::scrollZone, - windowPos.getHeight() - (PopupMenuSettings::scrollZone + m->getHeight())), - currentY); + auto parentArea = getParentArea (windowPos.getPosition(), parentComponent) / scaleFactor; + auto deltaY = wantedY - currentY; - auto parentArea = getParentArea (windowPos.getPosition(), parentComponent) / scaleFactor; - auto deltaY = wantedY - currentY; + windowPos.setSize (jmin (windowPos.getWidth(), parentArea.getWidth()), + jmin (windowPos.getHeight(), parentArea.getHeight())); - windowPos.setSize (jmin (windowPos.getWidth(), parentArea.getWidth()), - jmin (windowPos.getHeight(), parentArea.getHeight())); + auto newY = jlimit (parentArea.getY(), + parentArea.getBottom() - windowPos.getHeight(), + windowPos.getY() + deltaY); - auto newY = jlimit (parentArea.getY(), - parentArea.getBottom() - windowPos.getHeight(), - windowPos.getY() + deltaY); + deltaY -= newY - windowPos.getY(); - deltaY -= newY - windowPos.getY(); + childYOffset -= deltaY; + windowPos.setPosition (windowPos.getX(), newY); - childYOffset -= deltaY; - windowPos.setPosition (windowPos.getX(), newY); - - updateYPositions(); - } - - break; - } + updateYPositions(); } } } @@ -1016,6 +1100,9 @@ struct MenuWindow : public Component void setCurrentlyHighlightedChild (ItemComponent* child) { + if (currentChild == child) + return; + if (currentChild != nullptr) currentChild->setHighlighted (false); @@ -1026,6 +1113,9 @@ struct MenuWindow : public Component currentChild->setHighlighted (true); timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter(); } + + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::rowSelectionChanged); } bool isSubMenuVisible() const noexcept { return activeSubMenu != nullptr && activeSubMenu->isVisible(); } @@ -1119,6 +1209,19 @@ struct MenuWindow : public Component bool isTopScrollZoneActive() const noexcept { return canScroll() && childYOffset > 0; } bool isBottomScrollZoneActive() const noexcept { return canScroll() && childYOffset < contentHeight - windowPos.getHeight(); } + //============================================================================== + std::unique_ptr createAccessibilityHandler() override + { + return std::make_unique (*this, + AccessibilityRole::popupMenu, + AccessibilityActions().addAction (AccessibilityActionType::focus, [this] + { + if (currentChild != nullptr) + if (auto* handler = currentChild->getAccessibilityHandler()) + handler->grabFocus(); + })); + } + //============================================================================== MenuWindow* parent; const Options options; @@ -1925,6 +2028,9 @@ int PopupMenu::showWithOptionalCallback (const Options& options, window->toFront (false); // need to do this after making it modal, or it could // be stuck behind other comps that are already modal.. + if (auto* handler = window->getAccessibilityHandler()) + handler->grabFocus(); + #if JUCE_MODAL_LOOPS_PERMITTED if (userCallback == nullptr && canBeModal) return window->runModalLoop(); diff --git a/modules/juce_gui_basics/misc/juce_BubbleComponent.h b/modules/juce_gui_basics/misc/juce_BubbleComponent.h index f086c8ff37..33a49c3557 100644 --- a/modules/juce_gui_basics/misc/juce_BubbleComponent.h +++ b/modules/juce_gui_basics/misc/juce_BubbleComponent.h @@ -156,6 +156,10 @@ public: const Rectangle& body) = 0; }; + //============================================================================== + /** @internal */ + void paint (Graphics&) override; + protected: //============================================================================== /** Subclasses should override this to return the size of the content they @@ -170,10 +174,6 @@ protected: */ virtual void paintContent (Graphics& g, int width, int height) = 0; -public: - /** @internal */ - void paint (Graphics&) override; - private: Rectangle content; Point arrowTip; diff --git a/modules/juce_gui_basics/misc/juce_DropShadower.cpp b/modules/juce_gui_basics/misc/juce_DropShadower.cpp index 41f2d6f9f9..0994c4ae0b 100644 --- a/modules/juce_gui_basics/misc/juce_DropShadower.cpp +++ b/modules/juce_gui_basics/misc/juce_DropShadower.cpp @@ -33,6 +33,7 @@ public: : target (comp), shadow (ds) { setVisible (true); + setAccessible (false); setInterceptsMouseClicks (false, false); if (comp->isOnDesktop()) diff --git a/modules/juce_gui_basics/misc/juce_JUCESplashScreen.cpp b/modules/juce_gui_basics/misc/juce_JUCESplashScreen.cpp index 4276326619..06dc9a9c52 100644 --- a/modules/juce_gui_basics/misc/juce_JUCESplashScreen.cpp +++ b/modules/juce_gui_basics/misc/juce_JUCESplashScreen.cpp @@ -188,6 +188,12 @@ void JUCESplashScreen::mouseUp (const MouseEvent&) juceWebsite.launchInDefaultBrowser(); } +//============================================================================== +std::unique_ptr JUCESplashScreen::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::splashScreen); +} + // END SECTION A } // namespace juce diff --git a/modules/juce_gui_basics/misc/juce_JUCESplashScreen.h b/modules/juce_gui_basics/misc/juce_JUCESplashScreen.h index 3767fc3017..d4e8f63c5f 100644 --- a/modules/juce_gui_basics/misc/juce_JUCESplashScreen.h +++ b/modules/juce_gui_basics/misc/juce_JUCESplashScreen.h @@ -55,6 +55,10 @@ public: static std::unique_ptr getSplashScreenLogo(); + //============================================================================== + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; + private: void paint (Graphics&) override; void timerCallback() override; diff --git a/modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm b/modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm new file mode 100644 index 0000000000..408e531452 --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm @@ -0,0 +1,1078 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +#if (! defined MAC_OS_X_VERSION_10_13) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_13 + using NSAccessibilityRole = NSString*; + using NSAccessibilityNotificationName = NSString*; +#endif + +//============================================================================== +class AccessibilityHandler::AccessibilityNativeImpl +{ +public: + explicit AccessibilityNativeImpl (AccessibilityHandler& handler) + : accessibilityElement (AccessibilityElement::create (handler)) + {} + + NSAccessibilityElement* getAccessibilityElement() const noexcept + { + return accessibilityElement.get(); + } + +private: + //============================================================================== + class AccessibilityElement : public ObjCClass> + { + private: + struct Deleter + { + void operator() (NSAccessibilityElement* element) const + { + object_setInstanceVariable (element, "handler", nullptr); + [element release]; + } + }; + + public: + using Holder = std::unique_ptr, Deleter>; + + static Holder create (AccessibilityHandler& handler) + { + static AccessibilityElement cls; + Holder element ([cls.createInstance() init]); + object_setInstanceVariable (element.get(), "handler", &handler); + return element; + } + + private: + AccessibilityElement() : ObjCClass> ("JUCEAccessibilityElement_") + { + addIvar ("handler"); + + addMethod (@selector (accessibilityNotifiesWhenDestroyed), getAccessibilityNotifiesWhenDestroyed, "c@:"); + addMethod (@selector (isAccessibilityElement), getIsAccessibilityElement, "c@:"); + addMethod (@selector (isAccessibilityEnabled), getIsAccessibilityEnabled, "c@:"); + addMethod (@selector (accessibilityWindow), getAccessibilityWindow, "@@:"); + addMethod (@selector (accessibilityTopLevelUIElement), getAccessibilityWindow, "@@:"); + addMethod (@selector (accessibilityFocusedUIElement), getAccessibilityFocusedUIElement, "@@:"); + addMethod (@selector (accessibilityHitTest:), accessibilityHitTest, "@@:", @encode (NSPoint)); + addMethod (@selector (accessibilityParent), getAccessibilityParent, "@@:"); + addMethod (@selector (accessibilityChildren), getAccessibilityChildren, "@@:"); + addMethod (@selector (isAccessibilityFocused), getIsAccessibilityFocused, "c@:"); + addMethod (@selector (setAccessibilityFocused:), setAccessibilityFocused, "v@:c"); + addMethod (@selector (isAccessibilityModal), getIsAccessibilityModal, "c@:"); + addMethod (@selector (accessibilityFrame), getAccessibilityFrame, @encode (NSRect), "@:"); + addMethod (@selector (accessibilityRole), getAccessibilityRole, "@@:"); + addMethod (@selector (accessibilitySubrole), getAccessibilitySubrole, "@@:"); + addMethod (@selector (accessibilityTitle), getAccessibilityTitle, "@@:"); + addMethod (@selector (accessibilityLabel), getAccessibilityLabel, "@@:"); + addMethod (@selector (accessibilityHelp), getAccessibilityHelp, "@@:"); + addMethod (@selector (accessibilityValue), getAccessibilityValue, "@@:"); + addMethod (@selector (setAccessibilityValue:), setAccessibilityValue, "v@:@"); + addMethod (@selector (accessibilitySelectedChildren), getAccessibilitySelectedChildren, "@@:"); + addMethod (@selector (setAccessibilitySelectedChildren:), setAccessibilitySelectedChildren, "v@:@"); + addMethod (@selector (accessibilityOrientation), getAccessibilityOrientation, "i@:@"); + + addMethod (@selector (accessibilityInsertionPointLineNumber), getAccessibilityInsertionPointLineNumber, "i@:"); + addMethod (@selector (accessibilitySharedCharacterRange), getAccessibilitySharedCharacterRange, @encode (NSRange), "@:"); + addMethod (@selector (accessibilitySharedTextUIElements), getAccessibilitySharedTextUIElements, "@@:"); + addMethod (@selector (accessibilityVisibleCharacterRange), getAccessibilityVisibleCharacterRange, @encode (NSRange), "@:"); + addMethod (@selector (accessibilityNumberOfCharacters), getAccessibilityNumberOfCharacters, "i@:"); + addMethod (@selector (accessibilitySelectedText), getAccessibilitySelectedText, "@@:"); + addMethod (@selector (accessibilitySelectedTextRange), getAccessibilitySelectedTextRange, @encode (NSRange), "@:"); + addMethod (@selector (accessibilitySelectedTextRanges), getAccessibilitySelectedTextRanges, "@@:"); + addMethod (@selector (accessibilityAttributedStringForRange:), getAccessibilityAttributedStringForRange, "@@:", @encode (NSRange)); + addMethod (@selector (accessibilityRangeForLine:), getAccessibilityRangeForLine, @encode (NSRange), "@:i"); + addMethod (@selector (accessibilityStringForRange:), getAccessibilityStringForRange, "@@:", @encode (NSRange)); + addMethod (@selector (accessibilityRangeForPosition:), getAccessibilityRangeForPosition, @encode (NSRange), "@:", @encode (NSPoint)); + addMethod (@selector (accessibilityRangeForIndex:), getAccessibilityRangeForIndex, @encode (NSRange), "@:i"); + addMethod (@selector (accessibilityFrameForRange:), getAccessibilityFrameForRange, @encode (NSRect), "@:", @encode (NSRange)); + addMethod (@selector (accessibilityRTFForRange:), getAccessibilityRTFForRange, "@@:", @encode (NSRange)); + addMethod (@selector (accessibilityStyleRangeForIndex:), getAccessibilityStyleRangeForIndex, @encode (NSRange), "@:i"); + addMethod (@selector (accessibilityLineForIndex:), getAccessibilityLineForIndex, "i@:i"); + addMethod (@selector (setAccessibilitySelectedTextRange:), setAccessibilitySelectedTextRange, "v@:", @encode (NSRange)); + + addMethod (@selector (accessibilityRowCount), getAccessibilityRowCount, "i@:"); + addMethod (@selector (accessibilityRows), getAccessibilityRows, "@@:"); + addMethod (@selector (accessibilityColumnCount), getAccessibilityColumnCount, "i@:"); + addMethod (@selector (accessibilityColumns), getAccessibilityColumns, "@@:"); + + addMethod (@selector (accessibilityRowIndexRange), getAccessibilityRowIndexRange, @encode (NSRange), "@:"); + addMethod (@selector (accessibilityColumnIndexRange), getAccessibilityColumnIndexRange, @encode (NSRange), "@:"); + addMethod (@selector (accessibilityIndex), getAccessibilityIndex, "i@:"); + addMethod (@selector (accessibilityDisclosureLevel), getAccessibilityDisclosureLevel, "i@:"); + + addMethod (@selector (accessibilityPerformIncrement), accessibilityPerformIncrement, "c@:"); + addMethod (@selector (accessibilityPerformDecrement), accessibilityPerformDecrement, "c@:"); + addMethod (@selector (accessibilityPerformDelete), accessibilityPerformDelete, "c@:"); + addMethod (@selector (accessibilityPerformPress), accessibilityPerformPress, "c@:"); + addMethod (@selector (accessibilityPerformRaise), accessibilityPerformRaise, "c@:"); + addMethod (@selector (accessibilityPerformShowMenu), accessibilityPerformShowMenu, "c@:"); + addMethod (@selector (accessibilityPerformPick), accessibilityPerformPick, "c@:"); + + addMethod (@selector (isAccessibilitySelectorAllowed:), getIsAccessibilitySelectorAllowed, "c@:@"); + + #if defined (MAC_OS_X_VERSION_10_13) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_13 + addMethod (@selector (accessibilityChildrenInNavigationOrder), getAccessibilityChildren, "@@:"); + #endif + + registerClass(); + } + + private: + static AccessibilityHandler* getHandler (id self) { return getIvar (self, "handler"); } + + template + static auto getInterface (id self, MemberFn fn) noexcept -> decltype ((std::declval().*fn)()) + { + if (auto* handler = getHandler (self)) + return (handler->*fn)(); + + return nullptr; + } + + static AccessibilityTextInterface* getTextInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getTextInterface); } + static AccessibilityValueInterface* getValueInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getValueInterface); } + static AccessibilityTableInterface* getTableInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getTableInterface); } + static AccessibilityCellInterface* getCellInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getCellInterface); } + + static bool hasEditableText (AccessibilityHandler& handler) noexcept + { + return handler.getRole() == AccessibilityRole::editableText + && handler.getTextInterface() != nullptr; + } + + static bool nameIsAccessibilityValue (AccessibilityRole role) noexcept + { + return role == AccessibilityRole::staticText; + } + + static bool hasSelection (AccessibilityRole role) noexcept + { + return role == AccessibilityRole::list + || role == AccessibilityRole::popupMenu + || role == AccessibilityRole::tree; + } + + static bool isSelectable (AccessibilityRole role) noexcept + { + return role == AccessibilityRole::listItem + || role == AccessibilityRole::menuItem + || role == AccessibilityRole::treeItem; + } + + static BOOL performActionIfSupported (id self, AccessibilityActionType actionType) + { + if (auto* handler = getHandler (self)) + if (handler->getActions().invoke (actionType)) + return YES; + + return NO; + } + + //============================================================================== + static BOOL getAccessibilityNotifiesWhenDestroyed (id, SEL) { return YES; } + + static BOOL getIsAccessibilityElement (id self, SEL) + { + if (auto* handler = getHandler (self)) + return ! handler->isIgnored() + && handler->getRole() != AccessibilityRole::window; + + return NO; + } + + static BOOL getIsAccessibilityEnabled (id self, SEL) + { + if (auto* handler = getHandler (self)) + return handler->getComponent().isEnabled(); + + return NO; + } + + static id getAccessibilityWindow (id self, SEL) + { + return [[self accessibilityParent] accessibilityWindow]; + } + + static id getAccessibilityFocusedUIElement (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + if (auto* modal = Component::getCurrentlyModalComponent()) + { + const auto& component = handler->getComponent(); + + if (! component.isParentOf (modal) + && component.isCurrentlyBlockedByAnotherModalComponent()) + { + if (auto* modalHandler = modal->getAccessibilityHandler()) + { + if (auto* focusChild = modalHandler->getChildFocus()) + return (id) focusChild->getNativeImplementation(); + + return (id) modalHandler->getNativeImplementation(); + } + } + } + + if (auto* focusChild = handler->getChildFocus()) + return (id) focusChild->getNativeImplementation(); + } + + return nil; + } + + static id accessibilityHitTest (id self, SEL, NSPoint point) + { + if (auto* handler = getHandler (self)) + { + if (auto* child = handler->getChildAt (convertToIntPoint (flippedScreenPoint (point)))) + return (id) child->getNativeImplementation(); + + return self; + } + + return nil; + } + + static id getAccessibilityParent (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + if (auto* parentHandler = handler->getParent()) + return NSAccessibilityUnignoredAncestor ((id) parentHandler->getNativeImplementation()); + + return NSAccessibilityUnignoredAncestor ((id) handler->getComponent().getWindowHandle()); + } + + return nil; + } + + static NSArray* getAccessibilityChildren (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + auto children = handler->getChildren(); + + NSMutableArray* accessibleChildren = [NSMutableArray arrayWithCapacity: (NSUInteger) children.size()]; + + for (auto* childHandler : children) + [accessibleChildren addObject: (id) childHandler->getNativeImplementation()]; + + return accessibleChildren; + } + + return nil; + } + + static NSArray* getAccessibilitySelectedChildren (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + NSMutableArray* selected = [[NSMutableArray new] autorelease]; + + for (auto* child : handler->getChildren()) + if (isSelectable (child->getRole()) && child->getCurrentState().isSelected()) + [selected addObject: (id) child->getNativeImplementation()]; + + return selected; + } + + return nil; + } + + static void setAccessibilitySelectedChildren (id self, SEL, NSArray* selected) + { + if (auto* handler = getHandler (self)) + { + for (auto* childHandler : handler->getChildren()) + { + id nativeHandler = (id) childHandler->getNativeImplementation(); + const auto isSelected = [selected containsObject: nativeHandler]; + + if (isSelectable (childHandler->getRole())) + { + if (childHandler->getCurrentState().isSelected() != isSelected) + childHandler->getActions().invoke (AccessibilityActionType::toggle); + } + else if (childHandler->getCurrentState().isFocusable()) + { + [nativeHandler setAccessibilityFocused: isSelected]; + } + } + } + } + + static NSAccessibilityOrientation getAccessibilityOrientation (id self, SEL) + { + if (auto* handler = getHandler (self)) + return handler->getComponent().getBounds().toFloat().getAspectRatio() > 1.0f + ? NSAccessibilityOrientationHorizontal + : NSAccessibilityOrientationVertical; + + return NSAccessibilityOrientationUnknown; + } + + static BOOL getIsAccessibilityFocused (id self, SEL) + { + return [[self accessibilityWindow] accessibilityFocusedUIElement] == self; + } + + static void setAccessibilityFocused (id self, SEL, BOOL focused) + { + if (auto* handler = getHandler (self)) + { + if (focused) + handler->grabFocus(); + else + handler->giveAwayFocus(); + } + } + + static BOOL getIsAccessibilityModal (id self, SEL) + { + if (auto* handler = getHandler (self)) + return handler->getComponent().isCurrentlyModal(); + + return NO; + } + + static NSRect getAccessibilityFrame (id self, SEL) + { + if (auto* handler = getHandler (self)) + return flippedScreenRect (makeNSRect (handler->getComponent().getScreenBounds())); + + return NSZeroRect; + } + + static NSAccessibilityRole getAccessibilityRole (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + switch (handler->getRole()) + { + case AccessibilityRole::button: return NSAccessibilityButtonRole; + case AccessibilityRole::toggleButton: return NSAccessibilityCheckBoxRole; + case AccessibilityRole::radioButton: return NSAccessibilityRadioButtonRole; + case AccessibilityRole::comboBox: return NSAccessibilityPopUpButtonRole; + case AccessibilityRole::image: return NSAccessibilityImageRole; + case AccessibilityRole::slider: return NSAccessibilitySliderRole; + case AccessibilityRole::staticText: return NSAccessibilityStaticTextRole; + case AccessibilityRole::editableText: return NSAccessibilityTextAreaRole; + case AccessibilityRole::menuItem: return NSAccessibilityMenuItemRole; + case AccessibilityRole::menuBar: return NSAccessibilityMenuRole; + case AccessibilityRole::popupMenu: return NSAccessibilityMenuRole; + case AccessibilityRole::table: return NSAccessibilityTableRole; + case AccessibilityRole::tableHeader: return NSAccessibilityGroupRole; + case AccessibilityRole::column: return NSAccessibilityColumnRole; + case AccessibilityRole::row: return NSAccessibilityRowRole; + case AccessibilityRole::cell: return NSAccessibilityCellRole; + case AccessibilityRole::hyperlink: return NSAccessibilityLinkRole; + case AccessibilityRole::list: return NSAccessibilityListRole; + case AccessibilityRole::listItem: return NSAccessibilityRowRole; + case AccessibilityRole::tree: return NSAccessibilityListRole; + case AccessibilityRole::treeItem: return NSAccessibilityRowRole; + case AccessibilityRole::progressBar: return NSAccessibilityProgressIndicatorRole; + case AccessibilityRole::group: return NSAccessibilityGroupRole; + case AccessibilityRole::dialogWindow: return NSAccessibilityWindowRole; + case AccessibilityRole::window: return NSAccessibilityWindowRole; + case AccessibilityRole::scrollBar: return NSAccessibilityScrollBarRole; + case AccessibilityRole::tooltip: return NSAccessibilityWindowRole; + case AccessibilityRole::splashScreen: return NSAccessibilityWindowRole; + case AccessibilityRole::ignored: return NSAccessibilityUnknownRole; + case AccessibilityRole::unspecified: return NSAccessibilityGroupRole; + default: break; + } + + return NSAccessibilityUnknownRole; + } + + return nil; + } + + static NSAccessibilityRole getAccessibilitySubrole (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + if (auto* textInterface = getTextInterface (self)) + if (textInterface->isDisplayingProtectedText()) + return NSAccessibilitySecureTextFieldSubrole; + + const auto role = handler->getRole(); + + if (role == AccessibilityRole::window) return NSAccessibilityStandardWindowSubrole; + if (role == AccessibilityRole::dialogWindow) return NSAccessibilityDialogSubrole; + if (role == AccessibilityRole::tooltip + || role == AccessibilityRole::splashScreen) return NSAccessibilityFloatingWindowSubrole; + if (role == AccessibilityRole::toggleButton) return NSAccessibilityToggleSubrole; + if (role == AccessibilityRole::treeItem) return NSAccessibilityOutlineRowSubrole; + if (role == AccessibilityRole::row && getCellInterface (self) != nullptr) return NSAccessibilityTableRowSubrole; + + const auto& component = handler->getComponent(); + + if (auto* documentWindow = component.findParentComponentOfClass()) + { + if (role == AccessibilityRole::button) + { + if (&component == documentWindow->getCloseButton()) return NSAccessibilityCloseButtonSubrole; + if (&component == documentWindow->getMinimiseButton()) return NSAccessibilityMinimizeButtonSubrole; + if (&component == documentWindow->getMaximiseButton()) return NSAccessibilityFullScreenButtonSubrole; + } + } + } + + return NSAccessibilityUnknownRole; + } + + static NSString* getAccessibilityTitle (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + if (nameIsAccessibilityValue (handler->getRole())) + return @""; + + auto title = handler->getTitle(); + + if (title.isEmpty() && handler->getComponent().isOnDesktop()) + title = getAccessibleApplicationOrPluginName(); + + return juceStringToNS (title); + } + + return nil; + } + + static NSString* getAccessibilityLabel (id self, SEL) + { + if (auto* handler = getHandler (self)) + return juceStringToNS (handler->getDescription()); + + return nil; + } + + static NSString* getAccessibilityHelp (id self, SEL) + { + if (auto* handler = getHandler (self)) + return juceStringToNS (handler->getHelp()); + + return nil; + } + + static id getAccessibilityValue (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + if (nameIsAccessibilityValue (handler->getRole())) + return juceStringToNS (handler->getTitle()); + + if (hasEditableText (*handler)) + { + auto* textInterface = handler->getTextInterface(); + return juceStringToNS (textInterface->getText ({ 0, textInterface->getTotalNumCharacters() })); + } + + if (handler->getCurrentState().isCheckable()) + return handler->getCurrentState().isChecked() ? @(1) : @(0); + + if (auto* valueInterface = handler->getValueInterface()) + return juceStringToNS (valueInterface->getCurrentValueAsString()); + } + + return nil; + } + + static void setAccessibilityValue (id self, SEL, NSString* value) + { + if (auto* handler = getHandler (self)) + { + if (hasEditableText (*handler)) + { + handler->getTextInterface()->setText (nsStringToJuce (value)); + return; + } + + if (auto* valueInterface = handler->getValueInterface()) + if (! valueInterface->isReadOnly()) + valueInterface->setValueAsString (nsStringToJuce (value)); + } + } + + //============================================================================== + static NSInteger getAccessibilityInsertionPointLineNumber (id self, SEL) + { + if (auto* textInterface = getTextInterface (self)) + return [self accessibilityLineForIndex: textInterface->getTextInsertionOffset()]; + + return 0; + } + + static NSRange getAccessibilitySharedCharacterRange (id self, SEL) + { + return [self accessibilityVisibleCharacterRange]; + } + + static NSArray* getAccessibilitySharedTextUIElements (id self, SEL) + { + return [NSArray arrayWithObject: self]; + } + + static NSRange getAccessibilityVisibleCharacterRange (id self, SEL) + { + if (auto* textInterface = getTextInterface (self)) + return juceRangeToNS ({ 0, textInterface->getTotalNumCharacters() }); + + return NSMakeRange (0, 0); + } + + static NSInteger getAccessibilityNumberOfCharacters (id self, SEL) + { + if (auto* textInterface = getTextInterface (self)) + return textInterface->getTotalNumCharacters(); + + return 0; + } + + static NSString* getAccessibilitySelectedText (id self, SEL) + { + if (auto* textInterface = getTextInterface (self)) + return juceStringToNS (textInterface->getText (textInterface->getSelection())); + + return nil; + } + + static NSRange getAccessibilitySelectedTextRange (id self, SEL) + { + if (auto* textInterface = getTextInterface (self)) + return juceRangeToNS (textInterface->getSelection()); + + return NSMakeRange (0, 0); + } + + static NSArray* getAccessibilitySelectedTextRanges (id self, SEL) + { + return [NSArray arrayWithObject: [NSValue valueWithRange: [self accessibilitySelectedTextRange]]]; + } + + static NSAttributedString* getAccessibilityAttributedStringForRange (id self, SEL, NSRange range) + { + NSString* string = [self accessibilityStringForRange: range]; + + if (string != nil) + return [[[NSAttributedString alloc] initWithString: string] autorelease]; + + return nil; + } + + static NSRange getAccessibilityRangeForLine (id self, SEL, NSInteger line) + { + if (auto* textInterface = getTextInterface (self)) + { + auto text = textInterface->getText ({ 0, textInterface->getTotalNumCharacters() }); + auto lines = StringArray::fromLines (text); + + if (line < lines.size()) + { + auto lineText = lines[(int) line]; + auto start = text.indexOf (lineText); + + if (start >= 0) + return NSMakeRange ((NSUInteger) start, (NSUInteger) lineText.length()); + } + } + + return NSMakeRange (0, 0); + } + + static NSString* getAccessibilityStringForRange (id self, SEL, NSRange range) + { + if (auto* textInterface = getTextInterface (self)) + return juceStringToNS (textInterface->getText (nsRangeToJuce (range))); + + return nil; + } + + static NSRange getAccessibilityRangeForPosition (id self, SEL, NSPoint position) + { + if (auto* handler = getHandler (self)) + { + if (auto* textInterface = handler->getTextInterface()) + { + auto screenPoint = convertToIntPoint (flippedScreenPoint (position)); + + if (handler->getComponent().getScreenBounds().contains (screenPoint)) + { + auto offset = textInterface->getOffsetAtPoint (screenPoint); + + if (offset >= 0) + return NSMakeRange ((NSUInteger) offset, 1); + } + } + } + + return NSMakeRange (0, 0); + } + + static NSRange getAccessibilityRangeForIndex (id self, SEL, NSInteger index) + { + if (auto* textInterface = getTextInterface (self)) + if (isPositiveAndBelow (index, textInterface->getTotalNumCharacters())) + return NSMakeRange ((NSUInteger) index, 1); + + return NSMakeRange (0, 0); + } + + static NSRect getAccessibilityFrameForRange (id self, SEL, NSRange range) + { + if (auto* textInterface = getTextInterface (self)) + return flippedScreenRect (makeNSRect (textInterface->getTextBounds (nsRangeToJuce (range)).getBounds())); + + return NSZeroRect; + } + + static NSData* getAccessibilityRTFForRange (id, SEL, NSRange) + { + return nil; + } + + static NSRange getAccessibilityStyleRangeForIndex (id self, SEL, NSInteger) + { + return [self accessibilityVisibleCharacterRange]; + } + + static NSInteger getAccessibilityLineForIndex (id self, SEL, NSInteger index) + { + if (auto* textInterface = getTextInterface (self)) + { + auto text = textInterface->getText ({ 0, (int) index }); + + if (! text.isEmpty()) + return StringArray::fromLines (text).size() - 1; + } + + return 0; + } + + static void setAccessibilitySelectedTextRange (id self, SEL, NSRange selectedRange) + { + if (auto* textInterface = getTextInterface (self)) + { + textInterface->setSelection ({}); + textInterface->setSelection (nsRangeToJuce (selectedRange)); + } + } + + //============================================================================== + static NSInteger getAccessibilityRowCount (id self, SEL) + { + if (auto* tableInterface = getTableInterface (self)) + return tableInterface->getNumRows(); + + return 0; + } + + static NSArray* getAccessibilityRows (id self, SEL) + { + NSMutableArray* rows = [[NSMutableArray new] autorelease]; + + if (auto* tableInterface = getTableInterface (self)) + for (int row = 0; row < tableInterface->getNumRows(); ++row) + if (auto* handler = tableInterface->getCellHandler (row, 0)) + [rows addObject: (id) handler->getNativeImplementation()]; + + return rows; + } + + static NSInteger getAccessibilityColumnCount (id self, SEL) + { + if (auto* tableInterface = getTableInterface (self)) + return tableInterface->getNumColumns(); + + return 0; + } + + static NSArray* getAccessibilityColumns (id self, SEL) + { + NSMutableArray* columns = [[NSMutableArray new] autorelease]; + + if (auto* tableInterface = getTableInterface (self)) + for (int column = 0; column < tableInterface->getNumColumns(); ++column) + if (auto* handler = tableInterface->getCellHandler (0, column)) + [columns addObject: (id) handler->getNativeImplementation()]; + + return columns; + } + + //============================================================================== + static NSRange getAccessibilityRowIndexRange (id self, SEL) + { + if (auto* cellInterface = getCellInterface (self)) + return NSMakeRange ((NSUInteger) cellInterface->getRowIndex(), + (NSUInteger) cellInterface->getRowSpan()); + + return NSMakeRange (0, 0); + } + + static NSRange getAccessibilityColumnIndexRange (id self, SEL) + { + if (auto* cellInterface = getCellInterface (self)) + return NSMakeRange ((NSUInteger) cellInterface->getColumnIndex(), + (NSUInteger) cellInterface->getColumnSpan()); + + return NSMakeRange (0, 0); + } + + static NSInteger getAccessibilityIndex (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + if (auto* cellInterface = handler->getCellInterface()) + { + NSAccessibilityRole role = [self accessibilityRole]; + + if ([role isEqual: NSAccessibilityRowRole]) + return cellInterface->getRowIndex(); + + if ([role isEqual: NSAccessibilityColumnRole]) + return cellInterface->getColumnIndex(); + } + } + + return 0; + } + + static NSInteger getAccessibilityDisclosureLevel (id self, SEL) + { + if (auto* handler = getHandler (self)) + if (auto* cellInterface = handler->getCellInterface()) + return cellInterface->getDisclosureLevel(); + + return 0; + } + + //============================================================================== + static BOOL accessibilityPerformPress (id self, SEL) { return performActionIfSupported (self, AccessibilityActionType::press); } + static BOOL accessibilityPerformShowMenu (id self, SEL) { return performActionIfSupported (self, AccessibilityActionType::showMenu); } + static BOOL accessibilityPerformPick (id self, SEL) { return [self accessibilityPerformPress]; } + + static BOOL accessibilityPerformRaise (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + if (handler->getCurrentState().isFocusable()) + { + handler->grabFocus(); + return YES; + } + } + + return NO; + } + + static BOOL accessibilityPerformIncrement (id self, SEL) + { + if (auto* valueInterface = getValueInterface (self)) + { + if (! valueInterface->isReadOnly()) + { + auto range = valueInterface->getRange(); + + if (range.isValid()) + { + valueInterface->setValue (jlimit (range.getMinimumValue(), + range.getMaximumValue(), + valueInterface->getCurrentValue() + range.getInterval())); + return YES; + } + } + } + + return NO; + } + + static BOOL accessibilityPerformDecrement (id self, SEL) + { + if (auto* valueInterface = getValueInterface (self)) + { + if (! valueInterface->isReadOnly()) + { + auto range = valueInterface->getRange(); + + if (range.isValid()) + { + valueInterface->setValue (jlimit (range.getMinimumValue(), + range.getMaximumValue(), + valueInterface->getCurrentValue() - range.getInterval())); + return YES; + } + } + } + + return NO; + } + + static BOOL accessibilityPerformDelete (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + if (hasEditableText (*handler)) + { + handler->getTextInterface()->setText ({}); + return YES; + } + + if (auto* valueInterface = handler->getValueInterface()) + { + if (! valueInterface->isReadOnly()) + { + valueInterface->setValue ({}); + return YES; + } + } + } + + return NO; + } + + //============================================================================== + static BOOL getIsAccessibilitySelectorAllowed (id self, SEL, SEL selector) + { + if (auto* handler = getHandler (self)) + { + for (auto textSelector : { @selector (accessibilityInsertionPointLineNumber), + @selector (accessibilitySharedCharacterRange), + @selector (accessibilitySharedTextUIElements), + @selector (accessibilityVisibleCharacterRange), + @selector (accessibilityNumberOfCharacters), + @selector (accessibilitySelectedText), + @selector (accessibilitySelectedTextRange), + @selector (accessibilitySelectedTextRanges), + @selector (accessibilityAttributedStringForRange:), + @selector (accessibilityRangeForLine:), + @selector (accessibilityStringForRange:), + @selector (accessibilityRangeForPosition:), + @selector (accessibilityRangeForIndex:), + @selector (accessibilityFrameForRange:), + @selector (accessibilityRTFForRange:), + @selector (accessibilityStyleRangeForIndex:), + @selector (accessibilityLineForIndex:), + @selector (setAccessibilitySelectedTextRange:) }) + { + if (selector == textSelector) + return handler->getTextInterface() != nullptr; + } + + for (auto tableSelector : { @selector (accessibilityRowCount), + @selector (accessibilityRows), + @selector (accessibilityColumnCount), + @selector (accessibilityColumns) }) + { + if (selector == tableSelector) + return handler->getTableInterface() != nullptr; + } + + for (auto cellSelector : { @selector (accessibilityRowIndexRange), + @selector (accessibilityColumnIndexRange), + @selector (accessibilityIndex), + @selector (accessibilityDisclosureLevel) }) + { + if (selector == cellSelector) + return handler->getCellInterface() != nullptr; + } + + for (auto valueSelector : { @selector (accessibilityValue), + @selector (setAccessibilityValue:), + @selector (accessibilityPerformDelete), + @selector (accessibilityPerformIncrement), + @selector (accessibilityPerformDecrement) }) + { + if (selector != valueSelector) + continue; + + auto* valueInterface = handler->getValueInterface(); + + if (selector == @selector (accessibilityValue)) + return valueInterface != nullptr + || hasEditableText (*handler) + || nameIsAccessibilityValue (handler->getRole()) + || handler->getCurrentState().isCheckable(); + + auto hasEditableValue = [valueInterface] { return valueInterface != nullptr && ! valueInterface->isReadOnly(); }; + + if (selector == @selector (setAccessibilityValue:) + || selector == @selector (accessibilityPerformDelete)) + return hasEditableValue() || hasEditableText (*handler); + + auto isRanged = [valueInterface] { return valueInterface != nullptr && valueInterface->getRange().isValid(); }; + + if (selector == @selector (accessibilityPerformIncrement) + || selector == @selector (accessibilityPerformDecrement)) + return hasEditableValue() && isRanged(); + + return NO; + } + + for (auto actionSelector : { @selector (accessibilityPerformPick), + @selector (accessibilityPerformPress), + @selector (accessibilityPerformRaise), + @selector (accessibilityPerformShowMenu), + @selector (setAccessibilityFocused:) }) + { + if (selector != actionSelector) + continue; + + if (selector == @selector (accessibilityPerformPick) + || selector == @selector (accessibilityPerformPress)) + return handler->getActions().contains (AccessibilityActionType::press); + + if (selector == @selector (accessibilityPerformRaise) + || selector == @selector (setAccessibilityFocused:)) + return handler->getCurrentState().isFocusable(); + + if (selector == @selector (accessibilityPerformShowMenu)) + return handler->getActions().contains (AccessibilityActionType::showMenu); + } + + if (selector == @selector (accessibilitySelectedChildren)) + return hasSelection (handler->getRole()); + + if (selector == @selector (accessibilityOrientation)) + return handler->getRole() == AccessibilityRole::scrollBar; + + return sendSuperclassMessage (self, @selector (isAccessibilitySelectorAllowed:), selector); + } + + return NO; + } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityElement) + }; + + //============================================================================== + AccessibilityElement::Holder accessibilityElement; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityNativeImpl) +}; + +//============================================================================== +AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const +{ + return (AccessibilityNativeHandle*) nativeImpl->getAccessibilityElement(); +} + +static void sendAccessibilityEvent (id accessibilityElement, + NSAccessibilityNotificationName notification, + NSDictionary* userInfo) +{ + jassert (notification != NSAccessibilityNotificationName{}); + + NSAccessibilityPostNotificationWithUserInfo (accessibilityElement, notification, userInfo); +} + +void notifyAccessibilityEventInternal (const AccessibilityHandler& handler, InternalAccessibilityEvent eventType) +{ + auto notification = [eventType] + { + switch (eventType) + { + case InternalAccessibilityEvent::elementCreated: return NSAccessibilityCreatedNotification; + case InternalAccessibilityEvent::elementDestroyed: return NSAccessibilityUIElementDestroyedNotification; + case InternalAccessibilityEvent::focusChanged: return NSAccessibilityFocusedUIElementChangedNotification; + case InternalAccessibilityEvent::windowOpened: return NSAccessibilityWindowCreatedNotification; + case InternalAccessibilityEvent::windowClosed: break; + } + + return NSAccessibilityNotificationName{}; + }(); + + if (notification != NSAccessibilityNotificationName{}) + sendAccessibilityEvent ((id) handler.getNativeImplementation(), notification, nil); +} + +void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent eventType) const +{ + auto notification = [eventType] + { + switch (eventType) + { + case AccessibilityEvent::textSelectionChanged: return NSAccessibilitySelectedTextChangedNotification; + case AccessibilityEvent::rowSelectionChanged: return NSAccessibilitySelectedRowsChangedNotification; + + case AccessibilityEvent::textChanged: + case AccessibilityEvent::valueChanged: return NSAccessibilityValueChangedNotification; + case AccessibilityEvent::structureChanged: return NSAccessibilityLayoutChangedNotification; + } + + return NSAccessibilityNotificationName{}; + }(); + + if (notification != NSAccessibilityNotificationName{}) + { + id accessibilityElement = (id) getNativeImplementation(); + + sendAccessibilityEvent (accessibilityElement, notification, + (notification == NSAccessibilityLayoutChangedNotification + ? @{ NSAccessibilityUIElementsKey: accessibilityElement } + : nil)); + } +} + +void AccessibilityHandler::postAnnouncement (const String& announcementString, AnnouncementPriority priority) +{ + auto nsPriority = [priority] + { + switch (priority) + { + case AnnouncementPriority::low: return NSAccessibilityPriorityLow; + case AnnouncementPriority::medium: return NSAccessibilityPriorityMedium; + case AnnouncementPriority::high: return NSAccessibilityPriorityHigh; + } + + jassertfalse; + return NSAccessibilityPriorityLow; + }(); + + sendAccessibilityEvent ((id) [NSApp mainWindow], + NSAccessibilityAnnouncementRequestedNotification, + @{ NSAccessibilityAnnouncementKey: juceStringToNS (announcementString), + NSAccessibilityPriorityKey: @(nsPriority) }); +} + +AccessibilityHandler::AccessibilityNativeImpl* AccessibilityHandler::createNativeImpl (AccessibilityHandler& handler) +{ + return new AccessibilityHandler::AccessibilityNativeImpl (handler); +} + +void AccessibilityHandler::DestroyNativeImpl::operator() (AccessibilityHandler::AccessibilityNativeImpl* impl) const noexcept +{ + delete impl; +} + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_Accessibility.cpp b/modules/juce_gui_basics/native/accessibility/juce_win32_Accessibility.cpp new file mode 100644 index 0000000000..223144dd97 --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_Accessibility.cpp @@ -0,0 +1,270 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +static bool isStartingUpOrShuttingDown() +{ + if (auto* app = JUCEApplicationBase::getInstance()) + if (app->isInitialising()) + return true; + + if (auto* mm = MessageManager::getInstanceWithoutCreating()) + if (mm->hasStopMessageBeenSent()) + return true; + + return false; +} + +static bool isHandlerValid (const AccessibilityHandler& handler) +{ + if (auto* provider = handler.getNativeImplementation()) + return provider->isElementValid(); + + return false; +} + +//============================================================================== +class AccessibilityHandler::AccessibilityNativeImpl +{ +public: + explicit AccessibilityNativeImpl (AccessibilityHandler& owner) + : accessibilityElement (new AccessibilityNativeHandle (owner)) + { + ++providerCount; + } + + ~AccessibilityNativeImpl() + { + accessibilityElement->invalidateElement(); + + if (auto* wrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) + { + ComSmartPtr provider; + accessibilityElement->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress())); + + wrapper->disconnectProvider (provider); + + if (--providerCount == 0) + wrapper->disconnectAllProviders(); + } + } + + //============================================================================== + ComSmartPtr accessibilityElement; + static int providerCount; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityNativeImpl) +}; + +int AccessibilityHandler::AccessibilityNativeImpl::providerCount = 0; + +//============================================================================== +AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const +{ + return nativeImpl->accessibilityElement; +} + +template +void getProviderWithCheckedWrapper (const AccessibilityHandler& handler, Callback&& callback) +{ + if (isStartingUpOrShuttingDown() || ! isHandlerValid (handler)) + return; + + if (auto* wrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) + { + if (! wrapper->clientsAreListening()) + return; + + ComSmartPtr provider; + handler.getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress())); + + callback (wrapper, provider); + } +} + +void sendAccessibilityAutomationEvent (const AccessibilityHandler& handler, EVENTID event) +{ + jassert (event != EVENTID{}); + + getProviderWithCheckedWrapper (handler, [event] (WindowsUIAWrapper* wrapper, ComSmartPtr& provider) + { + wrapper->raiseAutomationEvent (provider, event); + }); +} + +void sendAccessibilityPropertyChangedEvent (const AccessibilityHandler& handler, PROPERTYID property, VARIANT newValue) +{ + jassert (property != PROPERTYID{}); + + getProviderWithCheckedWrapper (handler, [property, newValue] (WindowsUIAWrapper* wrapper, ComSmartPtr& provider) + { + VARIANT oldValue; + VariantHelpers::clear (&oldValue); + + wrapper->raiseAutomationPropertyChangedEvent (provider, property, oldValue, newValue); + }); +} + +void notifyAccessibilityEventInternal (const AccessibilityHandler& handler, InternalAccessibilityEvent eventType) +{ + auto event = [eventType]() -> EVENTID + { + switch (eventType) + { + case InternalAccessibilityEvent::elementCreated: + case InternalAccessibilityEvent::elementDestroyed: return UIA_StructureChangedEventId; + case InternalAccessibilityEvent::focusChanged: return UIA_AutomationFocusChangedEventId; + case InternalAccessibilityEvent::windowOpened: return UIA_Window_WindowOpenedEventId; + case InternalAccessibilityEvent::windowClosed: return UIA_Window_WindowClosedEventId; + } + + return {}; + }(); + + if (event != EVENTID{}) + sendAccessibilityAutomationEvent (handler, event); +} + +void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent eventType) const +{ + auto event = [eventType] () -> EVENTID + { + switch (eventType) + { + case AccessibilityEvent::textSelectionChanged: return UIA_Text_TextSelectionChangedEventId; + case AccessibilityEvent::textChanged: return UIA_Text_TextChangedEventId; + case AccessibilityEvent::structureChanged: return UIA_StructureChangedEventId; + case AccessibilityEvent::rowSelectionChanged: return UIA_SelectionItem_ElementSelectedEventId; + case AccessibilityEvent::valueChanged: break; + } + + return {}; + }(); + + if (event != EVENTID{}) + sendAccessibilityAutomationEvent (*this, event); +} + +struct SpVoiceWrapper : public DeletedAtShutdown +{ + SpVoiceWrapper() + { + auto hr = voice.CoCreateInstance (CLSID_SpVoice); + + jassert (SUCCEEDED (hr)); + ignoreUnused (hr); + } + + ~SpVoiceWrapper() override + { + clearSingletonInstance(); + } + + ComSmartPtr voice; + + JUCE_DECLARE_SINGLETON (SpVoiceWrapper, false) +}; + +JUCE_IMPLEMENT_SINGLETON (SpVoiceWrapper) + + +void AccessibilityHandler::postAnnouncement (const String& announcementString, AnnouncementPriority priority) +{ + if (auto* sharedVoice = SpVoiceWrapper::getInstance()) + { + auto voicePriority = [priority] + { + switch (priority) + { + case AnnouncementPriority::low: return SPVPRI_OVER; + case AnnouncementPriority::medium: return SPVPRI_NORMAL; + case AnnouncementPriority::high: return SPVPRI_ALERT; + } + + jassertfalse; + return SPVPRI_OVER; + }(); + + sharedVoice->voice->SetPriority (voicePriority); + sharedVoice->voice->Speak (announcementString.toWideCharPointer(), SPF_ASYNC, nullptr); + } +} + +AccessibilityHandler::AccessibilityNativeImpl* AccessibilityHandler::createNativeImpl (AccessibilityHandler& handler) +{ + return new AccessibilityHandler::AccessibilityNativeImpl (handler); +} + +void AccessibilityHandler::DestroyNativeImpl::operator() (AccessibilityHandler::AccessibilityNativeImpl* impl) const noexcept +{ + delete impl; +} + +//============================================================================== +namespace WindowsAccessibility +{ + void initialiseUIAWrapper() + { + WindowsUIAWrapper::getInstance(); + } + + long getUiaRootObjectId() + { + return static_cast (UiaRootObjectId); + } + + bool handleWmGetObject (AccessibilityHandler* handler, WPARAM wParam, LPARAM lParam, LRESULT* res) + { + if (isStartingUpOrShuttingDown() || (handler == nullptr || ! isHandlerValid (*handler))) + return false; + + if (auto* wrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) + { + ComSmartPtr provider; + handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress())); + + if (! wrapper->isProviderDisconnecting (provider)) + *res = wrapper->returnRawElementProvider ((HWND) handler->getComponent().getWindowHandle(), wParam, lParam, provider); + + return true; + } + + return false; + } + + void revokeUIAMapEntriesForWindow (HWND hwnd) + { + if (auto* wrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) + wrapper->returnRawElementProvider (hwnd, 0, 0, nullptr); + } +} + + +JUCE_IMPLEMENT_SINGLETON (WindowsUIAWrapper) + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_AccessibilityElement.cpp b/modules/juce_gui_basics/native/accessibility/juce_win32_AccessibilityElement.cpp new file mode 100644 index 0000000000..8ec88912af --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_AccessibilityElement.cpp @@ -0,0 +1,558 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +int AccessibilityNativeHandle::idCounter = 0; + +//============================================================================== +static String getAutomationId (const AccessibilityHandler& handler) +{ + auto result = handler.getTitle(); + auto* parentComponent = handler.getComponent().getParentComponent(); + + while (parentComponent != nullptr) + { + if (auto* parentHandler = parentComponent->getAccessibilityHandler()) + { + auto parentTitle = parentHandler->getTitle(); + result << "." << (parentTitle.isNotEmpty() ? parentTitle : ""); + } + + parentComponent = parentComponent->getParentComponent(); + } + + return result; +} + +static long roleToControlTypeId (AccessibilityRole roleType) +{ + switch (roleType) + { + case AccessibilityRole::button: return UIA_ButtonControlTypeId; + case AccessibilityRole::toggleButton: return UIA_CheckBoxControlTypeId; + case AccessibilityRole::radioButton: return UIA_RadioButtonControlTypeId; + case AccessibilityRole::comboBox: return UIA_ComboBoxControlTypeId; + case AccessibilityRole::image: return UIA_ImageControlTypeId; + case AccessibilityRole::slider: return UIA_SliderControlTypeId; + case AccessibilityRole::staticText: return UIA_TextControlTypeId; + case AccessibilityRole::editableText: return UIA_EditControlTypeId; + case AccessibilityRole::menuItem: return UIA_MenuItemControlTypeId; + case AccessibilityRole::menuBar: return UIA_MenuBarControlTypeId; + case AccessibilityRole::popupMenu: return UIA_WindowControlTypeId; + case AccessibilityRole::table: return UIA_TableControlTypeId; + case AccessibilityRole::tableHeader: return UIA_HeaderControlTypeId; + case AccessibilityRole::column: return UIA_HeaderItemControlTypeId; + case AccessibilityRole::row: return UIA_HeaderItemControlTypeId; + case AccessibilityRole::cell: return UIA_DataItemControlTypeId; + case AccessibilityRole::hyperlink: return UIA_HyperlinkControlTypeId; + case AccessibilityRole::list: return UIA_ListControlTypeId; + case AccessibilityRole::listItem: return UIA_ListItemControlTypeId; + case AccessibilityRole::tree: return UIA_TreeControlTypeId; + case AccessibilityRole::treeItem: return UIA_TreeItemControlTypeId; + case AccessibilityRole::progressBar: return UIA_ProgressBarControlTypeId; + case AccessibilityRole::group: return UIA_GroupControlTypeId; + case AccessibilityRole::dialogWindow: return UIA_WindowControlTypeId; + case AccessibilityRole::window: return UIA_WindowControlTypeId; + case AccessibilityRole::scrollBar: return UIA_ScrollBarControlTypeId; + case AccessibilityRole::tooltip: return UIA_ToolTipControlTypeId; + case AccessibilityRole::splashScreen: return UIA_WindowControlTypeId; + case AccessibilityRole::ignored: return UIA_CustomControlTypeId; + case AccessibilityRole::unspecified: return UIA_CustomControlTypeId; + }; + + return UIA_CustomControlTypeId; +} + +static bool isEditableText (const AccessibilityHandler& handler) +{ + return handler.getRole() == AccessibilityRole::editableText + && handler.getTextInterface() != nullptr; +} + +//============================================================================== +AccessibilityNativeHandle::AccessibilityNativeHandle (AccessibilityHandler& handler) + : ComBaseClassHelper (0), + accessibilityHandler (handler) +{ +} + +//============================================================================== +JUCE_COMRESULT AccessibilityNativeHandle::QueryInterface (REFIID refId, void** result) +{ + *result = nullptr; + + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + if ((refId == __uuidof (IRawElementProviderFragmentRoot) && ! isFragmentRoot())) + return E_NOINTERFACE; + + return ComBaseClassHelper::QueryInterface (refId, result); +} + +//============================================================================== +JUCE_COMRESULT AccessibilityNativeHandle::get_HostRawElementProvider (IRawElementProviderSimple** pRetVal) +{ + return withCheckedComArgs (pRetVal, *this, [&] + { + if (isFragmentRoot()) + if (auto* wrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) + return wrapper->hostProviderFromHwnd ((HWND) accessibilityHandler.getComponent().getWindowHandle(), pRetVal); + + return S_OK; + }); +} + +JUCE_COMRESULT AccessibilityNativeHandle::get_ProviderOptions (ProviderOptions* options) +{ + if (options == nullptr) + return E_INVALIDARG; + + *options = ProviderOptions_ServerSideProvider | ProviderOptions_UseComThreading; + return S_OK; +} + +JUCE_COMRESULT AccessibilityNativeHandle::GetPatternProvider (PATTERNID pId, IUnknown** pRetVal) +{ + return withCheckedComArgs (pRetVal, *this, [&] + { + *pRetVal = [&]() -> IUnknown* + { + const auto role = accessibilityHandler.getRole(); + const auto fragmentRoot = isFragmentRoot(); + + switch (pId) + { + case UIA_WindowPatternId: + { + if (fragmentRoot) + return new UIAWindowProvider (this); + + break; + } + case UIA_TransformPatternId: + { + if (fragmentRoot) + return new UIATransformProvider (this); + + break; + } + case UIA_TextPatternId: + case UIA_TextPattern2Id: + { + if (accessibilityHandler.getTextInterface() != nullptr) + return new UIATextProvider (this); + + break; + } + case UIA_ValuePatternId: + { + auto editableText = isEditableText (accessibilityHandler); + + if (accessibilityHandler.getValueInterface() != nullptr || editableText) + return new UIAValueProvider (this, editableText); + + break; + } + case UIA_RangeValuePatternId: + { + if (accessibilityHandler.getValueInterface() != nullptr + && accessibilityHandler.getValueInterface()->getRange().isValid()) + { + return new UIARangeValueProvider (this); + } + + break; + } + case UIA_TogglePatternId: + { + if (accessibilityHandler.getActions().contains (AccessibilityActionType::toggle) + && accessibilityHandler.getCurrentState().isCheckable()) + { + return new UIAToggleProvider (this); + } + + break; + } + case UIA_SelectionPatternId: + { + if (role == AccessibilityRole::list + || role == AccessibilityRole::popupMenu + || role == AccessibilityRole::tree) + { + return new UIASelectionProvider (this); + } + + break; + } + case UIA_SelectionItemPatternId: + { + auto state = accessibilityHandler.getCurrentState(); + + if (state.isSelectable() || state.isMultiSelectable() + || role == AccessibilityRole::radioButton) + { + return new UIASelectionItemProvider (this); + } + + break; + } + case UIA_GridPatternId: + { + if ((role == AccessibilityRole::table || role == AccessibilityRole::tree) + && accessibilityHandler.getTableInterface() != nullptr) + { + return new UIAGridProvider (this); + } + + break; + } + case UIA_GridItemPatternId: + { + if ((role == AccessibilityRole::cell || role == AccessibilityRole::treeItem) + && accessibilityHandler.getCellInterface() != nullptr) + { + return new UIAGridItemProvider (this); + } + + break; + } + case UIA_InvokePatternId: + { + if (accessibilityHandler.getActions().contains (AccessibilityActionType::press)) + return new UIAInvokeProvider (this); + + break; + } + case UIA_ExpandCollapsePatternId: + { + if (role == AccessibilityRole::menuItem + && accessibilityHandler.getActions().contains (AccessibilityActionType::showMenu)) + { + return new UIAExpandCollapseProvider (this); + } + + break; + } + default: + break; + } + + return nullptr; + }(); + + return S_OK; + }); +} + +JUCE_COMRESULT AccessibilityNativeHandle::GetPropertyValue (PROPERTYID propertyId, VARIANT* pRetVal) +{ + return withCheckedComArgs (pRetVal, *this, [&] + { + VariantHelpers::clear (pRetVal); + + const auto fragmentRoot = isFragmentRoot(); + + switch (propertyId) + { + case UIA_AutomationIdPropertyId: + VariantHelpers::setString (getAutomationId (accessibilityHandler), pRetVal); + break; + case UIA_ControlTypePropertyId: + VariantHelpers::setInt (roleToControlTypeId (accessibilityHandler.getRole()), + pRetVal); + break; + case UIA_FrameworkIdPropertyId: + VariantHelpers::setString ("JUCE", pRetVal); + break; + case UIA_FullDescriptionPropertyId: + VariantHelpers::setString (accessibilityHandler.getDescription(), pRetVal); + break; + case UIA_HelpTextPropertyId: + VariantHelpers::setString (accessibilityHandler.getHelp(), pRetVal); + break; + case UIA_IsContentElementPropertyId: + VariantHelpers::setBool (! accessibilityHandler.isIgnored(), pRetVal); + break; + case UIA_IsControlElementPropertyId: + VariantHelpers::setBool (true, pRetVal); + break; + case UIA_IsDialogPropertyId: + VariantHelpers::setBool (accessibilityHandler.getRole() == AccessibilityRole::dialogWindow, pRetVal); + break; + case UIA_IsEnabledPropertyId: + VariantHelpers::setBool (accessibilityHandler.getComponent().isEnabled(), pRetVal); + break; + case UIA_IsKeyboardFocusablePropertyId: + VariantHelpers::setBool (accessibilityHandler.getCurrentState().isFocusable(), pRetVal); + break; + case UIA_HasKeyboardFocusPropertyId: + VariantHelpers::setBool (accessibilityHandler.hasFocus (true), pRetVal); + break; + case UIA_IsOffscreenPropertyId: + VariantHelpers::setBool (false, pRetVal); + break; + case UIA_IsPasswordPropertyId: + if (auto* textInterface = accessibilityHandler.getTextInterface()) + VariantHelpers::setBool (textInterface->isDisplayingProtectedText(), pRetVal); + + break; + case UIA_IsPeripheralPropertyId: + VariantHelpers::setBool (accessibilityHandler.getRole() == AccessibilityRole::tooltip + || accessibilityHandler.getRole() == AccessibilityRole::popupMenu + || accessibilityHandler.getRole() == AccessibilityRole::splashScreen, + pRetVal); + break; + case UIA_NamePropertyId: + VariantHelpers::setString (getElementName(), pRetVal); + break; + case UIA_ProcessIdPropertyId: + VariantHelpers::setInt ((int) GetCurrentProcessId(), pRetVal); + break; + case UIA_NativeWindowHandlePropertyId: + if (fragmentRoot) + VariantHelpers::setInt ((int) (pointer_sized_int) accessibilityHandler.getComponent().getWindowHandle(), pRetVal); + + break; + + default: + break; + } + + return S_OK; + }); +} + +//============================================================================== +JUCE_COMRESULT AccessibilityNativeHandle::Navigate (NavigateDirection direction, IRawElementProviderFragment** pRetVal) +{ + return withCheckedComArgs (pRetVal, *this, [&] + { + auto* handler = [&]() -> AccessibilityHandler* + { + if (direction == NavigateDirection_Parent) + return accessibilityHandler.getParent(); + + if (direction == NavigateDirection_FirstChild + || direction == NavigateDirection_LastChild) + { + auto children = accessibilityHandler.getChildren(); + + return children.empty() ? nullptr + : (direction == NavigateDirection_FirstChild ? children.front() + : children.back()); + } + + if (direction == NavigateDirection_NextSibling + || direction == NavigateDirection_PreviousSibling) + { + if (auto* parent = accessibilityHandler.getParent()) + { + const auto siblings = parent->getChildren(); + const auto iter = std::find (siblings.cbegin(), siblings.cend(), &accessibilityHandler); + + if (iter == siblings.end()) + return nullptr; + + if (direction == NavigateDirection_NextSibling && iter != std::prev (siblings.cend())) + return *std::next (iter); + + if (direction == NavigateDirection_PreviousSibling && iter != siblings.cbegin()) + return *std::prev (iter); + } + } + + return nullptr; + }(); + + if (handler != nullptr) + if (auto* provider = handler->getNativeImplementation()) + if (provider->isElementValid()) + provider->QueryInterface (IID_PPV_ARGS (pRetVal)); + + return S_OK; + }); +} + +JUCE_COMRESULT AccessibilityNativeHandle::GetRuntimeId (SAFEARRAY** pRetVal) +{ + return withCheckedComArgs (pRetVal, *this, [&] + { + if (! isFragmentRoot()) + { + *pRetVal = SafeArrayCreateVector (VT_I4, 0, 2); + + if (*pRetVal == nullptr) + return E_OUTOFMEMORY; + + for (LONG i = 0; i < 2; ++i) + { + auto hr = SafeArrayPutElement (*pRetVal, &i, &rtid[i]); + + if (FAILED (hr)) + return E_FAIL; + } + } + + return S_OK; + }); +} + +JUCE_COMRESULT AccessibilityNativeHandle::get_BoundingRectangle (UiaRect* pRetVal) +{ + return withCheckedComArgs (pRetVal, *this, [&] + { + auto bounds = Desktop::getInstance().getDisplays() + .logicalToPhysical (accessibilityHandler.getComponent().getScreenBounds()); + + pRetVal->left = bounds.getX(); + pRetVal->top = bounds.getY(); + pRetVal->width = bounds.getWidth(); + pRetVal->height = bounds.getHeight(); + + return S_OK; + }); +} + +JUCE_COMRESULT AccessibilityNativeHandle::GetEmbeddedFragmentRoots (SAFEARRAY** pRetVal) +{ + return withCheckedComArgs (pRetVal, *this, [] + { + return S_OK; + }); +} + +JUCE_COMRESULT AccessibilityNativeHandle::SetFocus() +{ + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + accessibilityHandler.grabFocus(); + + return S_OK; +} + +JUCE_COMRESULT AccessibilityNativeHandle::get_FragmentRoot (IRawElementProviderFragmentRoot** pRetVal) +{ + return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT + { + auto* handler = [&]() -> AccessibilityHandler* + { + if (isFragmentRoot()) + return &accessibilityHandler; + + if (auto* peer = accessibilityHandler.getComponent().getPeer()) + if (auto* handler = peer->getComponent().getAccessibilityHandler()) + return handler; + + return nullptr; + }(); + + if (handler != nullptr) + { + handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); + return S_OK; + } + + return UIA_E_ELEMENTNOTAVAILABLE; + }); +} + +//============================================================================== +JUCE_COMRESULT AccessibilityNativeHandle::ElementProviderFromPoint (double x, double y, IRawElementProviderFragment** pRetVal) +{ + return withCheckedComArgs (pRetVal, *this, [&] + { + auto* handler = [&] + { + auto logicalScreenPoint = Desktop::getInstance().getDisplays() + .physicalToLogical (Point (roundToInt (x), + roundToInt (y))); + + if (auto* child = accessibilityHandler.getChildAt (logicalScreenPoint)) + return child; + + return &accessibilityHandler; + }(); + + handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); + + return S_OK; + }); +} + +JUCE_COMRESULT AccessibilityNativeHandle::GetFocus (IRawElementProviderFragment** pRetVal) +{ + return withCheckedComArgs (pRetVal, *this, [&] + { + const auto getFocusHandler = [this]() -> AccessibilityHandler* + { + if (auto* modal = Component::getCurrentlyModalComponent()) + { + const auto& component = accessibilityHandler.getComponent(); + + if (! component.isParentOf (modal) + && component.isCurrentlyBlockedByAnotherModalComponent()) + { + if (auto* modalHandler = modal->getAccessibilityHandler()) + { + if (auto* focusChild = modalHandler->getChildFocus()) + return focusChild; + + return modalHandler; + } + } + } + + if (auto* focusChild = accessibilityHandler.getChildFocus()) + return focusChild; + + return nullptr; + }; + + if (auto* focusHandler = getFocusHandler()) + focusHandler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); + + return S_OK; + }); +} + +//============================================================================== +String AccessibilityNativeHandle::getElementName() const +{ + if (accessibilityHandler.getRole() == AccessibilityRole::tooltip) + return accessibilityHandler.getDescription(); + + auto name = accessibilityHandler.getTitle(); + + if (name.isEmpty() && isFragmentRoot()) + return getAccessibleApplicationOrPluginName(); + + return name; +} + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_AccessibilityElement.h b/modules/juce_gui_basics/native/accessibility/juce_win32_AccessibilityElement.h new file mode 100644 index 0000000000..a1f821df71 --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_AccessibilityElement.h @@ -0,0 +1,80 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +#define UIA_FullDescriptionPropertyId 30159 +#define UIA_IsDialogPropertyId 30174 + +class AccessibilityNativeHandle : public ComBaseClassHelper +{ +public: + explicit AccessibilityNativeHandle (AccessibilityHandler& handler); + + //============================================================================== + void invalidateElement() noexcept { valid = false; } + bool isElementValid() const noexcept { return valid; } + + const AccessibilityHandler& getHandler() { return accessibilityHandler; } + + //============================================================================== + JUCE_COMRESULT QueryInterface (REFIID refId, void** result) override; + + //============================================================================== + JUCE_COMRESULT get_HostRawElementProvider (IRawElementProviderSimple** provider) override; + JUCE_COMRESULT get_ProviderOptions (ProviderOptions* options) override; + JUCE_COMRESULT GetPatternProvider (PATTERNID pId, IUnknown** provider) override; + JUCE_COMRESULT GetPropertyValue (PROPERTYID propertyId, VARIANT* pRetVal) override; + + JUCE_COMRESULT Navigate (NavigateDirection direction, IRawElementProviderFragment** pRetVal) override; + JUCE_COMRESULT GetRuntimeId (SAFEARRAY** pRetVal) override; + JUCE_COMRESULT get_BoundingRectangle (UiaRect* pRetVal) override; + JUCE_COMRESULT GetEmbeddedFragmentRoots (SAFEARRAY** pRetVal) override; + JUCE_COMRESULT SetFocus() override; + JUCE_COMRESULT get_FragmentRoot (IRawElementProviderFragmentRoot** pRetVal) override; + + JUCE_COMRESULT ElementProviderFromPoint (double x, double y, IRawElementProviderFragment** pRetVal) override; + JUCE_COMRESULT GetFocus (IRawElementProviderFragment** pRetVal) override; + +private: + //============================================================================== + String getElementName() const; + bool isFragmentRoot() const { return accessibilityHandler.getComponent().isOnDesktop(); } + + //============================================================================== + AccessibilityHandler& accessibilityHandler; + + static int idCounter; + std::array rtid { UiaAppendRuntimeId, ++idCounter }; + bool valid = true; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityNativeHandle) +}; + +} diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIAExpandCollapseProvider.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAExpandCollapseProvider.h new file mode 100644 index 0000000000..159d1f2864 --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAExpandCollapseProvider.h @@ -0,0 +1,86 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +class UIAExpandCollapseProvider : public UIAProviderBase, + public ComBaseClassHelper +{ +public: + explicit UIAExpandCollapseProvider (AccessibilityNativeHandle* nativeHandle) + : UIAProviderBase (nativeHandle) + { + } + + //============================================================================== + JUCE_COMRESULT Expand() override + { + return invokeShowMenu(); + } + + JUCE_COMRESULT Collapse() override + { + return invokeShowMenu(); + } + + JUCE_COMRESULT get_ExpandCollapseState (ExpandCollapseState* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + *pRetVal = getHandler().getCurrentState().isExpanded() + ? ExpandCollapseState_Expanded + : ExpandCollapseState_Collapsed; + + return S_OK; + }); + } + +private: + JUCE_COMRESULT invokeShowMenu() + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + const auto& handler = getHandler(); + + if (handler.getActions().invoke (AccessibilityActionType::showMenu)) + { + sendAccessibilityAutomationEvent (handler, handler.getCurrentState().isExpanded() + ? UIA_MenuOpenedEventId + : UIA_MenuClosedEventId); + + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAExpandCollapseProvider) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridItemProvider.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridItemProvider.h new file mode 100644 index 0000000000..d40e689189 --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridItemProvider.h @@ -0,0 +1,101 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +class UIAGridItemProvider : public UIAProviderBase, + public ComBaseClassHelper +{ +public: + explicit UIAGridItemProvider (AccessibilityNativeHandle* nativeHandle) + : UIAProviderBase (nativeHandle) + { + } + + //============================================================================== + JUCE_COMRESULT get_Row (int* pRetVal) override + { + return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) + { + *pRetVal = cellInterface.getRowIndex(); + }); + } + + JUCE_COMRESULT get_Column (int* pRetVal) override + { + return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) + { + *pRetVal = cellInterface.getColumnIndex(); + }); + } + + JUCE_COMRESULT get_RowSpan (int* pRetVal) override + { + return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) + { + *pRetVal = cellInterface.getRowSpan(); + }); + } + + JUCE_COMRESULT get_ColumnSpan (int* pRetVal) override + { + return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) + { + *pRetVal = cellInterface.getColumnSpan(); + }); + } + + JUCE_COMRESULT get_ContainingGrid (IRawElementProviderSimple** pRetVal) override + { + return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) + { + if (auto* handler = cellInterface.getTableHandler()) + handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); + }); + } + +private: + template + JUCE_COMRESULT withCellInterface (Value* pRetVal, Callback&& callback) const + { + return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT + { + if (auto* cellInterface = getHandler().getCellInterface()) + { + callback (*cellInterface); + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + }); + } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAGridItemProvider) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridProvider.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridProvider.h new file mode 100644 index 0000000000..0e540c9deb --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridProvider.h @@ -0,0 +1,90 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +class UIAGridProvider : public UIAProviderBase, + public ComBaseClassHelper +{ +public: + explicit UIAGridProvider (AccessibilityNativeHandle* nativeHandle) + : UIAProviderBase (nativeHandle) + { + } + + //============================================================================== + JUCE_COMRESULT GetItem (int row, int column, IRawElementProviderSimple** pRetVal) override + { + return withTableInterface (pRetVal, [&] (const AccessibilityTableInterface& tableInterface) + { + if (! isPositiveAndBelow (row, tableInterface.getNumRows()) + || ! isPositiveAndBelow (column, tableInterface.getNumColumns())) + return E_INVALIDARG; + + if (auto* handler = tableInterface.getCellHandler (row, column)) + handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); + + return S_OK; + }); + } + + JUCE_COMRESULT get_RowCount (int* pRetVal) override + { + return withTableInterface (pRetVal, [&] (const AccessibilityTableInterface& tableInterface) + { + *pRetVal = tableInterface.getNumRows(); + return S_OK; + }); + } + + JUCE_COMRESULT get_ColumnCount (int* pRetVal) override + { + return withTableInterface (pRetVal, [&] (const AccessibilityTableInterface& tableInterface) + { + *pRetVal = tableInterface.getNumColumns(); + return S_OK; + }); + } + +private: + template + JUCE_COMRESULT withTableInterface (Value* pRetVal, Callback&& callback) const + { + return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT + { + if (auto* tableInterface = getHandler().getTableInterface()) + return callback (*tableInterface); + + return UIA_E_NOTSUPPORTED; + }); + } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAGridProvider) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIAHelpers.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAHelpers.h new file mode 100644 index 0000000000..02a473961c --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAHelpers.h @@ -0,0 +1,103 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +namespace VariantHelpers +{ + inline void clear (VARIANT* variant) + { + variant->vt = VT_EMPTY; + } + + inline void setInt (int value, VARIANT* variant) + { + variant->vt = VT_I4; + variant->lVal = value; + } + + inline void setBool (bool value, VARIANT* variant) + { + variant->vt = VT_BOOL; + variant->boolVal = value ? -1 : 0; + } + + inline void setString (const String& value, VARIANT* variant) + { + variant->vt = VT_BSTR; + variant->bstrVal = SysAllocString ((const OLECHAR*) value.toWideCharPointer()); + } + + inline void setDouble (double value, VARIANT* variant) + { + variant->vt = VT_R8; + variant->dblVal = value; + } +} + +JUCE_COMRESULT addHandlersToArray (const std::vector& handlers, SAFEARRAY** pRetVal) +{ + auto numHandlers = handlers.size(); + + *pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, (ULONG) numHandlers); + + if (pRetVal != nullptr) + { + for (LONG i = 0; i < (LONG) numHandlers; ++i) + { + auto* handler = handlers[i]; + + if (handler == nullptr) + continue; + + ComSmartPtr provider; + handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress())); + + auto hr = SafeArrayPutElement (*pRetVal, &i, provider); + + if (FAILED (hr)) + return E_FAIL; + } + } + + return S_OK; +} + +template +JUCE_COMRESULT withCheckedComArgs (Value* pRetVal, Object& handle, Callback&& callback) +{ + if (pRetVal == nullptr) + return E_INVALIDARG; + + *pRetVal = Value{}; + + if (! handle.isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + return callback(); +} + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIAInvokeProvider.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAInvokeProvider.h new file mode 100644 index 0000000000..8c9fa7674f --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAInvokeProvider.h @@ -0,0 +1,62 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +class UIAInvokeProvider : public UIAProviderBase, + public ComBaseClassHelper +{ +public: + explicit UIAInvokeProvider (AccessibilityNativeHandle* nativeHandle) + : UIAProviderBase (nativeHandle) + { + } + + //============================================================================== + JUCE_COMRESULT Invoke() override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + const auto& handler = getHandler(); + + if (handler.getActions().invoke (AccessibilityActionType::press)) + { + if (isElementValid()) + sendAccessibilityAutomationEvent (handler, UIA_Invoke_InvokedEventId); + + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAInvokeProvider) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIAProviderBase.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAProviderBase.h new file mode 100644 index 0000000000..e9294ee730 --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAProviderBase.h @@ -0,0 +1,58 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +class UIAProviderBase +{ +public: + explicit UIAProviderBase (AccessibilityNativeHandle* nativeHandleIn) + : nativeHandle (nativeHandleIn) + { + } + + bool isElementValid() const + { + if (nativeHandle != nullptr) + return nativeHandle->isElementValid(); + + return false; + } + + const AccessibilityHandler& getHandler() const + { + return nativeHandle->getHandler(); + } + +private: + ComSmartPtr nativeHandle; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAProviderBase) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIAProviders.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAProviders.h new file mode 100644 index 0000000000..84e266f309 --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAProviders.h @@ -0,0 +1,43 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + void sendAccessibilityAutomationEvent (const AccessibilityHandler&, EVENTID); + void sendAccessibilityPropertyChangedEvent (const AccessibilityHandler&, PROPERTYID, VARIANT); +} + +#include "juce_win32_UIAProviderBase.h" +#include "juce_win32_UIAExpandCollapseProvider.h" +#include "juce_win32_UIAGridItemProvider.h" +#include "juce_win32_UIAGridProvider.h" +#include "juce_win32_UIAInvokeProvider.h" +#include "juce_win32_UIARangeValueProvider.h" +#include "juce_win32_UIASelectionProvider.h" +#include "juce_win32_UIATextProvider.h" +#include "juce_win32_UIAToggleProvider.h" +#include "juce_win32_UIATransformProvider.h" +#include "juce_win32_UIAValueProvider.h" +#include "juce_win32_UIAWindowProvider.h" diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIARangeValueProvider.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIARangeValueProvider.h new file mode 100644 index 0000000000..5061af4d19 --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIARangeValueProvider.h @@ -0,0 +1,140 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +class UIARangeValueProvider : public UIAProviderBase, + public ComBaseClassHelper +{ +public: + explicit UIARangeValueProvider (AccessibilityNativeHandle* nativeHandle) + : UIAProviderBase (nativeHandle) + { + } + + //============================================================================== + JUCE_COMRESULT SetValue (double val) override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + const auto& handler = getHandler(); + + if (auto* valueInterface = handler.getValueInterface()) + { + auto range = valueInterface->getRange(); + + if (range.isValid()) + { + if (val < range.getMinimumValue() || val > range.getMaximumValue()) + return E_INVALIDARG; + + if (! valueInterface->isReadOnly()) + { + valueInterface->setValue (val); + + VARIANT newValue; + VariantHelpers::setDouble (valueInterface->getCurrentValue(), &newValue); + sendAccessibilityPropertyChangedEvent (handler, UIA_RangeValueValuePropertyId, newValue); + + return S_OK; + } + } + } + + return UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT get_Value (double* pRetVal) override + { + return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface) + { + return valueInterface.getCurrentValue(); + }); + } + + JUCE_COMRESULT get_IsReadOnly (BOOL* pRetVal) override + { + return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface) + { + return valueInterface.isReadOnly(); + }); + } + + JUCE_COMRESULT get_Maximum (double* pRetVal) override + { + return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface) + { + return valueInterface.getRange().getMaximumValue(); + }); + } + + JUCE_COMRESULT get_Minimum (double* pRetVal) override + { + return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface) + { + return valueInterface.getRange().getMinimumValue(); + }); + } + + JUCE_COMRESULT get_LargeChange (double* pRetVal) override + { + return get_SmallChange (pRetVal); + } + + JUCE_COMRESULT get_SmallChange (double* pRetVal) override + { + return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface) + { + return valueInterface.getRange().getInterval(); + }); + } + +private: + template + JUCE_COMRESULT withValueInterface (Value* pRetVal, Callback&& callback) const + { + return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT + { + if (auto* valueInterface = getHandler().getValueInterface()) + { + if (valueInterface->getRange().isValid()) + { + *pRetVal = callback (*valueInterface); + return S_OK; + } + } + + return UIA_E_NOTSUPPORTED; + }); + } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIARangeValueProvider) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIASelectionProvider.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIASelectionProvider.h new file mode 100644 index 0000000000..64f0761a15 --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIASelectionProvider.h @@ -0,0 +1,252 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +JUCE_COMCLASS (ISelectionProvider2, "14f68475-ee1c-44f6-a869-d239381f0fe7") : public ISelectionProvider +{ + JUCE_COMCALL get_FirstSelectedItem (IRawElementProviderSimple** retVal) = 0; + JUCE_COMCALL get_LastSelectedItem (IRawElementProviderSimple** retVal) = 0; + JUCE_COMCALL get_CurrentSelectedItem (IRawElementProviderSimple** retVal) = 0; + JUCE_COMCALL get_ItemCount (int* retVal) = 0; +}; + +//============================================================================== +class UIASelectionItemProvider : public UIAProviderBase, + public ComBaseClassHelper +{ +public: + explicit UIASelectionItemProvider (AccessibilityNativeHandle* nativeHandle) + : UIAProviderBase (nativeHandle), + isRadioButton (getHandler().getRole() == AccessibilityRole::radioButton) + { + } + + //============================================================================== + JUCE_COMRESULT AddToSelection() override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + const auto& handler = getHandler(); + + if (isRadioButton) + { + handler.getActions().invoke (AccessibilityActionType::press); + sendAccessibilityAutomationEvent (handler, UIA_SelectionItem_ElementSelectedEventId); + + return S_OK; + } + + handler.getActions().invoke (AccessibilityActionType::toggle); + handler.getActions().invoke (AccessibilityActionType::press); + + return S_OK; + } + + JUCE_COMRESULT get_IsSelected (BOOL* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + const auto state = getHandler().getCurrentState(); + *pRetVal = isRadioButton ? state.isChecked() : state.isSelected(); + return S_OK; + }); + } + + JUCE_COMRESULT get_SelectionContainer (IRawElementProviderSimple** pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + if (! isRadioButton) + if (auto* parent = getHandler().getParent()) + parent->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); + + return S_OK; + }); + } + + JUCE_COMRESULT RemoveFromSelection() override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + if (! isRadioButton) + { + const auto& handler = getHandler(); + + if (handler.getCurrentState().isSelected()) + getHandler().getActions().invoke (AccessibilityActionType::toggle); + } + + return S_OK; + } + + JUCE_COMRESULT Select() override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + AddToSelection(); + + if (! isRadioButton) + { + const auto& handler = getHandler(); + + if (auto* parent = handler.getParent()) + for (auto* child : parent->getChildren()) + if (child != &handler && child->getCurrentState().isSelected()) + child->getActions().invoke (AccessibilityActionType::toggle); + } + + return S_OK; + } + +private: + const bool isRadioButton; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIASelectionItemProvider) +}; + +//============================================================================== +class UIASelectionProvider : public UIAProviderBase, + public ComBaseClassHelper +{ +public: + explicit UIASelectionProvider (AccessibilityNativeHandle* nativeHandle) + : UIAProviderBase (nativeHandle) + { + } + + //============================================================================== + JUCE_COMRESULT QueryInterface (REFIID iid, void** result) override + { + if (iid == _uuidof (IUnknown) || iid == _uuidof (ISelectionProvider)) + return castToType (result); + + if (iid == _uuidof (ISelectionProvider2)) + return castToType (result); + + *result = nullptr; + return E_NOINTERFACE; + } + + //============================================================================== + JUCE_COMRESULT get_CanSelectMultiple (BOOL* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + *pRetVal = isMultiSelectable(); + return S_OK; + }); + } + + JUCE_COMRESULT get_IsSelectionRequired (BOOL* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + *pRetVal = getSelectedChildren().size() > 0 && ! isMultiSelectable(); + return S_OK; + }); + } + + JUCE_COMRESULT GetSelection (SAFEARRAY** pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + return addHandlersToArray (getSelectedChildren(), pRetVal); + }); + } + + //============================================================================== + JUCE_COMRESULT get_FirstSelectedItem (IRawElementProviderSimple** pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + auto selectedChildren = getSelectedChildren(); + + if (! selectedChildren.empty()) + selectedChildren.front()->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); + + return S_OK; + }); + } + + JUCE_COMRESULT get_LastSelectedItem (IRawElementProviderSimple** pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + auto selectedChildren = getSelectedChildren(); + + if (! selectedChildren.empty()) + selectedChildren.back()->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); + + return S_OK; + }); + } + + JUCE_COMRESULT get_CurrentSelectedItem (IRawElementProviderSimple** pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + get_FirstSelectedItem (pRetVal); + return S_OK; + }); + } + + JUCE_COMRESULT get_ItemCount (int* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + *pRetVal = (int) getSelectedChildren().size(); + return S_OK; + }); + } + +private: + bool isMultiSelectable() const noexcept + { + return getHandler().getCurrentState().isMultiSelectable(); + } + + std::vector getSelectedChildren() const + { + std::vector selectedHandlers; + + for (auto* child : getHandler().getComponent().getChildren()) + if (auto* handler = child->getAccessibilityHandler()) + if (handler->getCurrentState().isSelected()) + selectedHandlers.push_back (handler); + + return selectedHandlers; + } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIASelectionProvider) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIATextProvider.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIATextProvider.h new file mode 100644 index 0000000000..391ce9a507 --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIATextProvider.h @@ -0,0 +1,664 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +class UIATextProvider : public UIAProviderBase, + public ComBaseClassHelper +{ +public: + explicit UIATextProvider (AccessibilityNativeHandle* nativeHandle) + : UIAProviderBase (nativeHandle) + { + } + + //============================================================================== + JUCE_COMRESULT QueryInterface (REFIID iid, void** result) override + { + if (iid == _uuidof (IUnknown) || iid == _uuidof (ITextProvider)) + return castToType (result); + + if (iid == _uuidof (ITextProvider2)) + return castToType (result); + + *result = nullptr; + return E_NOINTERFACE; + } + + //============================================================================= + JUCE_COMRESULT get_DocumentRange (ITextRangeProvider** pRetVal) override + { + return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) + { + *pRetVal = new UIATextRangeProvider (*this, { 0, textInterface.getTotalNumCharacters() }); + return S_OK; + }); + } + + JUCE_COMRESULT get_SupportedTextSelection (SupportedTextSelection* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + *pRetVal = SupportedTextSelection_Single; + return S_OK; + }); + } + + JUCE_COMRESULT GetSelection (SAFEARRAY** pRetVal) override + { + return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) + { + *pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, 1); + + if (pRetVal != nullptr) + { + auto selection = textInterface.getSelection(); + auto hasSelection = ! selection.isEmpty(); + auto cursorPos = textInterface.getTextInsertionOffset(); + + auto* rangeProvider = new UIATextRangeProvider (*this, + { hasSelection ? selection.getStart() : cursorPos, + hasSelection ? selection.getEnd() : cursorPos }); + + LONG pos = 0; + auto hr = SafeArrayPutElement (*pRetVal, &pos, static_cast (rangeProvider)); + + if (FAILED (hr)) + return E_FAIL; + + rangeProvider->Release(); + } + + return S_OK; + }); + } + + JUCE_COMRESULT GetVisibleRanges (SAFEARRAY** pRetVal) override + { + return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) + { + *pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, 1); + + if (pRetVal != nullptr) + { + auto* rangeProvider = new UIATextRangeProvider (*this, { 0, textInterface.getTotalNumCharacters() }); + + LONG pos = 0; + auto hr = SafeArrayPutElement (*pRetVal, &pos, static_cast (rangeProvider)); + + if (FAILED (hr)) + return E_FAIL; + + rangeProvider->Release(); + } + + return S_OK; + }); + } + + JUCE_COMRESULT RangeFromChild (IRawElementProviderSimple*, ITextRangeProvider** pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [] + { + return S_OK; + }); + } + + JUCE_COMRESULT RangeFromPoint (UiaPoint point, ITextRangeProvider** pRetVal) override + { + return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) + { + auto offset = textInterface.getOffsetAtPoint ({ roundToInt (point.x), roundToInt (point.y) }); + + if (offset > 0) + *pRetVal = new UIATextRangeProvider (*this, { offset, offset }); + + return S_OK; + }); + } + + //============================================================================== + JUCE_COMRESULT GetCaretRange (BOOL* isActive, ITextRangeProvider** pRetVal) override + { + return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) + { + *isActive = getHandler().hasFocus (false); + + auto cursorPos = textInterface.getTextInsertionOffset(); + *pRetVal = new UIATextRangeProvider (*this, { cursorPos, cursorPos }); + + return S_OK; + }); + } + + JUCE_COMRESULT RangeFromAnnotation (IRawElementProviderSimple*, ITextRangeProvider** pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [] + { + return S_OK; + }); + } + +private: + //============================================================================== + template + JUCE_COMRESULT withTextInterface (Value* pRetVal, Callback&& callback) const + { + return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT + { + if (auto* textInterface = getHandler().getTextInterface()) + return callback (*textInterface); + + return UIA_E_NOTSUPPORTED; + }); + } + + //============================================================================== + class UIATextRangeProvider : public UIAProviderBase, + public ComBaseClassHelper + { + public: + UIATextRangeProvider (UIATextProvider& textProvider, Range range) + : UIAProviderBase (textProvider.getHandler().getNativeImplementation()), + owner (&textProvider), + selectionRange (range) + { + } + + //============================================================================== + Range getSelectionRange() const noexcept { return selectionRange; } + + //============================================================================== + JUCE_COMRESULT AddToSelection() override + { + return Select(); + } + + JUCE_COMRESULT Clone (ITextRangeProvider** pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + *pRetVal = new UIATextRangeProvider (*owner, selectionRange); + return S_OK; + }); + } + + JUCE_COMRESULT Compare (ITextRangeProvider* range, BOOL* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + *pRetVal = (selectionRange == static_cast (range)->getSelectionRange()); + return S_OK; + }); + } + + JUCE_COMRESULT CompareEndpoints (TextPatternRangeEndpoint endpoint, + ITextRangeProvider* targetRange, + TextPatternRangeEndpoint targetEndpoint, + int* pRetVal) override + { + if (targetRange == nullptr) + return E_INVALIDARG; + + return withCheckedComArgs (pRetVal, *this, [&] + { + auto offset = (endpoint == TextPatternRangeEndpoint_Start ? selectionRange.getStart() + : selectionRange.getEnd()); + + auto otherRange = static_cast (targetRange)->getSelectionRange(); + auto otherOffset = (targetEndpoint == TextPatternRangeEndpoint_Start ? otherRange.getStart() + : otherRange.getEnd()); + + *pRetVal = offset - otherOffset; + return S_OK; + }); + } + + JUCE_COMRESULT ExpandToEnclosingUnit (TextUnit unit) override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + if (auto* textInterface = getHandler().getTextInterface()) + { + auto numCharacters = textInterface->getTotalNumCharacters(); + + if (numCharacters == 0) + { + selectionRange = {}; + return S_OK; + } + + if (unit == TextUnit_Character) + { + selectionRange.setStart (jlimit (0, numCharacters - 1, selectionRange.getStart())); + selectionRange.setEnd (selectionRange.getStart() + 1); + + return S_OK; + } + + if (unit == TextUnit_Paragraph + || unit == TextUnit_Page + || unit == TextUnit_Document) + { + selectionRange = { 0, textInterface->getTotalNumCharacters() }; + return S_OK; + } + + auto start = getNextEndpointPosition (*textInterface, + selectionRange.getStart(), + unit, + NextEndpointDirection::backwards); + + if (start >= 0) + { + auto end = getNextEndpointPosition (*textInterface, + start, + unit, + NextEndpointDirection::forwards); + + if (end >= 0) + selectionRange = Range (start, end); + } + + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT FindAttribute (TEXTATTRIBUTEID, VARIANT, BOOL, ITextRangeProvider** pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [] + { + return S_OK; + }); + } + + JUCE_COMRESULT FindText (BSTR text, BOOL backward, BOOL ignoreCase, + ITextRangeProvider** pRetVal) override + { + return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) + { + auto selectionText = textInterface.getText (selectionRange); + String textToSearchFor (text); + + auto offset = (backward ? (ignoreCase ? selectionText.lastIndexOfIgnoreCase (textToSearchFor) : selectionText.lastIndexOf (textToSearchFor)) + : (ignoreCase ? selectionText.indexOfIgnoreCase (textToSearchFor) : selectionText.indexOf (textToSearchFor))); + + if (offset != -1) + *pRetVal = new UIATextRangeProvider (*owner, { offset, offset + textToSearchFor.length() }); + + return S_OK; + }); + } + + JUCE_COMRESULT GetAttributeValue (TEXTATTRIBUTEID attributeId, VARIANT* pRetVal) override + { + return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) + { + VariantHelpers::clear (pRetVal); + + const auto& handler = getHandler(); + + switch (attributeId) + { + case UIA_IsReadOnlyAttributeId: + { + const auto readOnly = [&] + { + if (auto* valueInterface = handler.getValueInterface()) + return valueInterface->isReadOnly(); + + return false; + }(); + + VariantHelpers::setBool (readOnly, pRetVal); + break; + } + case UIA_CaretPositionAttributeId: + { + auto cursorPos = textInterface.getTextInsertionOffset(); + + auto caretPos = [&] + { + if (cursorPos == 0) + return CaretPosition_BeginningOfLine; + + if (cursorPos == textInterface.getTotalNumCharacters()) + return CaretPosition_EndOfLine; + + return CaretPosition_Unknown; + }(); + + VariantHelpers::setInt (caretPos, pRetVal); + break; + } + default: + break; + } + + return S_OK; + }); + } + + JUCE_COMRESULT GetBoundingRectangles (SAFEARRAY** pRetVal) override + { + return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) + { + auto rectangleList = textInterface.getTextBounds (selectionRange); + auto numRectangles = rectangleList.getNumRectangles(); + + *pRetVal = SafeArrayCreateVector (VT_R8, 0, 4 * numRectangles); + + if (*pRetVal == nullptr) + return E_FAIL; + + if (numRectangles > 0) + { + double* doubleArr = nullptr; + + if (FAILED (SafeArrayAccessData (*pRetVal, reinterpret_cast (&doubleArr)))) + { + SafeArrayDestroy (*pRetVal); + return E_FAIL; + } + + for (int i = 0; i < numRectangles; ++i) + { + auto r = Desktop::getInstance().getDisplays().logicalToPhysical (rectangleList.getRectangle (i)); + + doubleArr[i * 4] = r.getX(); + doubleArr[i * 4 + 1] = r.getY(); + doubleArr[i * 4 + 2] = r.getWidth(); + doubleArr[i * 4 + 3] = r.getHeight(); + } + + if (FAILED (SafeArrayUnaccessData (*pRetVal))) + { + SafeArrayDestroy (*pRetVal); + return E_FAIL; + } + } + + return S_OK; + }); + } + + JUCE_COMRESULT GetChildren (SAFEARRAY** pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + *pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, 0); + return S_OK; + }); + } + + JUCE_COMRESULT GetEnclosingElement (IRawElementProviderSimple** pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + getHandler().getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); + return S_OK; + }); + } + + JUCE_COMRESULT GetText (int maxLength, BSTR* pRetVal) override + { + return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) + { + auto text = textInterface.getText (selectionRange); + + if (maxLength >= 0 && text.length() > maxLength) + text = text.substring (0, maxLength); + + *pRetVal = SysAllocString ((const OLECHAR*) text.toWideCharPointer()); + return S_OK; + }); + } + + JUCE_COMRESULT Move (TextUnit unit, int count, int* pRetVal) override + { + return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface&) + { + if (count > 0) + { + MoveEndpointByUnit (TextPatternRangeEndpoint_End, unit, count, pRetVal); + MoveEndpointByUnit (TextPatternRangeEndpoint_Start, unit, count, pRetVal); + } + else if (count < 0) + { + MoveEndpointByUnit (TextPatternRangeEndpoint_Start, unit, count, pRetVal); + MoveEndpointByUnit (TextPatternRangeEndpoint_End, unit, count, pRetVal); + } + + return S_OK; + }); + } + + JUCE_COMRESULT MoveEndpointByRange (TextPatternRangeEndpoint endpoint, + ITextRangeProvider* targetRange, + TextPatternRangeEndpoint targetEndpoint) override + { + if (targetRange == nullptr) + return E_INVALIDARG; + + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + if (auto* textInterface = getHandler().getTextInterface()) + { + auto otherRange = static_cast (targetRange)->getSelectionRange(); + auto targetPoint = (targetEndpoint == TextPatternRangeEndpoint_Start ? otherRange.getStart() + : otherRange.getEnd()); + + setEndpointChecked (endpoint, targetPoint); + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT MoveEndpointByUnit (TextPatternRangeEndpoint endpoint, + TextUnit unit, + int count, + int* pRetVal) override + { + return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) + { + auto numCharacters = textInterface.getTotalNumCharacters(); + + if (count == 0 || numCharacters == 0) + return S_OK; + + auto isStart = (endpoint == TextPatternRangeEndpoint_Start); + auto endpointToMove = (isStart ? selectionRange.getStart() : selectionRange.getEnd()); + + if (unit == TextUnit_Character) + { + auto targetPoint = jlimit (0, numCharacters, endpointToMove + count); + + *pRetVal = targetPoint - endpointToMove; + setEndpointChecked (endpoint, targetPoint); + + return S_OK; + } + + auto direction = (count > 0 ? NextEndpointDirection::forwards + : NextEndpointDirection::backwards); + + if (unit == TextUnit_Paragraph + || unit == TextUnit_Page + || unit == TextUnit_Document) + { + *pRetVal = (direction == NextEndpointDirection::forwards ? 1 : -1); + setEndpointChecked (endpoint, numCharacters); + return S_OK; + } + + for (int i = 0; i < std::abs (count); ++i) + { + auto nextEndpoint = getNextEndpointPosition (textInterface, + endpointToMove, + unit, + direction); + + if (nextEndpoint < 0) + { + *pRetVal = (direction == NextEndpointDirection::forwards ? i : -i); + setEndpointChecked (endpoint, endpointToMove); + return S_OK; + } + + endpointToMove = nextEndpoint; + } + + *pRetVal = count; + setEndpointChecked (endpoint, endpointToMove); + + return S_OK; + }); + } + + JUCE_COMRESULT RemoveFromSelection() override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + if (auto* textInterface = getHandler().getTextInterface()) + { + textInterface->setSelection ({}); + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT ScrollIntoView (BOOL) override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + return UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT Select() override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + if (auto* textInterface = getHandler().getTextInterface()) + { + textInterface->setSelection ({}); + textInterface->setSelection (selectionRange); + + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + } + + private: + enum class NextEndpointDirection { forwards, backwards }; + + static int getNextEndpointPosition (const AccessibilityTextInterface& textInterface, + int currentPosition, + TextUnit unit, + NextEndpointDirection direction) + { + auto isTextUnitSeparator = [unit] (const juce_wchar c) + { + return ((unit == TextUnit_Word || unit == TextUnit_Format) && CharacterFunctions::isWhitespace (c)) + || (unit == TextUnit_Line && (c == '\r' || c == '\n')); + }; + + constexpr int textBufferSize = 1024; + int numChars = 0; + + if (direction == NextEndpointDirection::forwards) + { + auto textBuffer = textInterface.getText ({ currentPosition, + jmin (textInterface.getTotalNumCharacters(), currentPosition + textBufferSize) }); + + for (auto charPtr = textBuffer.getCharPointer(); ! charPtr.isEmpty();) + { + auto character = charPtr.getAndAdvance(); + ++numChars; + + if (isTextUnitSeparator (character)) + return currentPosition + numChars; + } + } + else + { + auto textBuffer = textInterface.getText ({ jmax (0, currentPosition - textBufferSize), + currentPosition }); + + for (auto charPtr = textBuffer.end() - 1; charPtr != textBuffer.begin(); --charPtr) + { + auto character = *charPtr; + + if (isTextUnitSeparator (character)) + return currentPosition - numChars; + + ++numChars; + } + } + + return -1; + } + + void setEndpointChecked (TextPatternRangeEndpoint endpoint, int newEndpoint) + { + if (endpoint == TextPatternRangeEndpoint_Start) + { + if (selectionRange.getEnd() < newEndpoint) + selectionRange.setEnd (newEndpoint); + + selectionRange.setStart (newEndpoint); + } + else + { + if (selectionRange.getStart() > newEndpoint) + selectionRange.setStart (newEndpoint); + + selectionRange.setEnd (newEndpoint); + } + } + + ComSmartPtr owner; + Range selectionRange; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIATextRangeProvider) + }; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIATextProvider) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIAToggleProvider.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAToggleProvider.h new file mode 100644 index 0000000000..09bf7031c2 --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAToggleProvider.h @@ -0,0 +1,80 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +class UIAToggleProvider : public UIAProviderBase, + public ComBaseClassHelper +{ +public: + explicit UIAToggleProvider (AccessibilityNativeHandle* nativeHandle) + : UIAProviderBase (nativeHandle) + { + } + + //============================================================================== + JUCE_COMRESULT Toggle() override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + const auto& handler = getHandler(); + + if (handler.getActions().invoke (AccessibilityActionType::toggle)) + { + VARIANT newValue; + VariantHelpers::setInt (getCurrentToggleState(), &newValue); + + sendAccessibilityPropertyChangedEvent (handler, UIA_ToggleToggleStatePropertyId, newValue); + + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT get_ToggleState (ToggleState* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + *pRetVal = getCurrentToggleState(); + return S_OK; + }); + } + +private: + ToggleState getCurrentToggleState() const + { + return getHandler().getCurrentState().isChecked() ? ToggleState_On + : ToggleState_Off; + } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAToggleProvider) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIATransformProvider.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIATransformProvider.h new file mode 100644 index 0000000000..bdf16fd3c0 --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIATransformProvider.h @@ -0,0 +1,125 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +class UIATransformProvider : public UIAProviderBase, + public ComBaseClassHelper +{ +public: + explicit UIATransformProvider (AccessibilityNativeHandle* nativeHandle) + : UIAProviderBase (nativeHandle) + { + } + + //============================================================================== + JUCE_COMRESULT Move (double x, double y) override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + if (auto* peer = getPeer()) + { + RECT rect; + GetWindowRect ((HWND) peer->getNativeHandle(), &rect); + + rect.left = roundToInt (x); + rect.top = roundToInt (y); + + auto bounds = Rectangle::leftTopRightBottom (rect.left, rect.top, rect.right, rect.bottom); + + peer->setBounds (Desktop::getInstance().getDisplays().physicalToLogical (bounds), + peer->isFullScreen()); + } + + return S_OK; + } + + JUCE_COMRESULT Resize (double width, double height) override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + if (auto* peer = getPeer()) + { + auto scale = peer->getPlatformScaleFactor(); + + peer->getComponent().setSize (roundToInt (width / scale), + roundToInt (height / scale)); + } + + return S_OK; + } + + JUCE_COMRESULT Rotate (double) override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + return UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT get_CanMove (BOOL* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + *pRetVal = true; + return S_OK; + }); + } + + JUCE_COMRESULT get_CanResize (BOOL* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + if (auto* peer = getPeer()) + *pRetVal = ((peer->getStyleFlags() & ComponentPeer::windowIsResizable) != 0); + + return S_OK; + }); + } + + JUCE_COMRESULT get_CanRotate (BOOL* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + *pRetVal = false; + return S_OK; + }); + } + +private: + ComponentPeer* getPeer() const + { + return getHandler().getComponent().getPeer(); + } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIATransformProvider) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIAValueProvider.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAValueProvider.h new file mode 100644 index 0000000000..fb2195ad3f --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAValueProvider.h @@ -0,0 +1,121 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +class UIAValueProvider : public UIAProviderBase, + public ComBaseClassHelper +{ +public: + UIAValueProvider (AccessibilityNativeHandle* nativeHandle, bool editableText) + : UIAProviderBase (nativeHandle), + isEditableText (editableText) + { + } + + //============================================================================== + JUCE_COMRESULT SetValue (LPCWSTR val) override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + const auto& handler = getHandler(); + + const auto sendValuePropertyChangeMessage = [&]() + { + VARIANT newValue; + VariantHelpers::setString (getCurrentValueString(), &newValue); + + sendAccessibilityPropertyChangedEvent (handler, UIA_ValueValuePropertyId, newValue); + }; + + if (isEditableText) + { + handler.getTextInterface()->setText (String (val)); + sendValuePropertyChangeMessage(); + + return S_OK; + } + + if (auto* valueInterface = handler.getValueInterface()) + { + if (! valueInterface->isReadOnly()) + { + valueInterface->setValueAsString (String (val)); + sendValuePropertyChangeMessage(); + + return S_OK; + } + } + + return UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT get_Value (BSTR* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + auto currentValue = getCurrentValueString(); + + *pRetVal = SysAllocString ((const OLECHAR*) currentValue.toWideCharPointer()); + return S_OK; + }); + } + + JUCE_COMRESULT get_IsReadOnly (BOOL* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&] + { + if (! isEditableText) + if (auto* valueInterface = getHandler().getValueInterface()) + *pRetVal = valueInterface->isReadOnly(); + + return S_OK; + }); + } + +private: + String getCurrentValueString() const + { + if (isEditableText) + if (auto* textInterface = getHandler().getTextInterface()) + return textInterface->getText ({ 0, textInterface->getTotalNumCharacters() }); + + if (auto* valueInterface = getHandler().getValueInterface()) + return valueInterface->getCurrentValueAsString(); + + jassertfalse; + return {}; + } + + const bool isEditableText; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAValueProvider) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIAWindowProvider.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAWindowProvider.h new file mode 100644 index 0000000000..b6a32a0005 --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAWindowProvider.h @@ -0,0 +1,197 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +class UIAWindowProvider : public UIAProviderBase, + public ComBaseClassHelper +{ +public: + explicit UIAWindowProvider (AccessibilityNativeHandle* nativeHandle) + : UIAProviderBase (nativeHandle) + { + } + + //============================================================================== + JUCE_COMRESULT SetVisualState (WindowVisualState state) override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + if (auto* peer = getPeer()) + { + switch (state) + { + case WindowVisualState_Maximized: + peer->setFullScreen (true); + break; + + case WindowVisualState_Minimized: + peer->setMinimised (true); + break; + + case WindowVisualState_Normal: + peer->setFullScreen (false); + peer->setMinimised (false); + break; + + default: + break; + } + + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT Close() override + { + if (! isElementValid()) + return UIA_E_ELEMENTNOTAVAILABLE; + + if (auto* peer = getPeer()) + { + peer->handleUserClosingWindow(); + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT WaitForInputIdle (int, BOOL* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [] + { + return UIA_E_NOTSUPPORTED; + }); + } + + JUCE_COMRESULT get_CanMaximize (BOOL* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT + { + if (auto* peer = getPeer()) + { + *pRetVal = (peer->getStyleFlags() & ComponentPeer::windowHasMaximiseButton) != 0; + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + }); + } + + JUCE_COMRESULT get_CanMinimize (BOOL* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT + { + if (auto* peer = getPeer()) + { + *pRetVal = (peer->getStyleFlags() & ComponentPeer::windowHasMinimiseButton) != 0; + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + }); + } + + JUCE_COMRESULT get_IsModal (BOOL* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT + { + if (auto* peer = getPeer()) + { + *pRetVal = peer->getComponent().isCurrentlyModal(); + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + }); + } + + JUCE_COMRESULT get_WindowVisualState (WindowVisualState* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT + { + if (auto* peer = getPeer()) + { + if (peer->isFullScreen()) + *pRetVal = WindowVisualState_Maximized; + else if (peer->isMinimised()) + *pRetVal = WindowVisualState_Minimized; + else + *pRetVal = WindowVisualState_Normal; + + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + }); + } + + JUCE_COMRESULT get_WindowInteractionState (WindowInteractionState* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT + { + if (auto* peer = getPeer()) + { + *pRetVal = peer->getComponent().isCurrentlyBlockedByAnotherModalComponent() + ? WindowInteractionState::WindowInteractionState_BlockedByModalWindow + : WindowInteractionState::WindowInteractionState_Running; + + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + }); + } + + JUCE_COMRESULT get_IsTopmost (BOOL* pRetVal) override + { + return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT + { + if (auto* peer = getPeer()) + { + *pRetVal = peer->isFocused(); + return S_OK; + } + + return UIA_E_NOTSUPPORTED; + }); + } + +private: + ComponentPeer* getPeer() const + { + return getHandler().getComponent().getPeer(); + } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAWindowProvider) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_WindowsUIAWrapper.h b/modules/juce_gui_basics/native/accessibility/juce_win32_WindowsUIAWrapper.h new file mode 100644 index 0000000000..f31996de28 --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_WindowsUIAWrapper.h @@ -0,0 +1,158 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +class WindowsUIAWrapper : public DeletedAtShutdown +{ +public: + bool isLoaded() const noexcept + { + return uiaReturnRawElementProvider != nullptr + && uiaHostProviderFromHwnd != nullptr + && uiaRaiseAutomationPropertyChangedEvent != nullptr + && uiaRaiseAutomationEvent != nullptr + && uiaClientsAreListening != nullptr + && uiaDisconnectProvider != nullptr + && uiaDisconnectAllProviders != nullptr; + } + + //============================================================================== + LRESULT returnRawElementProvider (HWND hwnd, WPARAM wParam, LPARAM lParam, IRawElementProviderSimple* provider) + { + return uiaReturnRawElementProvider != nullptr ? uiaReturnRawElementProvider (hwnd, wParam, lParam, provider) + : (LRESULT) nullptr; + } + + JUCE_COMRESULT hostProviderFromHwnd (HWND hwnd, IRawElementProviderSimple** provider) + { + return uiaHostProviderFromHwnd != nullptr ? uiaHostProviderFromHwnd (hwnd, provider) + : UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT raiseAutomationPropertyChangedEvent (IRawElementProviderSimple* provider, PROPERTYID propID, VARIANT oldValue, VARIANT newValue) + { + return uiaRaiseAutomationPropertyChangedEvent != nullptr ? uiaRaiseAutomationPropertyChangedEvent (provider, propID, oldValue, newValue) + : UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT raiseAutomationEvent (IRawElementProviderSimple* provider, EVENTID eventID) + { + return uiaRaiseAutomationEvent != nullptr ? uiaRaiseAutomationEvent (provider, eventID) + : UIA_E_NOTSUPPORTED; + } + + BOOL clientsAreListening() + { + return uiaClientsAreListening != nullptr ? uiaClientsAreListening() + : false; + } + + JUCE_COMRESULT disconnectProvider (IRawElementProviderSimple* provider) + { + if (uiaDisconnectProvider != nullptr) + { + const ScopedValueSetter disconnectingProviderSetter (disconnectingProvider, provider); + return uiaDisconnectProvider (provider); + } + + return UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT disconnectAllProviders() + { + if (uiaDisconnectAllProviders != nullptr) + { + const ScopedValueSetter disconnectingAllProvidersSetter (disconnectingAllProviders, true); + return uiaDisconnectAllProviders(); + } + + return UIA_E_NOTSUPPORTED; + } + + //============================================================================== + bool isProviderDisconnecting (IRawElementProviderSimple* provider) + { + return disconnectingProvider == provider || disconnectingAllProviders; + } + + //============================================================================== + JUCE_DECLARE_SINGLETON_SINGLETHREADED_MINIMAL (WindowsUIAWrapper) + +private: + //============================================================================== + WindowsUIAWrapper() + { + // force UIA COM library initialisation here to prevent an exception when calling methods from SendMessage() + if (isLoaded()) + returnRawElementProvider (nullptr, 0, 0, nullptr); + else + jassertfalse; // UIAutomationCore could not be loaded! + } + + ~WindowsUIAWrapper() + { + disconnectAllProviders(); + + if (uiaHandle != nullptr) + ::FreeLibrary (uiaHandle); + + clearSingletonInstance(); + } + + //============================================================================== + template + static FuncType getUiaFunction (HMODULE module, StringRef funcName) + { + return (FuncType) GetProcAddress (module, funcName); + } + + //============================================================================== + using UiaReturnRawElementProviderFunc = LRESULT (WINAPI*) (HWND, WPARAM, LPARAM, IRawElementProviderSimple*); + using UiaHostProviderFromHwndFunc = HRESULT (WINAPI*) (HWND, IRawElementProviderSimple**); + using UiaRaiseAutomationPropertyChangedEventFunc = HRESULT (WINAPI*) (IRawElementProviderSimple*, PROPERTYID, VARIANT, VARIANT); + using UiaRaiseAutomationEventFunc = HRESULT (WINAPI*) (IRawElementProviderSimple*, EVENTID); + using UiaClientsAreListeningFunc = BOOL (WINAPI*) (); + using UiaDisconnectProviderFunc = HRESULT (WINAPI*) (IRawElementProviderSimple*); + using UiaDisconnectAllProvidersFunc = HRESULT (WINAPI*) (); + + HMODULE uiaHandle = ::LoadLibraryA ("UIAutomationCore.dll"); + UiaReturnRawElementProviderFunc uiaReturnRawElementProvider = getUiaFunction (uiaHandle, "UiaReturnRawElementProvider"); + UiaHostProviderFromHwndFunc uiaHostProviderFromHwnd = getUiaFunction (uiaHandle, "UiaHostProviderFromHwnd"); + UiaRaiseAutomationPropertyChangedEventFunc uiaRaiseAutomationPropertyChangedEvent = getUiaFunction (uiaHandle, "UiaRaiseAutomationPropertyChangedEvent"); + UiaRaiseAutomationEventFunc uiaRaiseAutomationEvent = getUiaFunction (uiaHandle, "UiaRaiseAutomationEvent"); + UiaClientsAreListeningFunc uiaClientsAreListening = getUiaFunction (uiaHandle, "UiaClientsAreListening"); + UiaDisconnectProviderFunc uiaDisconnectProvider = getUiaFunction (uiaHandle, "UiaDisconnectProvider"); + UiaDisconnectAllProvidersFunc uiaDisconnectAllProviders = getUiaFunction (uiaHandle, "UiaDisconnectAllProviders"); + + IRawElementProviderSimple* disconnectingProvider = nullptr; + bool disconnectingAllProviders = false; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WindowsUIAWrapper) +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm b/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm index a5632029ca..9bd73caf04 100644 --- a/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm +++ b/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm @@ -56,26 +56,6 @@ namespace juce namespace juce { -//============================================================================== -static CGFloat getMainScreenHeight() noexcept -{ - if ([[NSScreen screens] count] == 0) - return 0.0f; - - return [[[NSScreen screens] objectAtIndex: 0] frame].size.height; -} - -static void flipScreenRect (NSRect& r) noexcept -{ - r.origin.y = getMainScreenHeight() - (r.origin.y + r.size.height); -} - -static NSRect flippedScreenRect (NSRect r) noexcept -{ - flipScreenRect (r); - return r; -} - //============================================================================== class NSViewComponentPeer : public ComponentPeer, private Timer @@ -124,7 +104,7 @@ public: { r.origin.x = (CGFloat) component.getX(); r.origin.y = (CGFloat) component.getY(); - flipScreenRect (r); + r = flippedScreenRect (r); window = [createWindowInstance() initWithContentRect: r styleMask: getNSWindowStyleMask (windowStyleFlags) @@ -323,7 +303,7 @@ public: r = [[view superview] convertRect: r toView: nil]; r = [viewWindow convertRectToScreen: r]; - flipScreenRect (r); + r = flippedScreenRect (r); } return convertToRectInt (r); @@ -1669,68 +1649,100 @@ const SEL NSViewComponentPeer::becomeKeySelector = @selector (becomeKey:); JUCE_END_IGNORE_WARNINGS_GCC_LIKE //============================================================================== -struct JuceNSViewClass : public ObjCClass +template +struct NSViewComponentPeerWrapper : public Base { - JuceNSViewClass() : ObjCClass ("JUCEView_") + explicit NSViewComponentPeerWrapper (const char* baseName) + : Base (baseName) { - addIvar ("owner"); + Base::template addIvar ("owner"); + } - addMethod (@selector (isOpaque), isOpaque, "c@:"); - addMethod (@selector (drawRect:), drawRect, "v@:", @encode (NSRect)); - addMethod (@selector (mouseDown:), mouseDown, "v@:@"); - addMethod (@selector (mouseUp:), mouseUp, "v@:@"); - addMethod (@selector (mouseDragged:), mouseDragged, "v@:@"); - addMethod (@selector (mouseMoved:), mouseMoved, "v@:@"); - addMethod (@selector (mouseEntered:), mouseEntered, "v@:@"); - addMethod (@selector (mouseExited:), mouseExited, "v@:@"); - addMethod (@selector (rightMouseDown:), mouseDown, "v@:@"); - addMethod (@selector (rightMouseDragged:), mouseDragged, "v@:@"); - addMethod (@selector (rightMouseUp:), mouseUp, "v@:@"); - addMethod (@selector (otherMouseDown:), mouseDown, "v@:@"); - addMethod (@selector (otherMouseDragged:), mouseDragged, "v@:@"); - addMethod (@selector (otherMouseUp:), mouseUp, "v@:@"); - addMethod (@selector (scrollWheel:), scrollWheel, "v@:@"); - addMethod (@selector (magnifyWithEvent:), magnify, "v@:@"); - addMethod (@selector (acceptsFirstMouse:), acceptsFirstMouse, "c@:@"); - addMethod (@selector (windowWillMiniaturize:), windowWillMiniaturize, "v@:@"); - addMethod (@selector (windowDidDeminiaturize:), windowDidDeminiaturize, "v@:@"); - addMethod (@selector (wantsDefaultClipping), wantsDefaultClipping, "c@:"); - addMethod (@selector (worksWhenModal), worksWhenModal, "c@:"); - addMethod (@selector (viewWillMoveToWindow:), willMoveToWindow, "v@:@"); - addMethod (@selector (viewDidMoveToWindow), viewDidMoveToWindow, "v@:"); - addMethod (@selector (viewWillDraw), viewWillDraw, "v@:"); - addMethod (@selector (keyDown:), keyDown, "v@:@"); - addMethod (@selector (keyUp:), keyUp, "v@:@"); - addMethod (@selector (insertText:), insertText, "v@:@"); - addMethod (@selector (doCommandBySelector:), doCommandBySelector, "v@::"); - addMethod (@selector (setMarkedText:selectedRange:), setMarkedText, "v@:@", @encode (NSRange)); - addMethod (@selector (unmarkText), unmarkText, "v@:"); - addMethod (@selector (hasMarkedText), hasMarkedText, "c@:"); - addMethod (@selector (conversationIdentifier), conversationIdentifier, "l@:"); - addMethod (@selector (attributedSubstringFromRange:), attributedSubstringFromRange, "@@:", @encode (NSRange)); - addMethod (@selector (markedRange), markedRange, @encode (NSRange), "@:"); - addMethod (@selector (selectedRange), selectedRange, @encode (NSRange), "@:"); - addMethod (@selector (firstRectForCharacterRange:), firstRectForCharacterRange, @encode (NSRect), "@:", @encode (NSRange)); - addMethod (@selector (characterIndexForPoint:), characterIndexForPoint, "L@:", @encode (NSPoint)); - addMethod (@selector (validAttributesForMarkedText), validAttributesForMarkedText, "@@:"); - addMethod (@selector (flagsChanged:), flagsChanged, "v@:@"); + static NSViewComponentPeer* getOwner (id self) + { + return Base::template getIvar (self, "owner"); + } - addMethod (@selector (becomeFirstResponder), becomeFirstResponder, "c@:"); - addMethod (@selector (resignFirstResponder), resignFirstResponder, "c@:"); - addMethod (@selector (acceptsFirstResponder), acceptsFirstResponder, "c@:"); + static id getAccessibleChild (id self) + { + if (auto* owner = getOwner (self)) + if (auto* handler = owner->getComponent().getAccessibilityHandler()) + return (id) handler->getNativeImplementation(); - addMethod (@selector (draggingEntered:), draggingEntered, @encode (NSDragOperation), "@:@"); - addMethod (@selector (draggingUpdated:), draggingUpdated, @encode (NSDragOperation), "@:@"); - addMethod (@selector (draggingEnded:), draggingEnded, "v@:@"); - addMethod (@selector (draggingExited:), draggingExited, "v@:@"); - addMethod (@selector (prepareForDragOperation:), prepareForDragOperation, "c@:@"); - addMethod (@selector (performDragOperation:), performDragOperation, "c@:@"); - addMethod (@selector (concludeDragOperation:), concludeDragOperation, "v@:@"); + return nil; + } +}; - addMethod (@selector (paste:), paste, "v@:@"); - addMethod (@selector (copy:), copy, "v@:@"); - addMethod (@selector (cut:), cut, "v@:@"); - addMethod (@selector (selectAll:), selectAll, "v@:@"); +struct JuceNSViewClass : public NSViewComponentPeerWrapper> +{ + JuceNSViewClass() : NSViewComponentPeerWrapper ("JUCEView_") + { + addMethod (@selector (isOpaque), isOpaque, "c@:"); + addMethod (@selector (drawRect:), drawRect, "v@:", @encode (NSRect)); + addMethod (@selector (mouseDown:), mouseDown, "v@:@"); + addMethod (@selector (mouseUp:), mouseUp, "v@:@"); + addMethod (@selector (mouseDragged:), mouseDragged, "v@:@"); + addMethod (@selector (mouseMoved:), mouseMoved, "v@:@"); + addMethod (@selector (mouseEntered:), mouseEntered, "v@:@"); + addMethod (@selector (mouseExited:), mouseExited, "v@:@"); + addMethod (@selector (rightMouseDown:), mouseDown, "v@:@"); + addMethod (@selector (rightMouseDragged:), mouseDragged, "v@:@"); + addMethod (@selector (rightMouseUp:), mouseUp, "v@:@"); + addMethod (@selector (otherMouseDown:), mouseDown, "v@:@"); + addMethod (@selector (otherMouseDragged:), mouseDragged, "v@:@"); + addMethod (@selector (otherMouseUp:), mouseUp, "v@:@"); + addMethod (@selector (scrollWheel:), scrollWheel, "v@:@"); + addMethod (@selector (magnifyWithEvent:), magnify, "v@:@"); + addMethod (@selector (acceptsFirstMouse:), acceptsFirstMouse, "c@:@"); + addMethod (@selector (windowWillMiniaturize:), windowWillMiniaturize, "v@:@"); + addMethod (@selector (windowDidDeminiaturize:), windowDidDeminiaturize, "v@:@"); + addMethod (@selector (wantsDefaultClipping), wantsDefaultClipping, "c@:"); + addMethod (@selector (worksWhenModal), worksWhenModal, "c@:"); + addMethod (@selector (viewDidMoveToWindow), viewDidMoveToWindow, "v@:"); + addMethod (@selector (viewWillDraw), viewWillDraw, "v@:"); + addMethod (@selector (keyDown:), keyDown, "v@:@"); + addMethod (@selector (keyUp:), keyUp, "v@:@"); + addMethod (@selector (insertText:), insertText, "v@:@"); + addMethod (@selector (doCommandBySelector:), doCommandBySelector, "v@::"); + addMethod (@selector (setMarkedText:selectedRange:), setMarkedText, "v@:@", @encode (NSRange)); + addMethod (@selector (unmarkText), unmarkText, "v@:"); + addMethod (@selector (hasMarkedText), hasMarkedText, "c@:"); + addMethod (@selector (conversationIdentifier), conversationIdentifier, "l@:"); + addMethod (@selector (attributedSubstringFromRange:), attributedSubstringFromRange, "@@:", @encode (NSRange)); + addMethod (@selector (markedRange), markedRange, @encode (NSRange), "@:"); + addMethod (@selector (selectedRange), selectedRange, @encode (NSRange), "@:"); + addMethod (@selector (firstRectForCharacterRange:), firstRectForCharacterRange, @encode (NSRect), "@:", @encode (NSRange)); + addMethod (@selector (characterIndexForPoint:), characterIndexForPoint, "L@:", @encode (NSPoint)); + addMethod (@selector (validAttributesForMarkedText), validAttributesForMarkedText, "@@:"); + addMethod (@selector (flagsChanged:), flagsChanged, "v@:@"); + + addMethod (@selector (becomeFirstResponder), becomeFirstResponder, "c@:"); + addMethod (@selector (resignFirstResponder), resignFirstResponder, "c@:"); + addMethod (@selector (acceptsFirstResponder), acceptsFirstResponder, "c@:"); + + addMethod (@selector (draggingEntered:), draggingEntered, @encode (NSDragOperation), "@:@"); + addMethod (@selector (draggingUpdated:), draggingUpdated, @encode (NSDragOperation), "@:@"); + addMethod (@selector (draggingEnded:), draggingEnded, "v@:@"); + addMethod (@selector (draggingExited:), draggingExited, "v@:@"); + addMethod (@selector (prepareForDragOperation:), prepareForDragOperation, "c@:@"); + addMethod (@selector (performDragOperation:), performDragOperation, "c@:@"); + addMethod (@selector (concludeDragOperation:), concludeDragOperation, "v@:@"); + + addMethod (@selector (paste:), paste, "v@:@"); + addMethod (@selector (copy:), copy, "v@:@"); + addMethod (@selector (cut:), cut, "v@:@"); + addMethod (@selector (selectAll:), selectAll, "v@:@"); + + addMethod (@selector (viewWillMoveToWindow:), willMoveToWindow, "v@:@"); + + addMethod (@selector (isAccessibilityElement), getIsAccessibilityElement, "c@:"); + addMethod (@selector (accessibilityChildren), getAccessibilityChildren, "@@:"); + addMethod (@selector (accessibilityHitTest:), accessibilityHitTest, "@@:", @encode (NSPoint)); + addMethod (@selector (accessibilityFocusedUIElement), getAccessibilityFocusedUIElement, "@@:"); + + // deprecated methods required for backwards compatibility + addMethod (@selector (accessibilityIsIgnored), getAccessibilityIsIgnored, "c@:"); + addMethod (@selector (accessibilityAttributeValue:), getAccessibilityAttributeValue, "@@:@"); addMethod (@selector (isFlipped), isFlipped, "c@:"); @@ -1746,11 +1758,6 @@ struct JuceNSViewClass : public ObjCClass } private: - static NSViewComponentPeer* getOwner (id self) - { - return getIvar (self, "owner"); - } - static void mouseDown (id self, SEL s, NSEvent* ev) { if (JUCEApplicationBase::isStandaloneApp()) @@ -2072,15 +2079,47 @@ private: } static void concludeDragOperation (id, SEL, id) {} + + //============================================================================== + static BOOL getIsAccessibilityElement (id, SEL) + { + return NO; + } + + static NSArray* getAccessibilityChildren (id self, SEL) + { + return NSAccessibilityUnignoredChildrenForOnlyChild (getAccessibleChild (self)); + } + + static id accessibilityHitTest (id self, SEL, NSPoint point) + { + return [getAccessibleChild (self) accessibilityHitTest: point]; + } + + static id getAccessibilityFocusedUIElement (id self, SEL) + { + return [getAccessibleChild (self) accessibilityFocusedUIElement]; + } + + static BOOL getAccessibilityIsIgnored (id self, SEL) + { + return ! [self isAccessibilityElement]; + } + + static id getAccessibilityAttributeValue (id self, SEL, NSString* attribute) + { + if ([attribute isEqualToString: NSAccessibilityChildrenAttribute]) + return getAccessibilityChildren (self, {}); + + return sendSuperclassMessage (self, @selector (accessibilityAttributeValue:), attribute); + } }; //============================================================================== -struct JuceNSWindowClass : public ObjCClass +struct JuceNSWindowClass : public NSViewComponentPeerWrapper> { - JuceNSWindowClass() : ObjCClass ("JUCEWindow_") + JuceNSWindowClass() : NSViewComponentPeerWrapper ("JUCEWindow_") { - addIvar ("owner"); - addMethod (@selector (canBecomeKeyWindow), canBecomeKeyWindow, "c@:"); addMethod (@selector (canBecomeMainWindow), canBecomeMainWindow, "c@:"); addMethod (@selector (becomeKeyWindow), becomeKeyWindow, "v@:"); @@ -2096,6 +2135,12 @@ struct JuceNSWindowClass : public ObjCClass addMethod (@selector (window:shouldPopUpDocumentPathMenu:), shouldPopUpPathMenu, "B@:@", @encode (NSMenu*)); addMethod (@selector (isFlipped), isFlipped, "c@:"); + addMethod (@selector (accessibilityLabel), getAccessibilityLabel, "@@:"); + addMethod (@selector (accessibilityTopLevelUIElement), getAccessibilityWindow, "@@:"); + addMethod (@selector (accessibilityWindow), getAccessibilityWindow, "@@:"); + addMethod (@selector (accessibilityRole), getAccessibilityRole, "@@:"); + addMethod (@selector (accessibilitySubrole), getAccessibilitySubrole, "@@:"); + addMethod (@selector (window:shouldDragDocumentWithEvent:from:withPasteboard:), shouldAllowIconDrag, "B@:@", @encode (NSEvent*), @encode (NSPoint), @encode (NSPasteboard*)); @@ -2105,11 +2150,6 @@ struct JuceNSWindowClass : public ObjCClass } private: - static NSViewComponentPeer* getOwner (id self) - { - return getIvar (self, "owner"); - } - //============================================================================== static BOOL isFlipped (id, SEL) { return true; } @@ -2249,6 +2289,26 @@ private: return false; } + + static NSString* getAccessibilityLabel (id self, SEL) + { + return [getAccessibleChild (self) accessibilityLabel]; + } + + static id getAccessibilityWindow (id self, SEL) + { + return self; + } + + static NSAccessibilityRole getAccessibilityRole (id, SEL) + { + return NSAccessibilityWindowRole; + } + + static NSAccessibilityRole getAccessibilitySubrole (id self, SEL) + { + return [getAccessibleChild (self) accessibilitySubrole]; + } }; NSView* NSViewComponentPeer::createViewInstance() diff --git a/modules/juce_gui_basics/native/juce_win32_Windowing.cpp b/modules/juce_gui_basics/native/juce_win32_Windowing.cpp index e0b66211a3..efa415f5a4 100644 --- a/modules/juce_gui_basics/native/juce_win32_Windowing.cpp +++ b/modules/juce_gui_basics/native/juce_win32_Windowing.cpp @@ -63,6 +63,14 @@ static bool shouldDeactivateTitleBar = true; void* getUser32Function (const char*); +namespace WindowsAccessibility +{ + void initialiseUIAWrapper(); + long getUiaRootObjectId(); + bool handleWmGetObject (AccessibilityHandler*, WPARAM, LPARAM, LRESULT*); + void revokeUIAMapEntriesForWindow (HWND); +} + #if JUCE_DEBUG int numActiveScopedDpiAwarenessDisablers = 0; bool isInScopedDPIAwarenessDisabler() { return numActiveScopedDpiAwarenessDisablers > 0; } @@ -1372,12 +1380,14 @@ public: parentToAddTo (parent), currentRenderingEngine (softwareRenderingEngine) { + // make sure that the UIA wrapper singleton is loaded + WindowsAccessibility::initialiseUIAWrapper(); + callFunctionIfNotLocked (&createWindowCallback, this); setTitle (component.getName()); updateShadower(); - // make sure that the on-screen keyboard code is loaded OnScreenKeyboard::getInstance(); getNativeRealtimeModifiers = [] @@ -1397,13 +1407,15 @@ public: ~HWNDComponentPeer() { + // do this first to avoid messages arriving for this window before it's destroyed + JuceWindowIdentifier::setAsJUCEWindow (hwnd, false); + + if (isAccessibilityActive) + WindowsAccessibility::revokeUIAMapEntriesForWindow (hwnd); + shadower = nullptr; currentTouches.deleteAllTouchesForPeer (this); - // do this before the next bit to avoid messages arriving for this window - // before it's destroyed - JuceWindowIdentifier::setAsJUCEWindow (hwnd, false); - callFunctionIfNotLocked (&destroyWindowCallback, (void*) hwnd); if (currentWindowIcon != nullptr) @@ -1989,6 +2001,8 @@ private: double scaleFactor = 1.0; bool isInDPIChange = false; + bool isAccessibilityActive = false; + //============================================================================== static MultiTouchMapper currentTouches; @@ -3907,6 +3921,24 @@ private: case WM_GETDLGCODE: return DLGC_WANTALLKEYS; + case WM_GETOBJECT: + { + if (static_cast (lParam) == WindowsAccessibility::getUiaRootObjectId()) + { + if (auto* handler = component.getAccessibilityHandler()) + { + LRESULT res = 0; + + if (WindowsAccessibility::handleWmGetObject (handler, wParam, lParam, &res)) + { + isAccessibilityActive = true; + return res; + } + } + } + + break; + } default: break; } diff --git a/modules/juce_gui_basics/properties/juce_PropertyPanel.cpp b/modules/juce_gui_basics/properties/juce_PropertyPanel.cpp index b9a3aff2ae..701147e9d6 100644 --- a/modules/juce_gui_basics/properties/juce_PropertyPanel.cpp +++ b/modules/juce_gui_basics/properties/juce_PropertyPanel.cpp @@ -204,7 +204,7 @@ void PropertyPanel::init() addAndMakeVisible (viewport); viewport.setViewedComponent (propertyHolderComponent = new PropertyHolderComponent()); - viewport.setFocusContainer (true); + viewport.setFocusContainerType (FocusContainerType::keyboardFocusContainer); } PropertyPanel::~PropertyPanel() diff --git a/modules/juce_gui_basics/widgets/juce_ComboBox.cpp b/modules/juce_gui_basics/widgets/juce_ComboBox.cpp index 923917bf67..fe04769c97 100644 --- a/modules/juce_gui_basics/widgets/juce_ComboBox.cpp +++ b/modules/juce_gui_basics/widgets/juce_ComboBox.cpp @@ -424,6 +424,7 @@ void ComboBox::lookAndFeelChanged() label->onTextChange = [this] { triggerAsyncUpdate(); }; label->addMouseListener (this, false); + label->setAccessible (labelEditableState == labelIsEditable); label->setColour (Label::backgroundColourId, Colours::transparentBlack); label->setColour (Label::textColourId, findColour (ComboBox::textColourId)); @@ -641,4 +642,10 @@ void ComboBox::setSelectedItemIndex (const int index, const bool dontSendChange) void ComboBox::setSelectedId (const int newItemId, const bool dontSendChange) { setSelectedId (newItemId, dontSendChange ? dontSendNotification : sendNotification); } void ComboBox::setText (const String& newText, const bool dontSendChange) { setText (newText, dontSendChange ? dontSendNotification : sendNotification); } +//============================================================================== +std::unique_ptr ComboBox::createAccessibilityHandler() +{ + return std::make_unique (*this); +} + } // namespace juce diff --git a/modules/juce_gui_basics/widgets/juce_ComboBox.h b/modules/juce_gui_basics/widgets/juce_ComboBox.h index f6d67cb7dd..f1587428a5 100644 --- a/modules/juce_gui_basics/widgets/juce_ComboBox.h +++ b/modules/juce_gui_basics/widgets/juce_ComboBox.h @@ -419,6 +419,8 @@ public: void valueChanged (Value&) override; /** @internal */ void parentHierarchyChanged() override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; // These methods' bool parameters have changed: see their new method signatures. JUCE_DEPRECATED (void clear (bool)); diff --git a/modules/juce_gui_basics/widgets/juce_ImageComponent.cpp b/modules/juce_gui_basics/widgets/juce_ImageComponent.cpp index 1f7dc08f42..f5279148ca 100644 --- a/modules/juce_gui_basics/widgets/juce_ImageComponent.cpp +++ b/modules/juce_gui_basics/widgets/juce_ImageComponent.cpp @@ -80,4 +80,10 @@ void ImageComponent::paint (Graphics& g) g.drawImage (image, getLocalBounds().toFloat(), placement); } +//============================================================================== +std::unique_ptr ImageComponent::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::image); +} + } // namespace juce diff --git a/modules/juce_gui_basics/widgets/juce_ImageComponent.h b/modules/juce_gui_basics/widgets/juce_ImageComponent.h index c4bf106f9a..e9ac1de360 100644 --- a/modules/juce_gui_basics/widgets/juce_ImageComponent.h +++ b/modules/juce_gui_basics/widgets/juce_ImageComponent.h @@ -68,6 +68,8 @@ public: //============================================================================== /** @internal */ void paint (Graphics&) override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: Image image; diff --git a/modules/juce_gui_basics/widgets/juce_Label.cpp b/modules/juce_gui_basics/widgets/juce_Label.cpp index 8459d9e927..22da77137a 100644 --- a/modules/juce_gui_basics/widgets/juce_Label.cpp +++ b/modules/juce_gui_basics/widgets/juce_Label.cpp @@ -105,8 +105,13 @@ void Label::setEditable (bool editOnSingleClick, editDoubleClick = editOnDoubleClick; lossOfFocusDiscardsChanges = lossOfFocusDiscards; - setWantsKeyboardFocus (editOnSingleClick || editOnDoubleClick); - setFocusContainer (editOnSingleClick || editOnDoubleClick); + const auto isKeybordFocusable = (editOnSingleClick || editOnDoubleClick); + + setWantsKeyboardFocus (isKeybordFocusable); + setFocusContainerType (isKeybordFocusable ? FocusContainerType::keyboardFocusContainer + : FocusContainerType::none); + + invalidateAccessibilityHandler(); } void Label::setJustificationType (Justification newJustification) @@ -221,6 +226,7 @@ void Label::showEditor() if (editor == nullptr) { editor.reset (createEditorComponent()); + editor->setSize (10, 10); addAndMakeVisible (editor.get()); editor->setText (getText(), false); editor->setKeyboardType (keyboardType); @@ -351,7 +357,9 @@ void Label::mouseDoubleClick (const MouseEvent& e) if (editDoubleClick && isEnabled() && ! e.mods.isPopupMenu()) + { showEditor(); + } } void Label::resized() @@ -364,8 +372,11 @@ void Label::focusGained (FocusChangeType cause) { if (editSingleClick && isEnabled() - && cause == focusChangedByTabKey) + && (cause == focusChangedByTabKey + || (cause == focusChangedDirectly && ! isCurrentlyModal()))) + { showEditor(); + } } void Label::enablementChanged() @@ -393,21 +404,45 @@ void Label::setMinimumHorizontalScale (const float newScale) class LabelKeyboardFocusTraverser : public KeyboardFocusTraverser { public: - LabelKeyboardFocusTraverser() {} + explicit LabelKeyboardFocusTraverser (Label& l) : owner (l) {} - Component* getNextComponent (Component* c) override { return KeyboardFocusTraverser::getNextComponent (getComp (c)); } - Component* getPreviousComponent (Component* c) override { return KeyboardFocusTraverser::getPreviousComponent (getComp (c)); } - - static Component* getComp (Component* current) + Component* getDefaultComponent (Component* parent) override { - return dynamic_cast (current) != nullptr - ? current->getParentComponent() : current; + auto getContainer = [&] + { + if (owner.getCurrentTextEditor() != nullptr && parent == &owner) + return owner.findKeyboardFocusContainer(); + + return parent; + }; + + if (auto* container = getContainer()) + KeyboardFocusTraverser::getDefaultComponent (container); + + return nullptr; } + + Component* getNextComponent (Component* c) override { return KeyboardFocusTraverser::getNextComponent (getComp (c)); } + Component* getPreviousComponent (Component* c) override { return KeyboardFocusTraverser::getPreviousComponent (getComp (c)); } + +private: + Component* getComp (Component* current) const + { + if (auto* ed = owner.getCurrentTextEditor()) + if (current == ed) + return current->getParentComponent(); + + return current; + } + + Label& owner; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LabelKeyboardFocusTraverser) }; -KeyboardFocusTraverser* Label::createFocusTraverser() +std::unique_ptr Label::createKeyboardFocusTraverser() { - return new LabelKeyboardFocusTraverser(); + return std::make_unique (*this); } //============================================================================== @@ -480,4 +515,9 @@ void Label::textEditorFocusLost (TextEditor& ed) textEditorTextChanged (ed); } +std::unique_ptr Label::createAccessibilityHandler() +{ + return std::make_unique (*this); +} + } // namespace juce diff --git a/modules/juce_gui_basics/widgets/juce_Label.h b/modules/juce_gui_basics/widgets/juce_Label.h index 4fb3753ee7..8837da8b9f 100644 --- a/modules/juce_gui_basics/widgets/juce_Label.h +++ b/modules/juce_gui_basics/widgets/juce_Label.h @@ -323,7 +323,7 @@ protected: /** @internal */ void enablementChanged() override; /** @internal */ - KeyboardFocusTraverser* createFocusTraverser() override; + std::unique_ptr createKeyboardFocusTraverser() override; /** @internal */ void textEditorTextChanged (TextEditor&) override; /** @internal */ @@ -338,6 +338,8 @@ protected: void valueChanged (Value&) override; /** @internal */ void callChangeListeners(); + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: //============================================================================== diff --git a/modules/juce_gui_basics/widgets/juce_ListBox.cpp b/modules/juce_gui_basics/widgets/juce_ListBox.cpp index 6cd138d350..dba7ca1712 100644 --- a/modules/juce_gui_basics/widgets/juce_ListBox.cpp +++ b/modules/juce_gui_basics/widgets/juce_ListBox.cpp @@ -26,6 +26,34 @@ namespace juce { +template +static AccessibilityActions getListRowAccessibilityActions (RowHandlerType& handler, RowComponent& rowComponent) +{ + auto onFocus = [&rowComponent] + { + rowComponent.owner.scrollToEnsureRowIsOnscreen (rowComponent.row); + rowComponent.owner.selectRow (rowComponent.row); + }; + + auto onPress = [&rowComponent, onFocus] + { + onFocus(); + rowComponent.owner.keyPressed (KeyPress (KeyPress::returnKey)); + }; + + auto onToggle = [&handler, &rowComponent, onFocus] + { + if (handler.getCurrentState().isSelected()) + rowComponent.owner.deselectRow (rowComponent.row); + else + onFocus(); + }; + + return AccessibilityActions().addAction (AccessibilityActionType::focus, std::move (onFocus)) + .addAction (AccessibilityActionType::press, std::move (onPress)) + .addAction (AccessibilityActionType::toggle, std::move (onToggle)); +} + class ListBox::RowComponent : public Component, public TooltipClient { @@ -35,16 +63,28 @@ public: void paint (Graphics& g) override { if (auto* m = owner.getModel()) - m->paintListBoxItem (row, g, getWidth(), getHeight(), selected); + m->paintListBoxItem (row, g, getWidth(), getHeight(), isSelected); } void update (const int newRow, const bool nowSelected) { - if (row != newRow || selected != nowSelected) + const auto rowHasChanged = (row != newRow); + const auto selectionHasChanged = (isSelected != nowSelected); + + if (rowHasChanged || selectionHasChanged) { repaint(); - row = newRow; - selected = nowSelected; + + if (rowHasChanged) + row = newRow; + + if (selectionHasChanged) + { + isSelected = nowSelected; + + if (auto* handler = getAccessibilityHandler()) + isSelected ? handler->grabFocus() : handler->giveAwayFocus(); + } } if (auto* m = owner.getModel()) @@ -57,6 +97,9 @@ public: { addAndMakeVisible (customComponent.get()); customComponent->setBounds (getLocalBounds()); + + if (customComponent->getAccessibilityHandler() != nullptr) + invalidateAccessibilityHandler(); } } } @@ -85,7 +128,7 @@ public: if (isEnabled()) { - if (owner.selectOnMouseDown && ! (selected || isInDragToScrollViewport())) + if (owner.selectOnMouseDown && ! (isSelected || isInDragToScrollViewport())) performSelection (e, false); else selectRowOnMouseUp = true; @@ -150,10 +193,62 @@ public: return {}; } + //============================================================================== + class RowAccessibilityHandler : public AccessibilityHandler + { + public: + explicit RowAccessibilityHandler (RowComponent& rowComponentToWrap) + : AccessibilityHandler (rowComponentToWrap, + AccessibilityRole::listItem, + getListRowAccessibilityActions (*this, rowComponentToWrap)), + rowComponent (rowComponentToWrap) + { + } + + String getTitle() const override + { + if (auto* m = rowComponent.owner.getModel()) + return m->getNameForRow (rowComponent.row); + + return {}; + } + + AccessibleState getCurrentState() const override + { + if (auto* m = rowComponent.owner.getModel()) + if (rowComponent.row >= m->getNumRows()) + return AccessibleState().withIgnored(); + + auto state = AccessibilityHandler::getCurrentState().withAccessibleOffscreen(); + + if (rowComponent.owner.multipleSelection) + state = state.withMultiSelectable(); + else + state = state.withSelectable(); + + if (rowComponent.isSelected) + state = state.withSelected(); + + return state; + } + + private: + RowComponent& rowComponent; + }; + + std::unique_ptr createAccessibilityHandler() override + { + if (customComponent != nullptr && customComponent->getAccessibilityHandler() != nullptr) + return nullptr; + + return std::make_unique (*this); + } + + //============================================================================== ListBox& owner; std::unique_ptr customComponent; int row = -1; - bool selected = false, isDragging = false, isDraggingToScroll = false, selectRowOnMouseUp = false; + bool isSelected = false, isDragging = false, isDraggingToScroll = false, selectRowOnMouseUp = false; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RowComponent) }; @@ -166,10 +261,13 @@ public: ListViewport (ListBox& lb) : owner (lb) { setWantsKeyboardFocus (false); + setAccessible (false); - auto content = new Component(); - setViewedComponent (content); + auto content = std::make_unique(); content->setWantsKeyboardFocus (false); + content->setAccessible (false); + + setViewedComponent (content.release()); } RowComponent* getComponentForRow (const int row) const noexcept @@ -233,13 +331,12 @@ public: auto y = getViewPositionY(); auto w = content.getWidth(); - const int numNeeded = 2 + getMaximumVisibleHeight() / rowH; + const int numNeeded = 4 + getMaximumVisibleHeight() / rowH; rows.removeRange (numNeeded, rows.size()); while (numNeeded > rows.size()) { - auto newRow = new RowComponent (owner); - rows.add (newRow); + auto* newRow = rows.add (new RowComponent (owner)); content.addAndMakeVisible (newRow); } @@ -247,9 +344,11 @@ public: firstWholeIndex = (y + rowH - 1) / rowH; lastWholeIndex = (y + getMaximumVisibleHeight() - 1) / rowH; + auto startIndex = jmax (0, firstIndex - 1); + for (int i = 0; i < numNeeded; ++i) { - const int row = i + firstIndex; + const int row = i + startIndex; if (auto* rowComp = getComponentForRow (row)) { @@ -379,8 +478,9 @@ ListBox::ListBox (const String& name, ListBoxModel* const m) viewport.reset (new ListViewport (*this)); addAndMakeVisible (viewport.get()); - ListBox::setWantsKeyboardFocus (true); - ListBox::colourChanged(); + setWantsKeyboardFocus (true); + setFocusContainerType (FocusContainerType::focusContainer); + colourChanged(); } ListBox::~ListBox() @@ -938,6 +1038,11 @@ void ListBox::startDragAndDrop (const MouseEvent& e, const SparseSet& rowsT } } +std::unique_ptr ListBox::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::list); +} + //============================================================================== Component* ListBoxModel::refreshComponentForRow (int, bool, Component* existingComponentToUpdate) { @@ -946,6 +1051,7 @@ Component* ListBoxModel::refreshComponentForRow (int, bool, Component* existingC return nullptr; } +String ListBoxModel::getNameForRow (int rowNumber) { return "Row " + String (rowNumber + 1); } void ListBoxModel::listBoxItemClicked (int, const MouseEvent&) {} void ListBoxModel::listBoxItemDoubleClicked (int, const MouseEvent&) {} void ListBoxModel::backgroundClicked (const MouseEvent&) {} diff --git a/modules/juce_gui_basics/widgets/juce_ListBox.h b/modules/juce_gui_basics/widgets/juce_ListBox.h index 1b1f70a444..60af118ec3 100644 --- a/modules/juce_gui_basics/widgets/juce_ListBox.h +++ b/modules/juce_gui_basics/widgets/juce_ListBox.h @@ -86,6 +86,12 @@ public: virtual Component* refreshComponentForRow (int rowNumber, bool isRowSelected, Component* existingComponentToUpdate); + /** This can be overridden to return a name for the specified row. + + By default this will just return a string containing the row number. + */ + virtual String getNameForRow (int rowNumber); + /** This can be overridden to react to the user clicking on a row. @see listBoxItemDoubleClicked */ @@ -565,6 +571,8 @@ public: /** @internal */ void startDragAndDrop (const MouseEvent&, const SparseSet& rowsToDrag, const var& dragDescription, bool allowDraggingToOtherWindows); + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: //============================================================================== diff --git a/modules/juce_gui_basics/widgets/juce_ProgressBar.cpp b/modules/juce_gui_basics/widgets/juce_ProgressBar.cpp index 1723297e7f..085535072e 100644 --- a/modules/juce_gui_basics/widgets/juce_ProgressBar.cpp +++ b/modules/juce_gui_basics/widgets/juce_ProgressBar.cpp @@ -111,7 +111,36 @@ void ProgressBar::timerCallback() currentValue = newProgress; currentMessage = displayedMessage; repaint(); + + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::valueChanged); } } +//============================================================================== +std::unique_ptr ProgressBar::createAccessibilityHandler() +{ + class ProgressBarValueInterface : public AccessibilityRangedNumericValueInterface + { + public: + explicit ProgressBarValueInterface (ProgressBar& progressBarToWrap) + : progressBar (progressBarToWrap) + { + } + + bool isReadOnly() const override { return true; } + void setValue (double) override { jassertfalse; } + double getCurrentValue() const override { return progressBar.progress; } + AccessibleValueRange getRange() const override { return { { 0.0, 1.0 }, 0.001 }; } + + private: + ProgressBar& progressBar; + }; + + return std::make_unique (*this, + AccessibilityRole::progressBar, + AccessibilityActions{}, + AccessibilityHandler::Interfaces { std::make_unique (*this) }); +} + } // namespace juce diff --git a/modules/juce_gui_basics/widgets/juce_ProgressBar.h b/modules/juce_gui_basics/widgets/juce_ProgressBar.h index e6d9712dff..a7e7ee3645 100644 --- a/modules/juce_gui_basics/widgets/juce_ProgressBar.h +++ b/modules/juce_gui_basics/widgets/juce_ProgressBar.h @@ -127,6 +127,8 @@ protected: void visibilityChanged() override; /** @internal */ void colourChanged() override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: double& progress; diff --git a/modules/juce_gui_basics/widgets/juce_Slider.cpp b/modules/juce_gui_basics/widgets/juce_Slider.cpp index 731193ff1b..9c6f8d0411 100644 --- a/modules/juce_gui_basics/widgets/juce_Slider.cpp +++ b/modules/juce_gui_basics/widgets/juce_Slider.cpp @@ -339,6 +339,9 @@ public: if (owner.onValueChange != nullptr) owner.onValueChange(); + + if (auto* handler = owner.getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::valueChanged); } void sendDragStart() @@ -465,8 +468,10 @@ public: if (style != newStyle) { style = newStyle; + owner.repaint(); owner.lookAndFeelChanged(); + owner.invalidateAccessibilityHandler(); } } @@ -567,6 +572,7 @@ public: owner.addAndMakeVisible (valueBox.get()); valueBox->setWantsKeyboardFocus (false); + valueBox->setAccessible (false); valueBox->setText (previousTextBoxContent, dontSendNotification); valueBox->setTooltip (owner.getTooltip()); updateTextBoxEnablement(); @@ -588,26 +594,24 @@ public: incButton.reset (lf.createSliderButton (owner, true)); decButton.reset (lf.createSliderButton (owner, false)); - owner.addAndMakeVisible (incButton.get()); - owner.addAndMakeVisible (decButton.get()); - - incButton->onClick = [this] { incrementOrDecrement (normRange.interval); }; - decButton->onClick = [this] { incrementOrDecrement (-normRange.interval); }; - - if (incDecButtonMode != incDecButtonsNotDraggable) - { - incButton->addMouseListener (&owner, false); - decButton->addMouseListener (&owner, false); - } - else - { - incButton->setRepeatSpeed (300, 100, 20); - decButton->setRepeatSpeed (300, 100, 20); - } - auto tooltip = owner.getTooltip(); - incButton->setTooltip (tooltip); - decButton->setTooltip (tooltip); + + auto setupButton = [&] (Button& b, bool isIncrement) + { + owner.addAndMakeVisible (b); + b.onClick = [&] { incrementOrDecrement (isIncrement ? normRange.interval : -normRange.interval); }; + + if (incDecButtonMode != incDecButtonsNotDraggable) + b.addMouseListener (&owner, false); + else + b.setRepeatSpeed (300, 100, 20); + + b.setTooltip (tooltip); + b.setAccessible (false); + }; + + setupButton (*incButton, true); + setupButton (*decButton, false); } else { @@ -1210,7 +1214,6 @@ public: } //============================================================================== - void resizeIncDecButtons() { auto buttonRect = sliderRect; @@ -1675,4 +1678,9 @@ void Slider::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel Component::mouseWheelMove (e, wheel); } +std::unique_ptr Slider::createAccessibilityHandler() +{ + return std::make_unique (*this); +} + } // namespace juce diff --git a/modules/juce_gui_basics/widgets/juce_Slider.h b/modules/juce_gui_basics/widgets/juce_Slider.h index 5b56793518..ae31701d91 100644 --- a/modules/juce_gui_basics/widgets/juce_Slider.h +++ b/modules/juce_gui_basics/widgets/juce_Slider.h @@ -967,6 +967,8 @@ public: void mouseExit (const MouseEvent&) override; /** @internal */ void mouseEnter (const MouseEvent&) override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: //============================================================================== diff --git a/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.cpp b/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.cpp index 1ef18057c5..82558580e2 100644 --- a/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.cpp +++ b/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.cpp @@ -896,4 +896,10 @@ void TableHeaderComponent::Listener::tableColumnDraggingChanged (TableHeaderComp { } +//============================================================================== +std::unique_ptr TableHeaderComponent::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::tableHeader); +} + } // namespace juce diff --git a/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.h b/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.h index 5cbf7d1c86..807f6576a9 100644 --- a/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.h +++ b/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.h @@ -416,6 +416,8 @@ public: void mouseUp (const MouseEvent&) override; /** @internal */ MouseCursor getMouseCursor() override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; /** Can be overridden for more control over the pop-up menu behaviour. */ virtual void showColumnChooserMenu (int columnIdClicked); diff --git a/modules/juce_gui_basics/widgets/juce_TableListBox.cpp b/modules/juce_gui_basics/widgets/juce_TableListBox.cpp index 6df5515896..c54539fc8b 100644 --- a/modules/juce_gui_basics/widgets/juce_TableListBox.cpp +++ b/modules/juce_gui_basics/widgets/juce_TableListBox.cpp @@ -30,7 +30,11 @@ class TableListBox::RowComp : public Component, public TooltipClient { public: - RowComp (TableListBox& tlb) noexcept : owner (tlb) {} + RowComp (TableListBox& tlb) noexcept + : owner (tlb) + { + setFocusContainerType (FocusContainerType::focusContainer); + } void paint (Graphics& g) override { @@ -46,7 +50,7 @@ public: { if (columnComponents[i] == nullptr) { - auto columnRect = headerComp.getColumnPosition(i).withHeight (getHeight()); + auto columnRect = headerComp.getColumnPosition (i).withHeight (getHeight()); if (columnRect.getX() >= clipBounds.getRight()) break; @@ -219,7 +223,78 @@ public: return columnComponents [owner.getHeader().getIndexOfColumnId (columnId, true)]; } -private: + std::unique_ptr createAccessibilityHandler() override + { + return std::make_unique (*this); + } + + //============================================================================== + class RowAccessibilityHandler : public AccessibilityHandler + { + public: + RowAccessibilityHandler (RowComp& rowComp) + : AccessibilityHandler (rowComp, + AccessibilityRole::row, + getListRowAccessibilityActions (*this, rowComp), + { std::make_unique (*this) }), + rowComponent (rowComp) + { + } + + String getTitle() const override + { + if (auto* m = rowComponent.owner.ListBox::model) + return m->getNameForRow (rowComponent.row); + + return {}; + } + + AccessibleState getCurrentState() const override + { + if (auto* m = rowComponent.owner.getModel()) + if (rowComponent.row >= m->getNumRows()) + return AccessibleState().withIgnored(); + + auto state = AccessibilityHandler::getCurrentState(); + + if (rowComponent.owner.multipleSelection) + state = state.withMultiSelectable(); + else + state = state.withSelectable(); + + if (rowComponent.isSelected) + return state.withSelected(); + + return state; + } + + class RowComponentCellInterface : public AccessibilityCellInterface + { + public: + RowComponentCellInterface (RowAccessibilityHandler& handler) + : owner (handler) + { + } + + int getColumnIndex() const override { return 0; } + int getColumnSpan() const override { return 1; } + + int getRowIndex() const override { return owner.rowComponent.row; } + int getRowSpan() const override { return 1; } + + int getDisclosureLevel() const override { return 0; } + + const AccessibilityHandler* getTableHandler() const override { return owner.rowComponent.owner.getAccessibilityHandler(); } + + private: + RowAccessibilityHandler& owner; + }; + + private: + RowComp& rowComponent; + }; + + //============================================================================== TableListBox& owner; OwnedArray columnComponents; int row = -1; @@ -467,6 +542,11 @@ void TableListBox::updateColumnComponents() const rowComp->resized(); } +std::unique_ptr TableListBox::createAccessibilityHandler() +{ + return std::make_unique (*this); +} + //============================================================================== void TableListBoxModel::cellClicked (int, int, const MouseEvent&) {} void TableListBoxModel::cellDoubleClicked (int, int, const MouseEvent&) {} diff --git a/modules/juce_gui_basics/widgets/juce_TableListBox.h b/modules/juce_gui_basics/widgets/juce_TableListBox.h index fe60687c6c..86f3b28710 100644 --- a/modules/juce_gui_basics/widgets/juce_TableListBox.h +++ b/modules/juce_gui_basics/widgets/juce_TableListBox.h @@ -332,7 +332,8 @@ public: void tableColumnDraggingChanged (TableHeaderComponent*, int) override; /** @internal */ void resized() override; - + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: //============================================================================== diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp index 199e8db084..aa98efd6e8 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp @@ -415,6 +415,8 @@ struct TextEditor::Iterator void beginNewLine() { + ++currentLineIndex; + lineY += lineHeight * lineSpacing; float lineWidth = 0; @@ -497,14 +499,6 @@ struct TextEditor::Iterator } } - void addSelection (RectangleList& area, Range selected) const - { - auto startX = indexToX (selected.getStart()); - auto endX = indexToX (selected.getEnd()); - - area.add (startX, lineY, endX - startX, lineHeight * lineSpacing); - } - void drawUnderline (Graphics& g, Range underline, Colour colour, AffineTransform transform) const { auto startX = roundToInt (indexToX (underline.getStart())); @@ -657,8 +651,36 @@ struct TextEditor::Iterator return roundToInt (maxWidth); } + std::vector> getLineRanges() + { + std::vector> ranges; + + int index = currentLineIndex; + Range currentLineRange; + + while (next()) + { + if (index < currentLineIndex) + { + currentLineRange.setEnd (indexInText - 1); + ranges.push_back (currentLineRange); + + currentLineRange = { indexInText, indexInText }; + index = currentLineIndex; + } + } + + currentLineRange.setEnd (atom != nullptr ? indexInText + atom->numChars + : indexInText); + + if (! currentLineRange.isEmpty()) + ranges.push_back (currentLineRange); + + return ranges; + } + //============================================================================== - int indexInText = 0; + int indexInText = 0, currentLineIndex = 0; float lineY = 0, lineHeight = 0, maxDescent = 0; float atomX = 0, atomRight = 0; const TextAtom* atom = nullptr; @@ -702,9 +724,14 @@ private: if (shouldStartNewLine) { if (split == numRemaining) + { beginNewLine(); + } else + { + ++currentLineIndex; lineY += lineHeight * lineSpacing; + } } atomRight = atomX + longAtom.width; @@ -1010,6 +1037,7 @@ void TextEditor::setReadOnly (bool shouldBeReadOnly) { readOnly = shouldBeReadOnly; enablementChanged(); + invalidateAccessibilityHandler(); } } @@ -1143,6 +1171,9 @@ void TextEditor::updateCaretPosition() Iterator i (*this); caret->setCaretPosition (getCaretRectangle().translated (leftIndent, topIndent + roundToInt (i.getYOffset()))); + + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::textSelectionChanged); } } @@ -1276,6 +1307,20 @@ void TextEditor::textChanged() valueTextNeedsUpdating = false; textValue = getText(); } + + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::textChanged); +} + +void TextEditor::setSelection (Range newSelection) noexcept +{ + if (newSelection != selection) + { + selection = newSelection; + + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::textSelectionChanged); + } } void TextEditor::returnPressed() { postCommandMessage (TextEditorDefs::returnKeyMessageId); } @@ -1358,6 +1403,9 @@ void TextEditor::moveCaret (int newCaretPos) scrollToMakeSureCursorIsVisible(); updateCaretPosition(); + + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::textChanged); } } @@ -1424,6 +1472,36 @@ Rectangle TextEditor::getCaretRectangleFloat() const return { anchor.x, anchor.y, 2.0f, cursorHeight }; } +RectangleList TextEditor::getTextBounds (Range textRange) +{ + RectangleList boundingBox; + + Iterator i (*this); + auto yOffset = i.getYOffset(); + + for (auto lineRange : i.getLineRanges()) + { + auto intersection = lineRange.getIntersectionWith (textRange); + + if (! intersection.isEmpty()) + { + Point anchorStart, anchorEnd; + float lineHeight = 0.0f; + + getCharPosition (intersection.getStart(), anchorStart, lineHeight); + getCharPosition (intersection.getEnd(), anchorEnd, lineHeight); + + boundingBox.add (Rectangle (anchorStart.x, anchorStart.y, anchorEnd.x - anchorStart.x, lineHeight).toNearestInt()); + + if (intersection == textRange) + break; + } + } + + boundingBox.offsetAll (getLeftIndent(), roundToInt ((float) getTopIndent() + yOffset)); + return boundingBox; +} + //============================================================================== // Extra space for the cursor at the right-hand-edge constexpr int rightEdgeSpace = 2; @@ -1548,14 +1626,14 @@ void TextEditor::moveCaretTo (const int newPosition, const bool isSelecting) if (getCaretPosition() >= selection.getEnd()) dragType = draggingSelectionEnd; - selection = Range::between (getCaretPosition(), selection.getEnd()); + setSelection (Range::between (getCaretPosition(), selection.getEnd())); } else { if (getCaretPosition() < selection.getStart()) dragType = draggingSelectionStart; - selection = Range::between (getCaretPosition(), selection.getStart()); + setSelection (Range::between (getCaretPosition(), selection.getStart())); } repaintText (selection.getUnionWith (oldSelection)); @@ -1567,11 +1645,11 @@ void TextEditor::moveCaretTo (const int newPosition, const bool isSelecting) repaintText (selection); moveCaret (newPosition); - selection = Range::emptyRange (getCaretPosition()); + setSelection (Range::emptyRange (getCaretPosition())); } } -int TextEditor::getTextIndexAt (const int x, const int y) +int TextEditor::getTextIndexAt (const int x, const int y) const { Iterator i (*this); @@ -1661,22 +1739,14 @@ void TextEditor::drawContent (Graphics& g) if (! selection.isEmpty()) { - Iterator i2 (i); - RectangleList selectionArea; - - while (i2.next() && i2.lineY < (float) clip.getBottom()) - { - if (i2.lineY + i2.lineHeight >= (float) clip.getY() - && selection.intersects ({ i2.indexInText, i2.indexInText + i2.atom->numChars })) - { - i2.addSelection (selectionArea, selection); - } - } - selectedTextColour = findColour (highlightedTextColourId); g.setColour (findColour (highlightColourId).withMultipliedAlpha (hasKeyboardFocus (true) ? 1.0f : 0.5f)); - g.fillPath (selectionArea.toPath(), transform); + + auto boundingBox = getTextBounds (selection); + boundingBox.offsetAll (-leftIndent, -roundToInt ((float) topIndent + yOffset)); + + g.fillPath (boundingBox.toPath(), transform); } const UniformTextSection* lastSection = nullptr; @@ -2019,7 +2089,7 @@ bool TextEditor::deleteBackwards (bool moveInWholeWordSteps) if (moveInWholeWordSteps) moveCaretTo (findWordBreakBefore (getCaretPosition()), true); else if (selection.isEmpty() && selection.getStart() > 0) - selection = { selection.getEnd() - 1, selection.getEnd() }; + setSelection ({ selection.getEnd() - 1, selection.getEnd() }); cut(); return true; @@ -2028,7 +2098,7 @@ bool TextEditor::deleteBackwards (bool moveInWholeWordSteps) bool TextEditor::deleteForwards (bool /*moveInWholeWordSteps*/) { if (selection.isEmpty() && selection.getStart() < getTotalNumChars()) - selection = { selection.getStart(), selection.getStart() + 1 }; + setSelection ({ selection.getStart(), selection.getStart() + 1 }); cut(); return true; @@ -2520,7 +2590,7 @@ void TextEditor::getCharPosition (int index, Point& anchor, float& lineHe } } -int TextEditor::indexAtPosition (const float x, const float y) +int TextEditor::indexAtPosition (const float x, const float y) const { if (getWordWrapWidth() > 0) { @@ -2616,4 +2686,10 @@ void TextEditor::coalesceSimilarSections() } } +//============================================================================== +std::unique_ptr TextEditor::createAccessibilityHandler() +{ + return std::make_unique (*this); +} + } // namespace juce diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.h b/modules/juce_gui_basics/widgets/juce_TextEditor.h index a34af63846..d4ed48e7b1 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.h +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.h @@ -464,7 +464,7 @@ public: /** Finds the index of the character at a given position. The coordinates are relative to the component's top-left. */ - int getTextIndexAt (int x, int y); + int getTextIndexAt (int x, int y) const; /** Counts the number of characters in the text. @@ -492,6 +492,16 @@ public: */ void setIndents (int newLeftIndent, int newTopIndent); + /** Returns the gap at the top edge of the editor. + @see setIndents + */ + int getTopIndent() const noexcept { return topIndent; } + + /** Returns the gap at the left edge of the editor. + @see setIndents + */ + int getLeftIndent() const noexcept { return leftIndent; } + /** Changes the size of border left around the edge of the component. @see getBorder */ @@ -524,6 +534,11 @@ public: /** Returns the current line spacing of the TextEditor. */ float getLineSpacing() const noexcept { return lineSpacing; } + /** Returns the bounding box for a range of text in the editor. As the range may span + multiple lines, this method returns a RectangleList. + */ + RectangleList getTextBounds (Range textRange); + //============================================================================== void moveCaretToEnd(); bool moveCaretLeft (bool moveInWholeWordSteps, bool selecting); @@ -699,6 +714,8 @@ public: void setTemporaryUnderlining (const Array>&) override; /** @internal */ VirtualKeyboardType getKeyboardType() override { return keyboardType; } + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; protected: //============================================================================== @@ -791,7 +808,7 @@ private: void updateCaretPosition(); void updateValueFromText(); void textWasChangedByValue(); - int indexAtPosition (float x, float y); + int indexAtPosition (float x, float y) const; int findWordBreakAfter (int position) const; int findWordBreakBefore (int position) const; bool moveCaretWithTransaction (int newPos, bool selecting); @@ -806,6 +823,7 @@ private: void scrollByLines (int deltaLines); bool undoOrRedo (bool shouldUndo); UndoManager* getUndoManager() noexcept; + void setSelection (Range) noexcept; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TextEditor) }; diff --git a/modules/juce_gui_basics/widgets/juce_Toolbar.cpp b/modules/juce_gui_basics/widgets/juce_Toolbar.cpp index d16b0c176a..1b9cb89711 100644 --- a/modules/juce_gui_basics/widgets/juce_Toolbar.cpp +++ b/modules/juce_gui_basics/widgets/juce_Toolbar.cpp @@ -809,4 +809,10 @@ void Toolbar::showCustomisationDialog (ToolbarItemFactory& factory, const int op ->enterModalState (true, nullptr, true); } +//============================================================================== +std::unique_ptr Toolbar::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::group); +} + } // namespace juce diff --git a/modules/juce_gui_basics/widgets/juce_Toolbar.h b/modules/juce_gui_basics/widgets/juce_Toolbar.h index dd096ac53b..a5f4ddc07c 100644 --- a/modules/juce_gui_basics/widgets/juce_Toolbar.h +++ b/modules/juce_gui_basics/widgets/juce_Toolbar.h @@ -309,6 +309,8 @@ public: /** @internal */ static ToolbarItemComponent* createItem (ToolbarItemFactory&, int itemId); /** @internal */ + std::unique_ptr createAccessibilityHandler() override; + /** @internal */ static const char* const toolbarDragDescriptor; private: diff --git a/modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp b/modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp index 2026a115f9..8bf56b1f4c 100644 --- a/modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp +++ b/modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp @@ -239,4 +239,15 @@ void ToolbarItemComponent::setEditingMode (const ToolbarEditingMode newMode) } } +//============================================================================== +std::unique_ptr ToolbarItemComponent::createAccessibilityHandler() +{ + const auto shouldItemBeAccessible = (itemId != ToolbarItemFactory::separatorBarId + && itemId != ToolbarItemFactory::spacerId + && itemId != ToolbarItemFactory::flexibleSpacerId); + + return shouldItemBeAccessible ? std::make_unique (*this) + : nullptr; +} + } // namespace juce diff --git a/modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.h b/modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.h index 5be4e85c43..fe638c0951 100644 --- a/modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.h +++ b/modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.h @@ -187,6 +187,8 @@ public: void paintButton (Graphics&, bool isMouseOver, bool isMouseDown) override; /** @internal */ void resized() override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: friend class Toolbar; diff --git a/modules/juce_gui_basics/widgets/juce_ToolbarItemPalette.cpp b/modules/juce_gui_basics/widgets/juce_ToolbarItemPalette.cpp index d4c36fc6be..8f2bd6065b 100644 --- a/modules/juce_gui_basics/widgets/juce_ToolbarItemPalette.cpp +++ b/modules/juce_gui_basics/widgets/juce_ToolbarItemPalette.cpp @@ -107,4 +107,10 @@ void ToolbarItemPalette::resized() itemHolder->setSize (maxX, y + height + 8); } +//============================================================================== +std::unique_ptr ToolbarItemPalette::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::group); +} + } // namespace juce diff --git a/modules/juce_gui_basics/widgets/juce_ToolbarItemPalette.h b/modules/juce_gui_basics/widgets/juce_ToolbarItemPalette.h index c029ed2e01..652b638436 100644 --- a/modules/juce_gui_basics/widgets/juce_ToolbarItemPalette.h +++ b/modules/juce_gui_basics/widgets/juce_ToolbarItemPalette.h @@ -60,6 +60,8 @@ public: //============================================================================== /** @internal */ void resized() override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: ToolbarItemFactory& factory; diff --git a/modules/juce_gui_basics/widgets/juce_TreeView.cpp b/modules/juce_gui_basics/widgets/juce_TreeView.cpp index d439122283..d83d8a425b 100644 --- a/modules/juce_gui_basics/widgets/juce_TreeView.cpp +++ b/modules/juce_gui_basics/widgets/juce_TreeView.cpp @@ -26,6 +26,210 @@ namespace juce { +static int getItemDepth (const TreeViewItem* item) +{ + if (item == nullptr || item->getOwnerView() == nullptr) + return 0; + + auto depth = item->getOwnerView()->isRootItemVisible() ? 0 : -1; + + for (auto* parent = item->getParentItem(); parent != nullptr; parent = parent->getParentItem()) + ++depth; + + return depth; +} + +//============================================================================== +class TreeView::ItemComponent : public Component +{ +public: + explicit ItemComponent (TreeViewItem& itemToRepresent) + : item (itemToRepresent), + customComponent (item.createItemComponent()) + { + if (hasCustomComponent()) + addAndMakeVisible (*customComponent); + } + + void paint (Graphics& g) override + { + item.draw (g, getWidth(), mouseIsOverButton); + } + + void resized() override + { + if (hasCustomComponent()) + { + auto itemPosition = item.getItemPosition (false); + + customComponent->setBounds (getLocalBounds().withX (itemPosition.getX()) + .withWidth (itemPosition.getWidth())); + } + } + + std::unique_ptr createAccessibilityHandler() override + { + if (hasCustomComponent() && customComponent->getAccessibilityHandler() != nullptr) + return nullptr; + + return std::make_unique (*this); + } + + void setMouseIsOverButton (bool isOver) { mouseIsOverButton = isOver; } + TreeViewItem& getRepresentedItem() const noexcept { return item; } + +private: + //============================================================================== + class ItemAccessibilityHandler : public AccessibilityHandler + { + public: + explicit ItemAccessibilityHandler (ItemComponent& comp) + : AccessibilityHandler (comp, + AccessibilityRole::treeItem, + getAccessibilityActions (comp), + { std::make_unique (comp) }), + itemComponent (comp) + { + } + + String getTitle() const override + { + return itemComponent.getRepresentedItem().getAccessibilityName(); + } + + AccessibleState getCurrentState() const override + { + auto& treeItem = itemComponent.getRepresentedItem(); + + auto state = AccessibilityHandler::getCurrentState().withAccessibleOffscreen(); + + if (auto* tree = treeItem.getOwnerView()) + { + if (tree->isMultiSelectEnabled()) + state = state.withMultiSelectable(); + else + state = state.withSelectable(); + } + + if (treeItem.mightContainSubItems()) + state = state.withExpandable(); + + if (treeItem.isOpen()) + state = state.withExpanded(); + else + state = state.withCollapsed(); + + if (treeItem.isSelected()) + state = state.withSelected(); + + return state; + } + + class ItemCellInterface : public AccessibilityCellInterface + { + public: + explicit ItemCellInterface (ItemComponent& c) : itemComponent (c) {} + + int getColumnIndex() const override { return 0; } + int getColumnSpan() const override { return 1; } + + int getRowIndex() const override + { + return itemComponent.getRepresentedItem().getRowNumberInTree(); + } + + int getRowSpan() const override + { + return 1; + } + + int getDisclosureLevel() const override + { + return getItemDepth (&itemComponent.getRepresentedItem()); + } + + const AccessibilityHandler* getTableHandler() const override + { + if (auto* tree = itemComponent.getRepresentedItem().getOwnerView()) + return tree->getAccessibilityHandler(); + + return nullptr; + } + + private: + ItemComponent& itemComponent; + }; + + private: + static AccessibilityActions getAccessibilityActions (ItemComponent& itemComponent) + { + auto onFocus = [&itemComponent] + { + auto& treeItem = itemComponent.getRepresentedItem(); + + if (auto* tree = treeItem.getOwnerView()) + tree->scrollToKeepItemVisible (&treeItem); + }; + + auto onPress = [&itemComponent] + { + itemComponent.getRepresentedItem().itemClicked (generateMouseEvent (itemComponent, { ModifierKeys::leftButtonModifier })); + }; + + auto onShowMenu = [&itemComponent] + { + itemComponent.getRepresentedItem().itemClicked (generateMouseEvent (itemComponent, { ModifierKeys::popupMenuClickModifier })); + }; + + auto onToggle = [&itemComponent, onFocus] + { + if (auto* handler = itemComponent.getAccessibilityHandler()) + { + auto isSelected = handler->getCurrentState().isSelected(); + + if (! isSelected) + onFocus(); + + itemComponent.getRepresentedItem().setSelected (! isSelected, true); + } + }; + + auto actions = AccessibilityActions().addAction (AccessibilityActionType::focus, std::move (onFocus)) + .addAction (AccessibilityActionType::press, std::move (onPress)) + .addAction (AccessibilityActionType::showMenu, std::move (onShowMenu)) + .addAction (AccessibilityActionType::toggle, std::move (onToggle)); + + return actions; + } + + ItemComponent& itemComponent; + + static MouseEvent generateMouseEvent (ItemComponent& itemComp, ModifierKeys mods) + { + auto topLeft = itemComp.getRepresentedItem().getItemPosition (false).toFloat().getTopLeft(); + + return { Desktop::getInstance().getMainMouseSource(), topLeft, mods, + MouseInputSource::invalidPressure, MouseInputSource::invalidOrientation, MouseInputSource::invalidRotation, + MouseInputSource::invalidTiltX, MouseInputSource::invalidTiltY, + &itemComp, &itemComp, Time::getCurrentTime(), topLeft, Time::getCurrentTime(), 0, false }; + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ItemAccessibilityHandler) + }; + + //============================================================================== + bool hasCustomComponent() const noexcept { return customComponent.get() != nullptr; } + + TreeViewItem& item; + std::unique_ptr customComponent; + + bool mouseIsOverButton = false; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ItemComponent) +}; + +//============================================================================== class TreeView::ContentComponent : public Component, public TooltipClient, public AsyncUpdater @@ -33,72 +237,167 @@ class TreeView::ContentComponent : public Component, public: ContentComponent (TreeView& tree) : owner (tree) { + setAccessible (false); } - void mouseDown (const MouseEvent& e) override + //============================================================================== + void resized() override { - updateButtonUnderMouse (e); + triggerAsyncUpdate(); + } - isDragging = false; - needSelectionOnMouseUp = false; - Rectangle pos; + String getTooltip() override + { + if (auto* itemComponent = getItemComponentAt (getMouseXYRelative())) + return itemComponent->getRepresentedItem().getTooltip(); - if (auto* item = findItemAt (e.y, pos)) + return owner.getTooltip(); + } + + void mouseDown (const MouseEvent& e) override { mouseDownInternal (e.getEventRelativeTo (this)); } + void mouseUp (const MouseEvent& e) override { mouseUpInternal (e.getEventRelativeTo (this)); } + void mouseDoubleClick (const MouseEvent& e) override { mouseDoubleClickInternal (e.getEventRelativeTo (this));} + void mouseDrag (const MouseEvent& e) override { mouseDragInternal (e.getEventRelativeTo (this));} + void mouseMove (const MouseEvent& e) override { mouseMoveInternal (e.getEventRelativeTo (this)); } + void mouseExit (const MouseEvent& e) override { mouseExitInternal (e.getEventRelativeTo (this)); } + + //============================================================================== + ItemComponent* getItemComponentAt (Point p) + { + auto iter = std::find_if (itemComponents.cbegin(), itemComponents.cend(), + [p] (const std::unique_ptr& c) { return c->getBounds().contains (p); }); + + if (iter != itemComponents.cend()) + return iter->get(); + + return nullptr; + } + + ItemComponent* getComponentForItem (const TreeViewItem* item) const + { + if (item != nullptr) { - if (isEnabled()) + auto iter = std::find_if (itemComponents.cbegin(), itemComponents.cend(), + [item] (const std::unique_ptr& c) + { + return &c->getRepresentedItem() == item; + }); + + if (iter != itemComponents.cend()) + return iter->get(); + } + + return nullptr; + } + + void updateComponents() + { + std::vector componentsToKeep; + + for (auto* treeItem : getAllVisibleItems()) + { + if (auto* itemComp = getComponentForItem (treeItem)) { - // (if the open/close buttons are hidden, we'll treat clicks to the left of the item - // as selection clicks) - if (e.x < pos.getX() && owner.openCloseButtonsVisible) - { - if (e.x >= pos.getX() - owner.getIndentSize()) - item->setOpen (! item->isOpen()); + componentsToKeep.push_back (itemComp); + } + else + { + auto newComp = std::make_unique (*treeItem); - // (clicks to the left of an open/close button are ignored) - } + addAndMakeVisible (*newComp); + newComp->addMouseListener (this, true); + componentsToKeep.push_back (newComp.get()); + + itemComponents.push_back (std::move (newComp)); + } + } + + for (int i = (int) itemComponents.size(); --i >= 0;) + { + auto& comp = itemComponents[(size_t) i]; + + if (std::find (componentsToKeep.cbegin(), componentsToKeep.cend(), comp.get()) + != componentsToKeep.cend()) + { + auto& treeItem = comp->getRepresentedItem(); + comp->setBounds ({ 0, treeItem.y, getWidth(), treeItem.itemHeight }); + } + else + { + if (isMouseDraggingInChildComp (*comp)) + comp->setSize (0, 0); else - { - // mouse-down inside the body of the item.. - if (! owner.isMultiSelectEnabled()) - item->setSelected (true, true); - else if (item->isSelected()) - needSelectionOnMouseUp = ! e.mods.isPopupMenu(); - else - selectBasedOnModifiers (item, e.mods); - - if (e.x >= pos.getX()) - item->itemClicked (e.withNewPosition (e.position - pos.getPosition().toFloat())); - } + itemComponents.erase (itemComponents.begin() + i); } } } - void mouseUp (const MouseEvent& e) override +private: + //============================================================================== + void mouseDownInternal (const MouseEvent& e) { - updateButtonUnderMouse (e); + updateItemUnderMouse (e); - if (needSelectionOnMouseUp && e.mouseWasClicked() && isEnabled()) + isDragging = false; + needSelectionOnMouseUp = false; + + if (! isEnabled()) + return; + + if (auto* itemComponent = getItemComponentAt (e.getPosition())) { - Rectangle pos; + auto& item = itemComponent->getRepresentedItem(); + auto pos = item.getItemPosition (false); - if (auto* item = findItemAt (e.y, pos)) - selectBasedOnModifiers (item, e.mods); + // (if the open/close buttons are hidden, we'll treat clicks to the left of the item + // as selection clicks) + if (e.x < pos.getX() && owner.openCloseButtonsVisible) + { + // (clicks to the left of an open/close button are ignored) + if (e.x >= pos.getX() - owner.getIndentSize()) + item.setOpen (! item.isOpen()); + } + else + { + // mouse-down inside the body of the item.. + if (! owner.isMultiSelectEnabled()) + item.setSelected (true, true); + else if (item.isSelected()) + needSelectionOnMouseUp = ! e.mods.isPopupMenu(); + else + selectBasedOnModifiers (item, e.mods); + + if (e.x >= pos.getX()) + item.itemClicked (e.withNewPosition (e.position - pos.getPosition().toFloat())); + } } } - void mouseDoubleClick (const MouseEvent& e) override + void mouseUpInternal (const MouseEvent& e) { - if (e.getNumberOfClicks() != 3 && isEnabled()) // ignore triple clicks - { - Rectangle pos; + updateItemUnderMouse (e); + + if (isEnabled() && needSelectionOnMouseUp && e.mouseWasClicked()) + if (auto* itemComponent = getItemComponentAt (e.getPosition())) + selectBasedOnModifiers (itemComponent->getRepresentedItem(), e.mods); + } + + void mouseDoubleClickInternal (const MouseEvent& e) + { + if (isEnabled() && e.getNumberOfClicks() != 3) // ignore triple clicks + { + if (auto* itemComponent = getItemComponentAt (e.getPosition())) + { + auto& item = itemComponent->getRepresentedItem(); + auto pos = item.getItemPosition (false); - if (auto* item = findItemAt (e.y, pos)) if (e.x >= pos.getX() || ! owner.openCloseButtonsVisible) - item->itemDoubleClicked (e.withNewPosition (e.position - pos.getPosition().toFloat())); + item.itemDoubleClicked (e.withNewPosition (e.position - pos.getPosition().toFloat())); + } } } - void mouseDrag (const MouseEvent& e) override + void mouseDragInternal (const MouseEvent& e) { if (isEnabled() && ! (isDragging || e.mouseWasClicked() @@ -106,20 +405,22 @@ public: || e.mods.isPopupMenu())) { isDragging = true; - Rectangle pos; - if (auto* item = findItemAt (e.getMouseDownY(), pos)) + if (auto* itemComponent = getItemComponentAt (e.getMouseDownPosition())) { + auto& item = itemComponent->getRepresentedItem(); + auto pos = item.getItemPosition (false); + if (e.getMouseDownX() >= pos.getX()) { - auto dragDescription = item->getDragSourceDescription(); + auto dragDescription = item.getDragSourceDescription(); if (! (dragDescription.isVoid() || (dragDescription.isString() && dragDescription.toString().isEmpty()))) { if (auto* dragContainer = DragAndDropContainer::findParentDragContainerFor (this)) { - pos.setSize (pos.getWidth(), item->itemHeight); - Image dragImage (Component::createComponentSnapshot (pos, true)); + pos.setSize (pos.getWidth(), item.itemHeight); + auto dragImage = Component::createComponentSnapshot (pos, true); dragImage.multiplyAllAlphas (0.6f); auto imageOffset = pos.getPosition() - e.getPosition(); @@ -137,152 +438,64 @@ public: } } - void mouseMove (const MouseEvent& e) override { updateButtonUnderMouse (e); } - void mouseExit (const MouseEvent& e) override { updateButtonUnderMouse (e); } + void mouseMoveInternal (const MouseEvent& e) { updateItemUnderMouse (e); } + void mouseExitInternal (const MouseEvent& e) { updateItemUnderMouse (e); } - void paint (Graphics& g) override + bool isMouseDraggingInChildComp (const Component& comp) const { - if (owner.rootItem != nullptr) - { - owner.recalculateIfNeeded(); + for (auto& ms : Desktop::getInstance().getMouseSources()) + if (ms.isDragging()) + if (auto* underMouse = ms.getComponentUnderMouse()) + return (&comp == underMouse || comp.isParentOf (underMouse)); - if (! owner.rootItemVisible) - g.setOrigin (0, -owner.rootItem->itemHeight); - - owner.rootItem->paintRecursively (g, getWidth()); - } + return false; } - TreeViewItem* findItemAt (int y, Rectangle& itemPosition) const + void updateItemUnderMouse (const MouseEvent& e) { - if (owner.rootItem != nullptr) + ItemComponent* newItem = nullptr; + + if (owner.openCloseButtonsVisible) { - owner.recalculateIfNeeded(); - - if (! owner.rootItemVisible) - y += owner.rootItem->itemHeight; - - if (auto* ti = owner.rootItem->findItemRecursively (y)) + if (auto* itemComponent = getItemComponentAt (e.getPosition())) { - itemPosition = ti->getItemPosition (false); - return ti; - } - } + auto& item = itemComponent->getRepresentedItem(); + auto pos = item.getItemPosition (false); - return nullptr; - } - - void updateComponents() - { - auto visibleTop = -getY(); - auto visibleBottom = visibleTop + getParentHeight(); - - for (auto* i : items) - i->shouldKeep = false; - - { - auto* item = owner.rootItem; - int y = (item != nullptr && ! owner.rootItemVisible) ? -item->itemHeight : 0; - - while (item != nullptr && y < visibleBottom) - { - y += item->itemHeight; - - if (y >= visibleTop) + if (e.x < pos.getX() + && e.x >= pos.getX() - owner.getIndentSize() + && item.mightContainSubItems()) { - if (auto* ri = findItem (item->uid)) - { - ri->shouldKeep = true; - } - else if (auto* comp = item->createItemComponent()) - { - items.add (new RowItem (item, comp, item->uid)); - addAndMakeVisible (comp); - } - } - - item = item->getNextVisibleItem (true); - } - } - - for (int i = items.size(); --i >= 0;) - { - auto* ri = items.getUnchecked(i); - bool keep = false; - - if (isParentOf (ri->component)) - { - if (ri->shouldKeep) - { - auto pos = ri->item->getItemPosition (false); - pos.setSize (pos.getWidth(), ri->item->itemHeight); - - if (pos.getBottom() >= visibleTop && pos.getY() < visibleBottom) - { - keep = true; - ri->component->setBounds (pos); - } - } - - if ((! keep) && isMouseDraggingInChildCompOf (ri->component)) - { - keep = true; - ri->component->setSize (0, 0); + newItem = itemComponent; } } + } - if (! keep) - items.remove (i); + if (itemUnderMouse != newItem) + { + auto updateItem = [] (ItemComponent* itemComp, bool isMouseOverButton) + { + if (itemComp != nullptr) + { + itemComp->setMouseIsOverButton (isMouseOverButton); + itemComp->repaint(); + } + }; + + updateItem (itemUnderMouse, false); + updateItem (newItem, true); + + itemUnderMouse = newItem; } } - bool isMouseOverButton (TreeViewItem* item) const noexcept + void handleAsyncUpdate() override { - return item == buttonUnderMouse; + owner.updateVisibleItems(); } - void resized() override - { - owner.itemsChanged(); - } - - String getTooltip() override - { - Rectangle pos; - - if (auto* item = findItemAt (getMouseXYRelative().y, pos)) - return item->getTooltip(); - - return owner.getTooltip(); - } - -private: //============================================================================== - TreeView& owner; - - struct RowItem - { - RowItem (TreeViewItem* it, Component* c, int itemUID) - : component (c), item (it), uid (itemUID) - { - } - - ~RowItem() - { - delete component.get(); - } - - WeakReference component; - TreeViewItem* item; - int uid; - bool shouldKeep = true; - }; - - OwnedArray items; - TreeViewItem* buttonUnderMouse = nullptr; - bool isDragging = false, needSelectionOnMouseUp = false; - - void selectBasedOnModifiers (TreeViewItem* const item, const ModifierKeys modifiers) + void selectBasedOnModifiers (TreeViewItem& item, const ModifierKeys modifiers) { TreeViewItem* firstSelected = nullptr; @@ -297,7 +510,7 @@ private: if (rowStart > rowEnd) std::swap (rowStart, rowEnd); - auto ourRow = item->getRowNumberInTree(); + auto ourRow = item.getRowNumberInTree(); auto otherEnd = ourRow < rowEnd ? rowStart : rowEnd; if (ourRow > otherEnd) @@ -308,81 +521,78 @@ private: } else { - const bool cmd = modifiers.isCommandDown(); - item->setSelected ((! cmd) || ! item->isSelected(), ! cmd); + const auto cmd = modifiers.isCommandDown(); + item.setSelected ((! cmd) || ! item.isSelected(), ! cmd); } } - bool containsItem (TreeViewItem* const item) const noexcept + static TreeViewItem* getNextVisibleItem (TreeViewItem* item, bool forwards) { - for (auto* i : items) - if (i->item == item) - return true; + if (item == nullptr || item->ownerView == nullptr) + return nullptr; - return false; + auto* nextItem = item->ownerView->getItemOnRow (item->getRowNumberInTree() + (forwards ? 1 : -1)); + + return nextItem == item->ownerView->rootItem && ! item->ownerView->rootItemVisible ? nullptr + : nextItem; } - RowItem* findItem (const int uid) const noexcept + std::vector getAllVisibleItems() const { - for (auto* i : items) - if (i->uid == uid) - return i; + if (owner.rootItem == nullptr) + return {}; - return nullptr; - } + const auto visibleTop = -getY(); + const auto visibleBottom = visibleTop + getParentHeight(); - void updateButtonUnderMouse (const MouseEvent& e) - { - TreeViewItem* newItem = nullptr; + std::vector visibleItems; - if (owner.openCloseButtonsVisible) + auto* item = [&] { - Rectangle pos; + auto* i = owner.rootItemVisible ? owner.rootItem + : owner.rootItem->subItems.getFirst(); - if (auto* item = findItemAt (e.y, pos)) + while (i != nullptr && i->y < visibleTop) + i = getNextVisibleItem (i, true); + + return i; + }(); + + auto addOffscreenItemBuffer = [&visibleItems] (TreeViewItem* i, int num, bool forwards) + { + while (--num >= 0) { - if (e.x < pos.getX() && e.x >= pos.getX() - owner.getIndentSize()) - { - newItem = item; + i = getNextVisibleItem (i, forwards); - if (! newItem->mightContainSubItems()) - newItem = nullptr; - } + if (i == nullptr) + return; + + visibleItems.push_back (i); } - } + }; - if (buttonUnderMouse != newItem) + addOffscreenItemBuffer (item, 2, false); + + while (item != nullptr && item->y < visibleBottom) { - repaintButtonUnderMouse(); - buttonUnderMouse = newItem; - repaintButtonUnderMouse(); + visibleItems.push_back (item); + item = getNextVisibleItem (item, true); } + + if (item != nullptr) + visibleItems.push_back (item); + + addOffscreenItemBuffer (item, 2, true); + + return visibleItems; } - void repaintButtonUnderMouse() - { - if (buttonUnderMouse != nullptr && containsItem (buttonUnderMouse)) - { - auto r = buttonUnderMouse->getItemPosition (false); - repaint (0, r.getY(), r.getX(), buttonUnderMouse->getItemHeight()); - } - } + //============================================================================== + TreeView& owner; - static bool isMouseDraggingInChildCompOf (Component* const comp) - { - for (auto& ms : Desktop::getInstance().getMouseSources()) - if (ms.isDragging()) - if (auto* underMouse = ms.getComponentUnderMouse()) - if (comp == underMouse || comp->isParentOf (underMouse)) - return true; - - return false; - } - - void handleAsyncUpdate() override - { - owner.recalculateIfNeeded(); - } + std::vector> itemComponents; + ItemComponent* itemUnderMouse = nullptr; + bool isDragging = false, needSelectionOnMouseUp = false; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ContentComponent) }; @@ -391,9 +601,9 @@ private: class TreeView::TreeViewport : public Viewport { public: - TreeViewport() noexcept {} + TreeViewport() = default; - void updateComponents (const bool triggerResize) + void updateComponents (bool triggerResize) { if (auto* tvc = getContentComp()) { @@ -408,7 +618,7 @@ public: void visibleAreaChanged (const Rectangle& newVisibleArea) override { - const bool hasScrolledSideways = (newVisibleArea.getX() != lastX); + const auto hasScrolledSideways = (newVisibleArea.getX() != lastX); lastX = newVisibleArea.getX(); updateComponents (hasScrolledSideways); } @@ -433,15 +643,16 @@ private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TreeViewport) }; - //============================================================================== -TreeView::TreeView (const String& name) - : Component (name), - viewport (new TreeViewport()) +TreeView::TreeView (const String& name) : Component (name) { + viewport = std::make_unique(); + viewport->setAccessible (false); addAndMakeVisible (viewport.get()); viewport->setViewedComponent (new ContentComponent (*this)); + setWantsKeyboardFocus (true); + setFocusContainerType (FocusContainerType::focusContainer); } TreeView::~TreeView() @@ -456,7 +667,8 @@ void TreeView::setRootItem (TreeViewItem* const newRootItem) { if (newRootItem != nullptr) { - jassert (newRootItem->ownerView == nullptr); // can't use a tree item in more than one tree at once.. + // can't use a tree item in more than one tree at once.. + jassert (newRootItem->ownerView == nullptr); if (newRootItem->ownerView != nullptr) newRootItem->ownerView->setRootItem (nullptr); @@ -470,14 +682,13 @@ void TreeView::setRootItem (TreeViewItem* const newRootItem) if (newRootItem != nullptr) newRootItem->setOwnerView (this); - needsRecalculating = true; - recalculateIfNeeded(); - if (rootItem != nullptr && (defaultOpenness || ! rootItemVisible)) { rootItem->setOpen (false); // force a re-open rootItem->setOpen (true); } + + updateVisibleItems(); } } @@ -497,7 +708,7 @@ void TreeView::setRootItemVisible (const bool shouldBeVisible) rootItem->setOpen (true); } - itemsChanged(); + updateVisibleItems(); } void TreeView::colourChanged() @@ -526,7 +737,7 @@ void TreeView::setDefaultOpenness (const bool isOpenByDefault) if (defaultOpenness != isOpenByDefault) { defaultOpenness = isOpenByDefault; - itemsChanged(); + updateVisibleItems(); } } @@ -540,7 +751,7 @@ void TreeView::setOpenCloseButtonsVisible (const bool shouldBeVisible) if (openCloseButtonsVisible != shouldBeVisible) { openCloseButtonsVisible = shouldBeVisible; - itemsChanged(); + updateVisibleItems(); } } @@ -584,9 +795,10 @@ TreeViewItem* TreeView::getItemOnRow (int index) const TreeViewItem* TreeView::getItemAt (int y) const noexcept { - auto tc = viewport->getContentComp(); - Rectangle pos; - return tc->findItemAt (tc->getLocalPoint (this, Point (0, y)).y, pos); + if (auto* itemComponent = viewport->getContentComp()->getItemComponentAt (Point (0, y))) + return &itemComponent->getRepresentedItem(); + + return nullptr; } TreeViewItem* TreeView::findItemFromIdentifierString (const String& identifierString) const @@ -597,6 +809,11 @@ TreeViewItem* TreeView::findItemFromIdentifierString (const String& identifierSt return rootItem->findItemFromIdentifierString (identifierString); } +Component* TreeView::getItemComponent (const TreeViewItem* item) const +{ + return viewport->getContentComp()->getComponentForItem (item); +} + //============================================================================== static void addAllSelectedItemIds (TreeViewItem* item, XmlElement& parent) { @@ -606,38 +823,32 @@ static void addAllSelectedItemIds (TreeViewItem* item, XmlElement& parent) auto numSubItems = item->getNumSubItems(); for (int i = 0; i < numSubItems; ++i) - addAllSelectedItemIds (item->getSubItem(i), parent); + addAllSelectedItemIds (item->getSubItem (i), parent); } std::unique_ptr TreeView::getOpennessState (bool alsoIncludeScrollPosition) const { - std::unique_ptr e; - if (rootItem != nullptr) { - e.reset (rootItem->getOpennessState (false)); - - if (e != nullptr) + if (auto rootOpenness = rootItem->getOpennessState (false)) { if (alsoIncludeScrollPosition) - e->setAttribute ("scrollPos", viewport->getViewPositionY()); + rootOpenness->setAttribute ("scrollPos", viewport->getViewPositionY()); - addAllSelectedItemIds (rootItem, *e); + addAllSelectedItemIds (rootItem, *rootOpenness); + return rootOpenness; } } - return e; + return {}; } -void TreeView::restoreOpennessState (const XmlElement& newState, const bool restoreStoredSelection) +void TreeView::restoreOpennessState (const XmlElement& newState, bool restoreStoredSelection) { if (rootItem != nullptr) { rootItem->restoreOpennessState (newState); - needsRecalculating = true; - recalculateIfNeeded(); - if (newState.hasAttribute ("scrollPos")) viewport->setViewPosition (viewport->getViewPositionX(), newState.getIntAttribute ("scrollPos")); @@ -650,6 +861,8 @@ void TreeView::restoreOpennessState (const XmlElement& newState, const bool rest if (auto* item = rootItem->findItemFromIdentifierString (e->getStringAttribute ("id"))) item->setSelected (true, false); } + + updateVisibleItems(); } } @@ -662,9 +875,7 @@ void TreeView::paint (Graphics& g) void TreeView::resized() { viewport->setBounds (getLocalBounds()); - - itemsChanged(); - recalculateIfNeeded(); + updateVisibleItems(); } void TreeView::enablementChanged() @@ -672,7 +883,7 @@ void TreeView::enablementChanged() repaint(); } -void TreeView::moveSelectedRow (const int delta) +void TreeView::moveSelectedRow (int delta) { auto numRowsInTree = getNumRowsInTree(); @@ -717,7 +928,7 @@ void TreeView::scrollToKeepItemVisible (TreeViewItem* item) { if (item != nullptr && item->ownerView == this) { - recalculateIfNeeded(); + updateVisibleItems(); item = item->getDeepestOpenParentItem(); @@ -834,37 +1045,22 @@ bool TreeView::keyPressed (const KeyPress& key) return false; } -void TreeView::itemsChanged() noexcept +void TreeView::updateVisibleItems() { - needsRecalculating = true; - repaint(); - viewport->getContentComp()->triggerAsyncUpdate(); -} - -void TreeView::recalculateIfNeeded() -{ - if (needsRecalculating) + if (rootItem != nullptr) { - needsRecalculating = false; + rootItem->updatePositions (rootItemVisible ? 0 : -rootItem->itemHeight); - const ScopedLock sl (nodeAlterationLock); - - if (rootItem != nullptr) - rootItem->updatePositions (rootItemVisible ? 0 : -rootItem->itemHeight); - - viewport->updateComponents (false); - - if (rootItem != nullptr) - { - viewport->getViewedComponent() - ->setSize (jmax (viewport->getMaximumVisibleWidth(), rootItem->totalWidth + 50), - rootItem->totalHeight - (rootItemVisible ? 0 : rootItem->itemHeight)); - } - else - { - viewport->getViewedComponent()->setSize (0, 0); - } + viewport->getViewedComponent() + ->setSize (jmax (viewport->getMaximumVisibleWidth(), rootItem->totalWidth + 50), + rootItem->totalHeight - (rootItemVisible ? 0 : rootItem->itemHeight)); } + else + { + viewport->getViewedComponent()->setSize (0, 0); + } + + viewport->updateComponents (false); } //============================================================================== @@ -1007,8 +1203,8 @@ void TreeView::showDragHighlight (const InsertPoint& insertPos) noexcept if (dragInsertPointHighlight == nullptr) { - dragInsertPointHighlight.reset (new InsertPointHighlight()); - dragTargetGroupHighlight.reset (new TargetGroupHighlight()); + dragInsertPointHighlight = std::make_unique(); + dragTargetGroupHighlight = std::make_unique(); addAndMakeVisible (dragInsertPointHighlight.get()); addAndMakeVisible (dragTargetGroupHighlight.get()); @@ -1020,13 +1216,13 @@ void TreeView::showDragHighlight (const InsertPoint& insertPos) noexcept void TreeView::hideDragHighlight() noexcept { - dragInsertPointHighlight.reset(); - dragTargetGroupHighlight.reset(); + dragInsertPointHighlight = nullptr; + dragTargetGroupHighlight = nullptr; } void TreeView::handleDrag (const StringArray& files, const SourceDetails& dragSourceDetails) { - const bool scrolled = viewport->autoScroll (dragSourceDetails.localPosition.x, + const auto scrolled = viewport->autoScroll (dragSourceDetails.localPosition.x, dragSourceDetails.localPosition.y, 20, 10); InsertPoint insertPos (*this, files, dragSourceDetails); @@ -1125,24 +1321,19 @@ void TreeView::itemDropped (const SourceDetails& dragSourceDetails) handleDrop (StringArray(), dragSourceDetails); } +//============================================================================== +std::unique_ptr TreeView::createAccessibilityHandler() +{ + return std::make_unique (*this); +} + //============================================================================== TreeViewItem::TreeViewItem() - : selected (false), - redrawNeeded (true), - drawLinesInside (false), - drawLinesSet (false), - drawsInLeftMargin (false), - drawsInRightMargin (false), - openness (opennessDefault) { static int nextUID = 0; uid = nextUID++; } -TreeViewItem::~TreeViewItem() -{ -} - String TreeViewItem::getUniqueName() const { return {}; @@ -1166,8 +1357,6 @@ void TreeViewItem::clearSubItems() { if (ownerView != nullptr) { - const ScopedLock sl (ownerView->nodeAlterationLock); - if (! subItems.isEmpty()) { removeAllSubItemsFromList(); @@ -1201,7 +1390,6 @@ void TreeViewItem::addSubItem (TreeViewItem* const newItem, const int insertPosi if (ownerView != nullptr) { - const ScopedLock sl (ownerView->nodeAlterationLock); subItems.insert (insertPosition, newItem); treeHasChanged(); @@ -1222,8 +1410,6 @@ void TreeViewItem::removeSubItem (int index, bool deleteItem) { if (ownerView != nullptr) { - const ScopedLock sl (ownerView->nodeAlterationLock); - if (removeSubItemFromList (index, deleteItem)) treeHasChanged(); } @@ -1239,6 +1425,7 @@ bool TreeViewItem::removeSubItemFromList (int index, bool deleteItem) { child->parentItem = nullptr; subItems.remove (index, deleteItem); + return true; } @@ -1247,14 +1434,14 @@ bool TreeViewItem::removeSubItemFromList (int index, bool deleteItem) TreeViewItem::Openness TreeViewItem::getOpenness() const noexcept { - return (Openness) openness; + return openness; } void TreeViewItem::setOpenness (Openness newOpenness) { - const bool wasOpen = isOpen(); + auto wasOpen = isOpen(); openness = newOpenness; - const bool isNowOpen = isOpen(); + auto isNowOpen = isOpen(); if (isNowOpen != wasOpen) { @@ -1265,17 +1452,17 @@ void TreeViewItem::setOpenness (Openness newOpenness) bool TreeViewItem::isOpen() const noexcept { - if (openness == opennessDefault) + if (openness == Openness::opennessDefault) return ownerView != nullptr && ownerView->defaultOpenness; - return openness == opennessOpen; + return openness == Openness::opennessOpen; } void TreeViewItem::setOpen (const bool shouldBeOpen) { if (isOpen() != shouldBeOpen) - setOpenness (shouldBeOpen ? opennessOpen - : opennessClosed); + setOpenness (shouldBeOpen ? Openness::opennessOpen + : Openness::opennessClosed); } bool TreeViewItem::isFullyOpen() const noexcept @@ -1292,7 +1479,7 @@ bool TreeViewItem::isFullyOpen() const noexcept void TreeViewItem::restoreToDefaultOpenness() { - setOpenness (opennessDefault); + setOpenness (Openness::opennessDefault); } bool TreeViewItem::isSelected() const noexcept @@ -1324,8 +1511,20 @@ void TreeViewItem::setSelected (const bool shouldBeSelected, selected = shouldBeSelected; if (ownerView != nullptr) + { ownerView->repaint(); + if (selected) + { + if (auto* itemComponent = ownerView->getItemComponent (this)) + if (auto* itemHandler = itemComponent->getAccessibilityHandler()) + itemHandler->grabFocus(); + } + + if (auto* handler = ownerView->getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::rowSelectionChanged); + } + if (notify != dontSendNotification) itemSelectionChanged (shouldBeSelected); } @@ -1372,6 +1571,15 @@ String TreeViewItem::getTooltip() return {}; } +String TreeViewItem::getAccessibilityName() +{ + auto tooltipString = getTooltip(); + + return tooltipString.isNotEmpty() + ? tooltipString + : "Level " + String (getItemDepth (this)) + " row " + String (getIndexInParent()); +} + void TreeViewItem::ownerViewChanged (TreeView*) { } @@ -1418,13 +1626,14 @@ Rectangle TreeViewItem::getItemPosition (const bool relativeToTreeViewTopLe void TreeViewItem::treeHasChanged() const noexcept { if (ownerView != nullptr) - ownerView->itemsChanged(); + ownerView->updateVisibleItems(); } void TreeViewItem::repaintItem() const { if (ownerView != nullptr && areAllParentsOpen()) - ownerView->viewport->repaint (getItemPosition (true).withLeft (0)); + if (auto* component = ownerView->getItemComponent (this)) + component->repaint(); } bool TreeViewItem::areAllParentsOpen() const noexcept @@ -1457,8 +1666,8 @@ void TreeViewItem::updatePositions (int newY) TreeViewItem* TreeViewItem::getDeepestOpenParentItem() noexcept { - TreeViewItem* result = this; - TreeViewItem* item = this; + auto* result = this; + auto* item = this; while (item->parentItem != nullptr) { @@ -1505,118 +1714,12 @@ void TreeViewItem::setDrawsInRightMargin (bool canDrawInRightMargin) noexcept drawsInRightMargin = canDrawInRightMargin; } -namespace TreeViewHelpers -{ - static int calculateDepth (const TreeViewItem* item, const bool rootIsVisible) noexcept - { - jassert (item != nullptr); - int depth = rootIsVisible ? 0 : -1; - - for (auto* p = item->getParentItem(); p != nullptr; p = p->getParentItem()) - ++depth; - - return depth; - } -} - bool TreeViewItem::areLinesDrawn() const { return drawLinesSet ? drawLinesInside : (ownerView != nullptr && ownerView->getLookAndFeel().areLinesDrawnForTreeView (*ownerView)); } -void TreeViewItem::paintRecursively (Graphics& g, int width) -{ - jassert (ownerView != nullptr); - - if (ownerView == nullptr) - return; - - auto indent = getIndentX(); - auto itemW = (itemWidth < 0 || drawsInRightMargin) ? width - indent : itemWidth; - - { - Graphics::ScopedSaveState ss (g); - g.setOrigin (indent, 0); - - if (g.reduceClipRegion (drawsInLeftMargin ? -indent : 0, 0, - drawsInLeftMargin ? itemW + indent : itemW, itemHeight)) - { - if (isSelected()) - g.fillAll (ownerView->findColour (TreeView::selectedItemBackgroundColourId)); - else - g.fillAll ((getRowNumberInTree() % 2 == 0) ? ownerView->findColour (TreeView::oddItemsColourId) - : ownerView->findColour (TreeView::evenItemsColourId)); - - paintItem (g, itemWidth < 0 ? width - indent : itemWidth, itemHeight); - } - } - - auto halfH = (float) itemHeight * 0.5f; - auto indentWidth = ownerView->getIndentSize(); - auto depth = TreeViewHelpers::calculateDepth (this, ownerView->rootItemVisible); - - if (depth >= 0 && ownerView->openCloseButtonsVisible) - { - auto x = ((float) depth + 0.5f) * (float) indentWidth; - - const bool parentLinesDrawn = parentItem != nullptr && parentItem->areLinesDrawn(); - - if (parentLinesDrawn) - paintVerticalConnectingLine (g, Line (x, 0, x, isLastOfSiblings() ? halfH : (float) itemHeight)); - - if (parentLinesDrawn || (parentItem == nullptr && areLinesDrawn())) - paintHorizontalConnectingLine (g, Line (x, halfH, x + (float) indentWidth * 0.5f, halfH)); - - { - auto* p = parentItem; - int d = depth; - - while (p != nullptr && --d >= 0) - { - x -= (float) indentWidth; - - if ((p->parentItem == nullptr || p->parentItem->areLinesDrawn()) && ! p->isLastOfSiblings()) - p->paintVerticalConnectingLine (g, Line (x, 0, x, (float) itemHeight)); - - p = p->parentItem; - } - } - - if (mightContainSubItems()) - { - auto backgroundColour = ownerView->findColour (TreeView::backgroundColourId); - - paintOpenCloseButton (g, Rectangle ((float) (depth * indentWidth), 0, (float) indentWidth, (float) itemHeight), - backgroundColour.isTransparent() ? Colours::white : backgroundColour, - ownerView->viewport->getContentComp()->isMouseOverButton (this)); - } - } - - if (isOpen()) - { - auto clip = g.getClipBounds(); - - for (auto* ti : subItems) - { - auto relY = ti->y - y; - - if (relY >= clip.getBottom()) - break; - - if (relY + ti->totalHeight >= clip.getY()) - { - Graphics::ScopedSaveState ss (g); - - g.setOrigin (0, relY); - - if (g.reduceClipRegion (0, 0, width, ti->totalHeight)) - ti->paintRecursively (g, width); - } - } - } -} - bool TreeViewItem::isLastOfSiblings() const noexcept { return parentItem == nullptr @@ -1672,32 +1775,6 @@ TreeViewItem* TreeViewItem::getItemOnRow (int index) noexcept return nullptr; } -TreeViewItem* TreeViewItem::findItemRecursively (int targetY) noexcept -{ - if (isPositiveAndBelow (targetY, totalHeight)) - { - auto h = itemHeight; - - if (targetY < h) - return this; - - if (isOpen()) - { - targetY -= h; - - for (auto* i : subItems) - { - if (targetY < i->totalHeight) - return i->findItemRecursively (targetY); - - targetY -= i->totalHeight; - } - } - } - - return nullptr; -} - int TreeViewItem::countSelectedItemsRecursively (int depth) const noexcept { int total = isSelected() ? 1 : 0; @@ -1740,9 +1817,9 @@ int TreeViewItem::getRowNumberInTree() const noexcept if (! parentItem->isOpen()) return parentItem->getRowNumberInTree(); - int n = 1 + parentItem->getRowNumberInTree(); + auto n = 1 + parentItem->getRowNumberInTree(); - int ourIndex = parentItem->subItems.indexOf (this); + auto ourIndex = parentItem->subItems.indexOf (this); jassert (ourIndex >= 0); while (--ourIndex >= 0) @@ -1758,30 +1835,12 @@ int TreeViewItem::getRowNumberInTree() const noexcept return 0; } -void TreeViewItem::setLinesDrawnForSubItems (const bool drawLines) noexcept +void TreeViewItem::setLinesDrawnForSubItems (bool drawLines) noexcept { drawLinesInside = drawLines; drawLinesSet = true; } -TreeViewItem* TreeViewItem::getNextVisibleItem (const bool recurse) const noexcept -{ - if (recurse && isOpen() && ! subItems.isEmpty()) - return subItems.getFirst(); - - if (parentItem != nullptr) - { - const int nextIndex = parentItem->subItems.indexOf (this) + 1; - - if (nextIndex >= parentItem->subItems.size()) - return parentItem->getNextVisibleItem (false); - - return parentItem->subItems [nextIndex]; - } - - return nullptr; -} - static String escapeSlashesInTreeViewItemName (const String& s) { return s.replaceCharacter ('/', '\\'); @@ -1808,7 +1867,7 @@ TreeViewItem* TreeViewItem::findItemFromIdentifierString (const String& identifi { auto remainingPath = identifierString.substring (thisId.length()); - const bool wasOpen = isOpen(); + const auto wasOpen = isOpen(); setOpen (true); for (auto* i : subItems) @@ -1840,7 +1899,7 @@ void TreeViewItem::restoreOpennessState (const XmlElement& e) for (int i = 0; i < items.size(); ++i) { - auto* ti = items.getUnchecked(i); + auto* ti = items.getUnchecked (i); if (ti->getUniqueName() == id) { @@ -1859,33 +1918,33 @@ void TreeViewItem::restoreOpennessState (const XmlElement& e) std::unique_ptr TreeViewItem::getOpennessState() const { - return std::unique_ptr (getOpennessState (true)); + return getOpennessState (true); } -XmlElement* TreeViewItem::getOpennessState (bool canReturnNull) const +std::unique_ptr TreeViewItem::getOpennessState (bool canReturnNull) const { auto name = getUniqueName(); if (name.isNotEmpty()) { - XmlElement* e; + std::unique_ptr e; if (isOpen()) { if (canReturnNull && ownerView != nullptr && ownerView->defaultOpenness && isFullyOpen()) return nullptr; - e = new XmlElement ("OPEN"); + e = std::make_unique ("OPEN"); for (int i = subItems.size(); --i >= 0;) - e->prependChildElement (subItems.getUnchecked(i)->getOpennessState (true)); + e->prependChildElement (subItems.getUnchecked (i)->getOpennessState (true).release()); } else { if (canReturnNull && ownerView != nullptr && ! ownerView->defaultOpenness) return nullptr; - e = new XmlElement ("CLOSED"); + e = std::make_unique ("CLOSED"); } e->setAttribute ("id", name); @@ -1895,7 +1954,7 @@ XmlElement* TreeViewItem::getOpennessState (bool canReturnNull) const // trying to save the openness for an element that has no name - this won't // work because it needs the names to identify what to open. jassertfalse; - return nullptr; + return {}; } //============================================================================== @@ -1911,4 +1970,67 @@ TreeViewItem::OpennessRestorer::~OpennessRestorer() treeViewItem.restoreOpennessState (*oldOpenness); } +void TreeViewItem::draw (Graphics& g, int width, bool isMouseOverButton) +{ + const auto indent = getIndentX(); + const auto itemW = (itemWidth < 0 || drawsInRightMargin) ? width - indent : itemWidth; + + { + Graphics::ScopedSaveState ss (g); + g.setOrigin (indent, 0); + + if (g.reduceClipRegion (drawsInLeftMargin ? -indent : 0, 0, + drawsInLeftMargin ? itemW + indent : itemW, itemHeight)) + { + if (isSelected()) + g.fillAll (ownerView->findColour (TreeView::selectedItemBackgroundColourId)); + else + g.fillAll ((getRowNumberInTree() % 2 == 0) ? ownerView->findColour (TreeView::oddItemsColourId) + : ownerView->findColour (TreeView::evenItemsColourId)); + + paintItem (g, itemWidth < 0 ? width - indent : itemWidth, itemHeight); + } + } + + const auto halfH = (float) itemHeight * 0.5f; + const auto indentWidth = ownerView->getIndentSize(); + const auto depth = getItemDepth (this); + + if (depth >= 0 && ownerView->openCloseButtonsVisible) + { + auto x = ((float) depth + 0.5f) * (float) indentWidth; + const auto parentLinesDrawn = parentItem != nullptr && parentItem->areLinesDrawn(); + + if (parentLinesDrawn) + paintVerticalConnectingLine (g, Line (x, 0, x, isLastOfSiblings() ? halfH : (float) itemHeight)); + + if (parentLinesDrawn || (parentItem == nullptr && areLinesDrawn())) + paintHorizontalConnectingLine (g, Line (x, halfH, x + (float) indentWidth * 0.5f, halfH)); + + { + auto* p = parentItem; + auto d = depth; + + while (p != nullptr && --d >= 0) + { + x -= (float) indentWidth; + + if ((p->parentItem == nullptr || p->parentItem->areLinesDrawn()) && ! p->isLastOfSiblings()) + p->paintVerticalConnectingLine (g, Line (x, 0, x, (float) itemHeight)); + + p = p->parentItem; + } + } + + if (mightContainSubItems()) + { + auto backgroundColour = ownerView->findColour (TreeView::backgroundColourId); + + paintOpenCloseButton (g, Rectangle ((float) (depth * indentWidth), 0, (float) indentWidth, (float) itemHeight), + backgroundColour.isTransparent() ? Colours::white : backgroundColour, + isMouseOverButton); + } + } +} + } // namespace juce diff --git a/modules/juce_gui_basics/widgets/juce_TreeView.h b/modules/juce_gui_basics/widgets/juce_TreeView.h index 0d8055ced1..72321151a8 100644 --- a/modules/juce_gui_basics/widgets/juce_TreeView.h +++ b/modules/juce_gui_basics/widgets/juce_TreeView.h @@ -28,10 +28,9 @@ namespace juce class TreeView; - //============================================================================== /** - An item in a treeview. + An item in a TreeView. A TreeViewItem can either be a leaf-node in the tree, or it can contain its own sub-items. @@ -53,11 +52,12 @@ public: TreeViewItem(); /** Destructor. */ - virtual ~TreeViewItem(); + virtual ~TreeViewItem() = default; //============================================================================== /** Returns the number of sub-items that have been added to this item. Note that this doesn't mean much if the node isn't open. + @see getSubItem, mightContainSubItems, addSubItem */ int getNumSubItems() const noexcept; @@ -133,7 +133,8 @@ public: TreeViewItem* getParentItem() const noexcept { return parentItem; } //============================================================================== - /** True if this item is currently open in the treeview. + /** True if this item is currently open in the TreeView. + @see getOpenness */ bool isOpen() const noexcept; @@ -154,14 +155,15 @@ public: void setOpen (bool shouldBeOpen); /** An enum of states to describe the explicit or implicit openness of an item. */ - enum Openness + enum class Openness { - opennessDefault = 0, - opennessClosed = 1, - opennessOpen = 2 + opennessDefault, + opennessClosed, + opennessOpen }; /** Returns the openness state of this item. + @see isOpen */ Openness getOpenness() const noexcept; @@ -177,11 +179,13 @@ public: void setOpenness (Openness newOpenness); /** True if this item is currently selected. + Use this when painting the node, to decide whether to draw it as selected or not. */ bool isSelected() const noexcept; /** Selects or deselects the item. + If shouldNotify == sendNotification, then a callback will be made to itemSelectionChanged() if the item's selection has changed. */ @@ -198,7 +202,8 @@ public: */ Rectangle getItemPosition (bool relativeToTreeViewTopLeft) const noexcept; - /** Sends a signal to the treeview to make it refresh itself. + /** Sends a signal to the TreeView to make it refresh itself. + Call this if your items have changed and you want the tree to update to reflect this. */ void treeHasChanged() const noexcept; @@ -211,17 +216,21 @@ public: void repaintItem() const; /** Returns the row number of this item in the tree. + The row number of an item will change according to which items are open. + @see TreeView::getNumRowsInTree(), TreeView::getItemOnRow() */ int getRowNumberInTree() const noexcept; /** Returns true if all the item's parent nodes are open. + This is useful to check whether the item might actually be visible or not. */ bool areAllParentsOpen() const noexcept; /** Changes whether lines are drawn to connect any sub-items to this item. + By default, line-drawing is turned on according to LookAndFeel::areLinesDrawnForTreeView(). */ void setLinesDrawnForSubItems (bool shouldDrawLines) noexcept; @@ -272,17 +281,17 @@ public: /** Must return the width required by this item. If your item needs to have a particular width in pixels, return that value; if - you'd rather have it just fill whatever space is available in the treeview, + you'd rather have it just fill whatever space is available in the TreeView, return -1. If all your items return -1, no horizontal scrollbar will be shown, but if any - items have fixed widths and extend beyond the width of the treeview, a + items have fixed widths and extend beyond the width of the TreeView, a scrollbar will appear. Each item can be a different width, but if they change width, you should call treeHasChanged() to update the tree. */ - virtual int getItemWidth() const { return -1; } + virtual int getItemWidth() const { return -1; } /** Must return the height required by this item. @@ -290,22 +299,22 @@ public: can be different heights, but if they change height, you should call treeHasChanged() to update the tree. */ - virtual int getItemHeight() const { return 20; } + virtual int getItemHeight() const { return 20; } /** You can override this method to return false if you don't want to allow the user to select this item. */ - virtual bool canBeSelected() const { return true; } + virtual bool canBeSelected() const { return true; } /** Creates a component that will be used to represent this item. You don't have to implement this method - if it returns nullptr then no component will be used for the item, and you can just draw it using the paintItem() callback. But if you do return a component, it will be positioned in the - treeview so that it can be used to represent this item. + TreeView so that it can be used to represent this item. - The component returned will be managed by the treeview, so always return - a new component, and don't keep a reference to it, as the treeview will + The component returned will be managed by the TreeView, so always return + a new component, and don't keep a reference to it, as the TreeView will delete it later when it goes off the screen or is no longer needed. Also bear in mind that if the component keeps a reference to the item that created it, that item could be deleted before the component. Its position @@ -321,7 +330,7 @@ public: component you like. It's most useful if you're doing things like drag-and-drop of items, or want to use a Label component to edit item names, etc. */ - virtual Component* createItemComponent() { return nullptr; } + virtual std::unique_ptr createItemComponent() { return nullptr; } //============================================================================== /** Draws the item's contents. @@ -361,7 +370,7 @@ public: /** Called when the user clicks on this item. If you're using createItemComponent() to create a custom component for the - item, the mouse-clicks might not make it through to the treeview, but this + item, the mouse-clicks might not make it through to the TreeView, but this is how you find out about clicks when just drawing each item individually. The associated mouse-event details are passed in, so you can find out about @@ -374,7 +383,7 @@ public: /** Called when the user double-clicks on this item. If you're using createItemComponent() to create a custom component for the - item, the mouse-clicks might not make it through to the treeview, but this + item, the mouse-clicks might not make it through to the TreeView, but this is how you find out about clicks when just drawing each item individually. The associated mouse-event details are passed in, so you can find out about @@ -398,16 +407,28 @@ public: virtual void ownerViewChanged (TreeView* newOwner); /** The item can return a tool tip string here if it wants to. + @see TooltipClient */ virtual String getTooltip(); - //============================================================================== - /** To allow items from your treeview to be dragged-and-dropped, implement this method. + /** Use this to set the name for this item that will be read out by accessibility + clients. - If this returns a non-null variant then when the user drags an item, the treeview will + The default implementation will return the tooltip string from getTooltip() + if it is not empty, otherwise it will return a description of the nested level + and row number of the item. + + @see AccessibilityHandler + */ + virtual String getAccessibilityName(); + + //============================================================================== + /** To allow items from your TreeView to be dragged-and-dropped, implement this method. + + If this returns a non-null variant then when the user drags an item, the TreeView will try to find a DragAndDropContainer in its parent hierarchy, and will use it to trigger - a drag-and-drop operation, using this string as the source description, with the treeview + a drag-and-drop operation, using this string as the source description, with the TreeView itself as the source component. If you need more complex drag-and-drop behaviour, you can use custom components for @@ -430,6 +451,7 @@ public: certainly no time to try opening the files and having a think about what's inside them! For responding to internal drag-and-drop of other types of object, see isInterestedInDragSource(). + @see FileDragAndDropTarget::isInterestedInFileDrag, isInterestedInDragSource */ virtual bool isInterestedInFileDrag (const StringArray& files); @@ -440,6 +462,7 @@ public: The insertIndex value indicates where in the list of sub-items the files were dropped. If files are dropped onto an area of the tree where there are no visible items, this method is called on the root item of the tree, with an insert index of 0. + @see FileDragAndDropTarget::filesDropped, isInterestedInFileDrag */ virtual void filesDropped (const StringArray& files, int insertIndex); @@ -449,6 +472,7 @@ public: If you implement this method, you'll also need to implement itemDropped() in order to handle the items when they are dropped. To respond to drag-and-drop of files from external applications, see isInterestedInFileDrag(). + @see DragAndDropTarget::isInterestedInDragSource, itemDropped */ virtual bool isInterestedInDragSource (const DragAndDropTarget::SourceDetails& dragSourceDetails); @@ -459,13 +483,14 @@ public: The insertIndex value indicates where in the list of sub-items the new items should be placed. If files are dropped onto an area of the tree where there are no visible items, this method is called on the root item of the tree, with an insert index of 0. + @see isInterestedInDragSource, DragAndDropTarget::itemDropped */ virtual void itemDropped (const DragAndDropTarget::SourceDetails& dragSourceDetails, int insertIndex); //============================================================================== /** Sets a flag to indicate that the item wants to be allowed - to draw all the way across to the left edge of the treeview. + to draw all the way across to the left edge of the TreeView. By default this is false, which means that when the paintItem() method is called, its graphics context is clipped to only allow @@ -480,7 +505,7 @@ public: void setDrawsInLeftMargin (bool canDrawInLeftMargin) noexcept; /** Sets a flag to indicate that the item wants to be allowed - to draw all the way across to the right edge of the treeview. + to draw all the way across to the right edge of the TreeView. Similar to setDrawsInLeftMargin: when this flag is set to true, then the graphics context isn't clipped on the right side. Unlike @@ -537,6 +562,7 @@ public: The string that is returned can be passed to TreeView::findItemFromIdentifierString(). The string takes the form of a path, constructed from the getUniqueName() of this item and all its parents, so these must all be correctly implemented for it to work. + @see TreeView::findItemFromIdentifierString, getUniqueName */ String getItemIdentifierString() const; @@ -563,7 +589,7 @@ public: } @endcode */ - class OpennessRestorer + class JUCE_API OpennessRestorer { public: OpennessRestorer (TreeViewItem&); @@ -578,53 +604,47 @@ public: private: //============================================================================== - TreeView* ownerView = nullptr; - TreeViewItem* parentItem = nullptr; - OwnedArray subItems; - int y = 0, itemHeight = 0, totalHeight = 0, itemWidth = 0, totalWidth = 0; - int uid = 0; - bool selected : 1; - bool redrawNeeded : 1; - bool drawLinesInside : 1; - bool drawLinesSet : 1; - bool drawsInLeftMargin : 1; - bool drawsInRightMargin : 1; - unsigned int openness : 2; - friend class TreeView; - void updatePositions (int newY); + void updatePositions (int); int getIndentX() const noexcept; void setOwnerView (TreeView*) noexcept; - void paintRecursively (Graphics&, int width); TreeViewItem* getTopLevelItem() noexcept; - TreeViewItem* findItemRecursively (int y) noexcept; TreeViewItem* getDeepestOpenParentItem() noexcept; int getNumRows() const noexcept; - TreeViewItem* getItemOnRow (int index) noexcept; - void deselectAllRecursively (TreeViewItem* itemToIgnore); - int countSelectedItemsRecursively (int depth) const noexcept; - TreeViewItem* getSelectedItemWithIndex (int index) noexcept; - TreeViewItem* getNextVisibleItem (bool recurse) const noexcept; + TreeViewItem* getItemOnRow (int) noexcept; + void deselectAllRecursively (TreeViewItem*); + int countSelectedItemsRecursively (int) const noexcept; + TreeViewItem* getSelectedItemWithIndex (int) noexcept; TreeViewItem* findItemFromIdentifierString (const String&); void restoreToDefaultOpenness(); bool isFullyOpen() const noexcept; - XmlElement* getOpennessState (bool canReturnNull) const; - bool removeSubItemFromList (int index, bool deleteItem); + std::unique_ptr getOpennessState (bool) const; + bool removeSubItemFromList (int, bool); void removeAllSubItemsFromList(); bool areLinesDrawn() const; + void draw (Graphics&, int, bool); + //============================================================================== + TreeView* ownerView = nullptr; + TreeViewItem* parentItem = nullptr; + OwnedArray subItems; + + Openness openness = Openness::opennessDefault; + int y = 0, itemHeight = 0, totalHeight = 0, itemWidth = 0, totalWidth = 0, uid = 0; + bool selected = false, redrawNeeded = true, drawLinesInside = false, drawLinesSet = false, + drawsInLeftMargin = false, drawsInRightMargin = false; + + //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TreeViewItem) }; - //============================================================================== /** A tree-view component. Use one of these to hold and display a structure of TreeViewItem objects. - @tags{GUI} */ class JUCE_API TreeView : public Component, @@ -634,18 +654,18 @@ class JUCE_API TreeView : public Component, { public: //============================================================================== - /** Creates an empty treeview. + /** Creates an empty TreeView. - Once you've got a treeview component, you'll need to give it something to + Once you've got a TreeView component, you'll need to give it something to display, using the setRootItem() method. */ - TreeView (const String& componentName = String()); + TreeView (const String& componentName = {}); /** Destructor. */ ~TreeView() override; //============================================================================== - /** Sets the item that is displayed in the treeview. + /** Sets the item that is displayed in the TreeView. A tree has a single root item which contains as many sub-items as it needs. If you want the tree to contain a number of root items, you should still use a single @@ -653,7 +673,7 @@ public: You can pass nullptr to this method to clear the tree and remove its current root item. - The object passed in will not be deleted by the treeview, it's up to the caller + The object passed in will not be deleted by the TreeView, it's up to the caller to delete it when no longer needed. BUT make absolutely sure that you don't delete this item until you've removed it from the tree, either by calling setRootItem (nullptr), or by deleting the tree first. You can also use deleteRootItem() as a quick way @@ -665,18 +685,19 @@ public: This will be the last object passed to setRootItem(), or nullptr if none has been set. */ - TreeViewItem* getRootItem() const noexcept { return rootItem; } + TreeViewItem* getRootItem() const noexcept { return rootItem; } /** This will remove and delete the current root item. + It's a convenient way of deleting the item and calling setRootItem (nullptr). */ void deleteRootItem(); /** Changes whether the tree's root item is shown or not. - If the root item is hidden, only its sub-items will be shown in the treeview - this + If the root item is hidden, only its sub-items will be shown in the TreeView - this lets you make the tree look as if it's got many root items. If it's hidden, this call - will also make sure the root item is open (otherwise the treeview would look empty). + will also make sure the root item is open (otherwise the TreeView would look empty). */ void setRootItemVisible (bool shouldBeVisible); @@ -684,7 +705,7 @@ public: @see setRootItemVisible */ - bool isRootItemVisible() const noexcept { return rootItemVisible; } + bool isRootItemVisible() const noexcept { return rootItemVisible; } /** Sets whether items are open or closed by default. @@ -699,7 +720,7 @@ public: @see setDefaultOpenness */ - bool areItemsOpenByDefault() const noexcept { return defaultOpenness; } + bool areItemsOpenByDefault() const noexcept { return defaultOpenness; } /** This sets a flag to indicate that the tree can be used for multi-selection. @@ -717,7 +738,7 @@ public: @see setMultiSelectEnabled */ - bool isMultiSelectEnabled() const noexcept { return multiSelectEnabled; } + bool isMultiSelectEnabled() const noexcept { return multiSelectEnabled; } /** Sets a flag to indicate whether to hide the open/close buttons. @@ -729,20 +750,23 @@ public: @see setOpenCloseButtonsVisible */ - bool areOpenCloseButtonsVisible() const noexcept { return openCloseButtonsVisible; } + bool areOpenCloseButtonsVisible() const noexcept { return openCloseButtonsVisible; } //============================================================================== /** Deselects any items that are currently selected. */ void clearSelectedItems(); /** Returns the number of items that are currently selected. + If maximumDepthToSearchTo is >= 0, it lets you specify a maximum depth to which the tree will be recursed. + @see getSelectedItem, clearSelectedItems */ int getNumSelectedItems (int maximumDepthToSearchTo = -1) const noexcept; /** Returns one of the selected items in the tree. + @param index the index, 0 to (getNumSelectedItems() - 1) */ TreeViewItem* getSelectedItem (int index) const noexcept; @@ -751,46 +775,54 @@ public: void moveSelectedRow (int deltaRows); //============================================================================== - /** Returns the number of rows the tree is using. - This will depend on which items are open. + /** Returns the number of rows the tree is using, depending on which items are open. + @see TreeViewItem::getRowNumberInTree() */ int getNumRowsInTree() const; /** Returns the item on a particular row of the tree. + If the index is out of range, this will return nullptr. + @see getNumRowsInTree, TreeViewItem::getRowNumberInTree() */ TreeViewItem* getItemOnRow (int index) const; - /** Returns the item that contains a given y position. - The y is relative to the top of the TreeView component. + /** Returns the item that contains a given y-position relative to the top + of the TreeView component. */ TreeViewItem* getItemAt (int yPosition) const noexcept; /** Tries to scroll the tree so that this item is on-screen somewhere. */ void scrollToKeepItemVisible (TreeViewItem* item); - /** Returns the treeview's Viewport object. */ + /** Returns the TreeView's Viewport object. */ Viewport* getViewport() const noexcept; /** Returns the number of pixels by which each nested level of the tree is indented. + @see setIndentSize */ int getIndentSize() noexcept; /** Changes the distance by which each nested level of the tree is indented. + @see getIndentSize */ void setIndentSize (int newIndentSize); /** Searches the tree for an item with the specified identifier. + The identifier string must have been created by calling TreeViewItem::getItemIdentifierString(). If no such item exists, this will return false. If the item is found, all of its items will be automatically opened. */ TreeViewItem* findItemFromIdentifierString (const String& identifierString) const; + /** Returns the component that currently represents a given TreeViewItem. */ + Component* getItemComponent (const TreeViewItem* item) const; + //============================================================================== /** Saves the current state of open/closed nodes so it can be restored later. @@ -820,11 +852,10 @@ public: @see getOpennessState */ - void restoreOpennessState (const XmlElement& newState, - bool restoreStoredSelection); + void restoreOpennessState (const XmlElement& newState, bool restoreStoredSelection); //============================================================================== - /** A set of colour IDs to use to change the colour of various aspects of the treeview. + /** A set of colour IDs to use to change the colour of various aspects of the TreeView. These constants can be used either via the Component::setColour(), or LookAndFeel::setColour() methods. @@ -843,7 +874,7 @@ public: //============================================================================== /** This abstract base class is implemented by LookAndFeel classes to provide - treeview drawing functionality. + TreeView drawing functionality. */ struct JUCE_API LookAndFeelMethods { @@ -868,15 +899,15 @@ public: /** @internal */ void enablementChanged() override; /** @internal */ - bool isInterestedInFileDrag (const StringArray& files) override; + bool isInterestedInFileDrag (const StringArray&) override; /** @internal */ - void fileDragEnter (const StringArray& files, int x, int y) override; + void fileDragEnter (const StringArray&, int, int) override; /** @internal */ - void fileDragMove (const StringArray& files, int x, int y) override; + void fileDragMove (const StringArray&, int, int) override; /** @internal */ - void fileDragExit (const StringArray& files) override; + void fileDragExit (const StringArray&) override; /** @internal */ - void filesDropped (const StringArray& files, int x, int y) override; + void filesDropped (const StringArray&, int, int) override; /** @internal */ bool isInterestedInDragSource (const SourceDetails&) override; /** @internal */ @@ -887,28 +918,23 @@ public: void itemDragExit (const SourceDetails&) override; /** @internal */ void itemDropped (const SourceDetails&) override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: friend class TreeViewItem; + class ItemComponent; class ContentComponent; class TreeViewport; class InsertPointHighlight; class TargetGroupHighlight; - - std::unique_ptr viewport; - CriticalSection nodeAlterationLock; - TreeViewItem* rootItem = nullptr; - std::unique_ptr dragInsertPointHighlight; - std::unique_ptr dragTargetGroupHighlight; - int indentSize = -1; - bool defaultOpenness = false, needsRecalculating = true, rootItemVisible = true; - bool multiSelectEnabled = false, openCloseButtonsVisible = true; + class TreeAccessibilityHandler; + struct InsertPoint; void itemsChanged() noexcept; - void recalculateIfNeeded(); + void updateVisibleItems(); void updateButtonUnderMouse (const MouseEvent&); - struct InsertPoint; void showDragHighlight (const InsertPoint&) noexcept; void hideDragHighlight() noexcept; void handleDrag (const StringArray&, const SourceDetails&); @@ -916,7 +942,14 @@ private: bool toggleOpenSelectedItem(); void moveOutOfSelectedItem(); void moveIntoSelectedItem(); - void moveByPages (int numPages); + void moveByPages (int); + + std::unique_ptr viewport; + TreeViewItem* rootItem = nullptr; + std::unique_ptr dragInsertPointHighlight; + std::unique_ptr dragTargetGroupHighlight; + int indentSize = -1; + bool defaultOpenness = false, rootItemVisible = true, multiSelectEnabled = false, openCloseButtonsVisible = true; #if JUCE_CATCH_DEPRECATED_CODE_MISUSE // this method has been deprecated - see the new version.. diff --git a/modules/juce_gui_basics/windows/juce_AlertWindow.cpp b/modules/juce_gui_basics/windows/juce_AlertWindow.cpp index c41874b567..bf4861ac39 100644 --- a/modules/juce_gui_basics/windows/juce_AlertWindow.cpp +++ b/modules/juce_gui_basics/windows/juce_AlertWindow.cpp @@ -47,6 +47,9 @@ AlertWindow::AlertWindow (const String& title, { setAlwaysOnTop (juce_areThereAnyAlwaysOnTopWindows()); + accessibleMessageLabel.setColour (Label::textColourId, Colours::transparentBlack); + addAndMakeVisible (accessibleMessageLabel); + if (message.isEmpty()) text = " "; // to force an update if the message is empty @@ -65,8 +68,7 @@ AlertWindow::~AlertWindow() // Give away focus before removing the editors, so that any TextEditor // with focus has a chance to dismiss native keyboard if shown. - if (hasKeyboardFocus (true)) - Component::unfocusAllComponents(); + giveAwayKeyboardFocus(); removeAllChildren(); } @@ -85,6 +87,11 @@ void AlertWindow::setMessage (const String& message) if (text != newMessage) { text = newMessage; + + auto accessibleText = getName() + ". " + text; + accessibleMessageLabel.setText (accessibleText, NotificationType::dontSendNotification); + setDescription (accessibleText); + updateLayout (true); repaint(); } @@ -107,6 +114,7 @@ void AlertWindow::addButton (const String& name, buttons.add (b); b->setWantsKeyboardFocus (true); + b->setExplicitFocusOrder (1); b->setMouseClickGrabsKeyboardFocus (false); b->setCommandToTrigger (nullptr, returnValue, false); b->addShortcut (shortcutKey1); @@ -436,6 +444,7 @@ void AlertWindow::updateLayout (const bool onlyIncreaseSize) setBounds (getBounds().withSizeKeepingCentre (w, h)); textArea.setBounds (edgeGap, edgeGap, w - (edgeGap * 2), h - edgeGap); + accessibleMessageLabel.setBounds (textArea); const int spacer = 16; int totalWidth = -spacer; @@ -705,4 +714,10 @@ bool AlertWindow::showNativeDialogBox (const String& title, } #endif +//============================================================================== +std::unique_ptr AlertWindow::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::dialogWindow); +} + } // namespace juce diff --git a/modules/juce_gui_basics/windows/juce_AlertWindow.h b/modules/juce_gui_basics/windows/juce_AlertWindow.h index 7008b279e9..aaa814deb7 100644 --- a/modules/juce_gui_basics/windows/juce_AlertWindow.h +++ b/modules/juce_gui_basics/windows/juce_AlertWindow.h @@ -462,11 +462,14 @@ protected: int getDesktopWindowStyleFlags() const override; /** @internal */ float getDesktopScaleFactor() const override { return desktopScale; } + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: //============================================================================== String text; TextLayout textLayout; + Label accessibleMessageLabel; AlertIconType alertIconType; ComponentBoundsConstrainer constrainer; ComponentDragger dragger; diff --git a/modules/juce_gui_basics/windows/juce_CallOutBox.cpp b/modules/juce_gui_basics/windows/juce_CallOutBox.cpp index bab4c69610..aa70a4af00 100644 --- a/modules/juce_gui_basics/windows/juce_CallOutBox.cpp +++ b/modules/juce_gui_basics/windows/juce_CallOutBox.cpp @@ -262,4 +262,10 @@ void CallOutBox::timerCallback() stopTimer(); } +//============================================================================== +std::unique_ptr CallOutBox::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::window); +} + } // namespace juce diff --git a/modules/juce_gui_basics/windows/juce_CallOutBox.h b/modules/juce_gui_basics/windows/juce_CallOutBox.h index eaa469928c..e2be016b59 100644 --- a/modules/juce_gui_basics/windows/juce_CallOutBox.h +++ b/modules/juce_gui_basics/windows/juce_CallOutBox.h @@ -164,6 +164,8 @@ public: int getBorderSize() const noexcept; /** @internal */ void lookAndFeelChanged() override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: //============================================================================== diff --git a/modules/juce_gui_basics/windows/juce_ComponentPeer.cpp b/modules/juce_gui_basics/windows/juce_ComponentPeer.cpp index 8d7febd4b6..86966089de 100644 --- a/modules/juce_gui_basics/windows/juce_ComponentPeer.cpp +++ b/modules/juce_gui_basics/windows/juce_ComponentPeer.cpp @@ -192,7 +192,7 @@ bool ComponentPeer::handleKeyPress (const KeyPress& keyInfo) { for (int i = keyListeners->size(); --i >= 0;) { - keyWasUsed = keyListeners->getUnchecked(i)->keyPressed (keyInfo, target); + keyWasUsed = keyListeners->getUnchecked (i)->keyPressed (keyInfo, target); if (keyWasUsed || deletionChecker == nullptr) return keyWasUsed; @@ -205,20 +205,14 @@ bool ComponentPeer::handleKeyPress (const KeyPress& keyInfo) if (keyWasUsed || deletionChecker == nullptr) break; + } + if (! keyWasUsed && keyInfo.isKeyCode (KeyPress::tabKey)) + { if (auto* currentlyFocused = Component::getCurrentlyFocusedComponent()) { - const bool isTab = (keyInfo == KeyPress::tabKey); - const bool isShiftTab = (keyInfo == KeyPress (KeyPress::tabKey, ModifierKeys::shiftModifier, 0)); - - if (isTab || isShiftTab) - { - currentlyFocused->moveKeyboardFocusToSibling (isTab); - keyWasUsed = (currentlyFocused != Component::getCurrentlyFocusedComponent()); - - if (keyWasUsed || deletionChecker == nullptr) - break; - } + currentlyFocused->moveKeyboardFocusToSibling (! keyInfo.getModifiers().isShiftDown()); + return true; } } @@ -242,7 +236,7 @@ bool ComponentPeer::handleKeyUpOrDown (const bool isKeyDown) { for (int i = keyListeners->size(); --i >= 0;) { - keyWasUsed = keyListeners->getUnchecked(i)->keyStateChanged (isKeyDown, target); + keyWasUsed = keyListeners->getUnchecked (i)->keyStateChanged (isKeyDown, target); if (keyWasUsed || deletionChecker == nullptr) return keyWasUsed; @@ -340,7 +334,7 @@ void ComponentPeer::handleFocusGain() { Component::currentlyFocusedComponent = lastFocusedComponent; Desktop::getInstance().triggerFocusCallback(); - lastFocusedComponent->internalFocusGain (Component::focusChangedDirectly); + lastFocusedComponent->internalKeyboardFocusGain (Component::focusChangedDirectly); } else { @@ -361,7 +355,7 @@ void ComponentPeer::handleFocusLoss() { Component::currentlyFocusedComponent = nullptr; Desktop::getInstance().triggerFocusCallback(); - lastFocusedComponent->internalFocusLoss (Component::focusChangedByMouseClick); + lastFocusedComponent->internalKeyboardFocusLoss (Component::focusChangedByMouseClick); } } } @@ -412,7 +406,7 @@ Rectangle ComponentPeer::globalToLocal (const Rectangle& screenPos return screenPosition.withPosition (globalToLocal (screenPosition.getPosition())); } -Rectangle ComponentPeer::getAreaCoveredBy (Component& subComponent) const +Rectangle ComponentPeer::getAreaCoveredBy (const Component& subComponent) const { return ScalingHelpers::scaledScreenPosToUnscaled (component, component.getLocalArea (&subComponent, subComponent.getLocalBounds())); diff --git a/modules/juce_gui_basics/windows/juce_ComponentPeer.h b/modules/juce_gui_basics/windows/juce_ComponentPeer.h index 691e5679b9..83f90db4ec 100644 --- a/modules/juce_gui_basics/windows/juce_ComponentPeer.h +++ b/modules/juce_gui_basics/windows/juce_ComponentPeer.h @@ -177,7 +177,7 @@ public: /** Returns the area in peer coordinates that is covered by the given sub-comp (which may be at any depth) */ - Rectangle getAreaCoveredBy (Component& subComponent) const; + Rectangle getAreaCoveredBy (const Component& subComponent) const; /** Minimises the window. */ virtual void setMinimised (bool shouldBeMinimised) = 0; @@ -247,8 +247,8 @@ public: */ virtual bool setAlwaysOnTop (bool alwaysOnTop) = 0; - /** Brings the window to the top, optionally also giving it focus. */ - virtual void toFront (bool makeActive) = 0; + /** Brings the window to the top, optionally also giving it keyboard focus. */ + virtual void toFront (bool takeKeyboardFocus) = 0; /** Moves the window to be just behind another one. */ virtual void toBehind (ComponentPeer* other) = 0; diff --git a/modules/juce_gui_basics/windows/juce_DialogWindow.cpp b/modules/juce_gui_basics/windows/juce_DialogWindow.cpp index 037c986c12..37067b2fef 100644 --- a/modules/juce_gui_basics/windows/juce_DialogWindow.cpp +++ b/modules/juce_gui_basics/windows/juce_DialogWindow.cpp @@ -172,4 +172,10 @@ int DialogWindow::showModalDialog (const String& dialogTitle, } #endif +//============================================================================== +std::unique_ptr DialogWindow::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::dialogWindow); +} + } // namespace juce diff --git a/modules/juce_gui_basics/windows/juce_DialogWindow.h b/modules/juce_gui_basics/windows/juce_DialogWindow.h index 70b98b3f94..5048169542 100644 --- a/modules/juce_gui_basics/windows/juce_DialogWindow.h +++ b/modules/juce_gui_basics/windows/juce_DialogWindow.h @@ -263,6 +263,8 @@ protected: bool keyPressed (const KeyPress&) override; /** @internal */ float getDesktopScaleFactor() const override { return desktopScale; } + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: float desktopScale = 1.0f; diff --git a/modules/juce_gui_basics/windows/juce_TooltipWindow.cpp b/modules/juce_gui_basics/windows/juce_TooltipWindow.cpp index c893c8d9e9..f76eb1b682 100644 --- a/modules/juce_gui_basics/windows/juce_TooltipWindow.cpp +++ b/modules/juce_gui_basics/windows/juce_TooltipWindow.cpp @@ -117,6 +117,12 @@ void TooltipWindow::displayTip (Point screenPos, const String& tip) #endif toFront (false); + + if (auto* handler = getAccessibilityHandler()) + { + setDescription (tip); + handler->grabFocus(); + } } } @@ -137,6 +143,9 @@ void TooltipWindow::hideTip() { if (! reentrant) { + if (auto* handler = getAccessibilityHandler()) + handler->giveAwayFocus(); + tipShowing.clear(); removeFromDesktop(); setVisible (false); @@ -206,4 +215,10 @@ void TooltipWindow::timerCallback() } } +//============================================================================== +std::unique_ptr TooltipWindow::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::tooltip); +} + } // namespace juce diff --git a/modules/juce_gui_basics/windows/juce_TooltipWindow.h b/modules/juce_gui_basics/windows/juce_TooltipWindow.h index f82cbf6a7a..5b493eb578 100644 --- a/modules/juce_gui_basics/windows/juce_TooltipWindow.h +++ b/modules/juce_gui_basics/windows/juce_TooltipWindow.h @@ -128,6 +128,10 @@ public: #endif }; + //============================================================================== + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; + private: //============================================================================== Point lastMousePos; diff --git a/modules/juce_gui_basics/windows/juce_TopLevelWindow.cpp b/modules/juce_gui_basics/windows/juce_TopLevelWindow.cpp index d454803e96..b5bd04818d 100644 --- a/modules/juce_gui_basics/windows/juce_TopLevelWindow.cpp +++ b/modules/juce_gui_basics/windows/juce_TopLevelWindow.cpp @@ -134,6 +134,8 @@ void juce_checkCurrentlyFocusedTopLevelWindow() TopLevelWindow::TopLevelWindow (const String& name, const bool shouldAddToDesktop) : Component (name) { + setTitle (name); + setOpaque (true); if (shouldAddToDesktop) @@ -279,6 +281,11 @@ void TopLevelWindow::addToDesktop (int windowStyleFlags, void* nativeWindowToAtt sendLookAndFeelChange(); } +std::unique_ptr TopLevelWindow::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::window); +} + //============================================================================== void TopLevelWindow::centreAroundComponent (Component* c, const int width, const int height) { diff --git a/modules/juce_gui_basics/windows/juce_TopLevelWindow.h b/modules/juce_gui_basics/windows/juce_TopLevelWindow.h index b11d90b225..473bf53d32 100644 --- a/modules/juce_gui_basics/windows/juce_TopLevelWindow.h +++ b/modules/juce_gui_basics/windows/juce_TopLevelWindow.h @@ -130,6 +130,8 @@ public: //============================================================================== /** @internal */ void addToDesktop (int windowStyleFlags, void* nativeWindowToAttachTo = nullptr) override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; protected: //============================================================================== diff --git a/modules/juce_gui_extra/code_editor/juce_CodeEditorComponent.cpp b/modules/juce_gui_extra/code_editor/juce_CodeEditorComponent.cpp index 9e2538e3a9..8144fd39f6 100644 --- a/modules/juce_gui_extra/code_editor/juce_CodeEditorComponent.cpp +++ b/modules/juce_gui_extra/code_editor/juce_CodeEditorComponent.cpp @@ -26,6 +26,131 @@ namespace juce { +//============================================================================== +class CodeEditorComponent::CodeEditorAccessibilityHandler : public AccessibilityHandler +{ +public: + explicit CodeEditorAccessibilityHandler (CodeEditorComponent& codeEditorComponentToWrap) + : AccessibilityHandler (codeEditorComponentToWrap, + codeEditorComponentToWrap.isReadOnly() ? AccessibilityRole::staticText + : AccessibilityRole::editableText, + {}, + { codeEditorComponentToWrap.isReadOnly() ? nullptr + : std::make_unique (codeEditorComponentToWrap) }), + codeEditorComponent (codeEditorComponentToWrap) + { + } + + String getTitle() const override + { + return codeEditorComponent.isReadOnly() ? codeEditorComponent.document.getAllContent() + : codeEditorComponent.getTitle(); + } + +private: + class CodeEditorComponentTextInterface : public AccessibilityTextInterface + { + public: + explicit CodeEditorComponentTextInterface (CodeEditorComponent& codeEditorComponentToWrap) + : codeEditorComponent (codeEditorComponentToWrap) + { + } + + bool isDisplayingProtectedText() const override + { + return false; + } + + int getTotalNumCharacters() const override + { + return codeEditorComponent.document.getAllContent().length(); + } + + Range getSelection() const override + { + return { codeEditorComponent.selectionStart.getPosition(), + codeEditorComponent.selectionEnd.getPosition() }; + } + + void setSelection (Range r) override + { + auto& doc = codeEditorComponent.document; + + codeEditorComponent.selectRegion (CodeDocument::Position (doc, r.getStart()), + CodeDocument::Position (doc, r.getEnd())); + } + + String getText (Range r) const override + { + auto& doc = codeEditorComponent.document; + + return doc.getTextBetween (CodeDocument::Position (doc, r.getStart()), + CodeDocument::Position (doc, r.getEnd())); + } + + void setText (const String& newText) override + { + codeEditorComponent.document.replaceAllContent (newText); + } + + int getTextInsertionOffset() const override + { + return codeEditorComponent.caretPos.getPosition(); + } + + RectangleList getTextBounds (Range textRange) const override + { + auto& doc = codeEditorComponent.document; + + RectangleList localRects; + + CodeDocument::Position startPosition (doc, textRange.getStart()); + CodeDocument::Position endPosition (doc, textRange.getEnd()); + + for (int line = startPosition.getLineNumber(); line <= endPosition.getLineNumber(); ++line) + { + CodeDocument::Position lineStart (doc, line, 0); + CodeDocument::Position lineEnd (doc, line, doc.getLine (line).length()); + + if (line == startPosition.getLineNumber()) + lineStart = lineStart.movedBy (startPosition.getIndexInLine()); + + if (line == endPosition.getLineNumber()) + lineEnd = { doc, line, endPosition.getIndexInLine() }; + + auto startPos = codeEditorComponent.getCharacterBounds (lineStart).getTopLeft(); + auto endPos = codeEditorComponent.getCharacterBounds (lineEnd).getTopLeft(); + + localRects.add (startPos.x, + startPos.y, + endPos.x - startPos.x, + codeEditorComponent.getLineHeight()); + } + + RectangleList globalRects; + + for (auto r : localRects) + globalRects.add (codeEditorComponent.localAreaToGlobal (r)); + + return globalRects; + } + + int getOffsetAtPoint (Point point) const override + { + return codeEditorComponent.getPositionAt (point.x, point.y).getPosition(); + } + + private: + CodeEditorComponent& codeEditorComponent; + }; + + CodeEditorComponent& codeEditorComponent; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CodeEditorAccessibilityHandler) +}; + +//============================================================================== class CodeEditorComponent::CodeEditorLine { public: @@ -437,6 +562,8 @@ void CodeEditorComponent::setReadOnly (bool b) noexcept removeChildComponent (caret.get()); else addAndMakeVisible (caret.get()); + + invalidateAccessibilityHandler(); } } @@ -585,7 +712,12 @@ void CodeEditorComponent::retokenise (int startIndex, int endIndex) void CodeEditorComponent::updateCaretPosition() { if (caret != nullptr) + { caret->setCaretPosition (getCharacterBounds (getCaretPos())); + + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::textSelectionChanged); + } } void CodeEditorComponent::moveCaretTo (const CodeDocument::Position& newPos, const bool highlighting) @@ -598,38 +730,36 @@ void CodeEditorComponent::moveCaretTo (const CodeDocument::Position& newPos, con { if (dragType == notDragging) { - if (std::abs (caretPos.getPosition() - selectionStart.getPosition()) - < std::abs (caretPos.getPosition() - selectionEnd.getPosition())) - dragType = draggingSelectionStart; - else - dragType = draggingSelectionEnd; + auto oldCaretPos = caretPos.getPosition(); + auto isStart = std::abs (oldCaretPos - selectionStart.getPosition()) + < std::abs (oldCaretPos - selectionEnd.getPosition()); + + dragType = isStart ? draggingSelectionStart : draggingSelectionEnd; } if (dragType == draggingSelectionStart) { - selectionStart = caretPos; - - if (selectionEnd.getPosition() < selectionStart.getPosition()) + if (selectionEnd.getPosition() < caretPos.getPosition()) { - auto temp = selectionStart; - selectionStart = selectionEnd; - selectionEnd = temp; - + setSelection (selectionEnd, caretPos); dragType = draggingSelectionEnd; } + else + { + setSelection (caretPos, selectionEnd); + } } else { - selectionEnd = caretPos; - - if (selectionEnd.getPosition() < selectionStart.getPosition()) + if (caretPos.getPosition() < selectionStart.getPosition()) { - auto temp = selectionStart; - selectionStart = selectionEnd; - selectionEnd = temp; - + setSelection (caretPos, selectionStart); dragType = draggingSelectionStart; } + else + { + setSelection (selectionStart, caretPos); + } } rebuildLineTokensAsync(); @@ -644,6 +774,9 @@ void CodeEditorComponent::moveCaretTo (const CodeDocument::Position& newPos, con updateScrollBars(); caretPositionMoved(); + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::textChanged); + if (appCommandManager != nullptr && selectionWasActive != isHighlightActive()) appCommandManager->commandStatusChanged(); } @@ -653,8 +786,7 @@ void CodeEditorComponent::deselectAll() if (isHighlightActive()) rebuildLineTokensAsync(); - selectionStart = caretPos; - selectionEnd = caretPos; + setSelection (caretPos, caretPos); dragType = notDragging; } @@ -746,7 +878,7 @@ Rectangle CodeEditorComponent::getCharacterBounds (const CodeDocument::Posi lineHeight }; } -CodeDocument::Position CodeEditorComponent::getPositionAt (int x, int y) +CodeDocument::Position CodeEditorComponent::getPositionAt (int x, int y) const { const int line = y / lineHeight + firstLineOnScreen; const int column = roundToInt ((x - (getGutterSize() - xOffset * charWidth)) / charWidth); @@ -772,6 +904,9 @@ void CodeEditorComponent::insertText (const String& newText) scrollToKeepCaretOnScreen(); caretPositionMoved(); + + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::textChanged); } } @@ -865,9 +1000,15 @@ void CodeEditorComponent::indentSelectedLines (const int spacesToAdd) } } - selectionStart = oldSelectionStart; - selectionEnd = oldSelectionEnd; - caretPos = oldCaret; + setSelection (oldSelectionStart, oldSelectionEnd); + + if (caretPos != oldCaret) + { + caretPos = oldCaret; + + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::textChanged); + } } } @@ -1341,6 +1482,20 @@ bool CodeEditorComponent::performCommand (const CommandID commandID) return true; } +void CodeEditorComponent::setSelection (CodeDocument::Position newSelectionStart, + CodeDocument::Position newSelectionEnd) +{ + if (selectionStart != newSelectionStart + || selectionEnd != newSelectionEnd) + { + selectionStart = newSelectionStart; + selectionEnd = newSelectionEnd; + + if (auto* handler = getAccessibilityHandler()) + handler->notifyAccessibilityEvent (AccessibilityEvent::textSelectionChanged); + } +} + //============================================================================== void CodeEditorComponent::addPopupMenuItems (PopupMenu& m, const MouseEvent*) { @@ -1674,4 +1829,10 @@ String CodeEditorComponent::State::toString() const return String (lastTopLine) + ":" + String (lastCaretPos) + ":" + String (lastSelectionEnd); } +//============================================================================== +std::unique_ptr CodeEditorComponent::createAccessibilityHandler() +{ + return std::make_unique (*this); +} + } // namespace juce diff --git a/modules/juce_gui_extra/code_editor/juce_CodeEditorComponent.h b/modules/juce_gui_extra/code_editor/juce_CodeEditorComponent.h index 29617a1538..c443ae061e 100644 --- a/modules/juce_gui_extra/code_editor/juce_CodeEditorComponent.h +++ b/modules/juce_gui_extra/code_editor/juce_CodeEditorComponent.h @@ -110,7 +110,7 @@ public: /** Finds the character at a given on-screen position. The coordinates are relative to this component's top-left origin. */ - CodeDocument::Position getPositionAt (int x, int y); + CodeDocument::Position getPositionAt (int x, int y) const; /** Returns the start of the selection as a position. */ CodeDocument::Position getSelectionStart() const { return selectionStart; } @@ -380,6 +380,8 @@ public: bool perform (const InvocationInfo&) override; /** @internal */ void lookAndFeelChanged() override; + /** @internal */ + std::unique_ptr createAccessibilityHandler() override; private: //============================================================================== @@ -404,6 +406,8 @@ private: class GutterComponent; std::unique_ptr gutter; + class CodeEditorAccessibilityHandler; + enum DragType { notDragging, @@ -442,6 +446,7 @@ private: void indentSelectedLines (int spacesToAdd); bool skipBackwardsToPreviousTab(); bool performCommand (CommandID); + void setSelection (CodeDocument::Position, CodeDocument::Position); int indexToColumn (int line, int index) const noexcept; int columnToIndex (int line, int column) const noexcept; diff --git a/modules/juce_gui_extra/misc/juce_KeyMappingEditorComponent.cpp b/modules/juce_gui_extra/misc/juce_KeyMappingEditorComponent.cpp index 4b8088c5fb..ad958528e9 100644 --- a/modules/juce_gui_extra/misc/juce_KeyMappingEditorComponent.cpp +++ b/modules/juce_gui_extra/misc/juce_KeyMappingEditorComponent.cpp @@ -225,7 +225,7 @@ public: for (int i = 0; i < jmin ((int) maxNumAssignments, keyPresses.size()); ++i) addKeyPressButton (owner.getDescriptionForKeyPress (keyPresses.getReference (i)), i, isReadOnly); - addKeyPressButton (String(), -1, isReadOnly); + addKeyPressButton ("Change Key Mapping", -1, isReadOnly); } void addKeyPressButton (const String& desc, const int index, const bool isReadOnly) @@ -262,6 +262,11 @@ public: } } + std::unique_ptr createAccessibilityHandler() override + { + return nullptr; + } + private: KeyMappingEditorComponent& owner; OwnedArray keyChangeButtons; @@ -280,10 +285,11 @@ public: : owner (kec), commandID (command) {} - String getUniqueName() const override { return String ((int) commandID) + "_id"; } - bool mightContainSubItems() override { return false; } - int getItemHeight() const override { return 20; } - Component* createItemComponent() override { return new ItemComponent (owner, commandID); } + String getUniqueName() const override { return String ((int) commandID) + "_id"; } + bool mightContainSubItems() override { return false; } + int getItemHeight() const override { return 20; } + std::unique_ptr createItemComponent() override { return std::make_unique (owner, commandID); } + String getAccessibilityName() override { return TRANS (owner.getCommandManager().getNameOfCommand (commandID)); } private: KeyMappingEditorComponent& owner; @@ -304,6 +310,7 @@ public: String getUniqueName() const override { return categoryName + "_cat"; } bool mightContainSubItems() override { return true; } int getItemHeight() const override { return 22; } + String getAccessibilityName() override { return categoryName; } void paintItem (Graphics& g, int width, int height) override { @@ -406,6 +413,7 @@ KeyMappingEditorComponent::KeyMappingEditorComponent (KeyPressMappingSet& mappin } addAndMakeVisible (tree); + tree.setTitle ("Key Mappings"); tree.setColour (TreeView::backgroundColourId, findColour (backgroundColourId)); tree.setRootItemVisible (false); tree.setDefaultOpenness (true);