| @@ -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' | |||
| @@ -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 | |||
| @@ -29,6 +29,16 @@ namespace juce | |||
| { | |||
| //============================================================================== | |||
| inline Range<int> nsRangeToJuce (NSRange range) | |||
| { | |||
| return { (int) range.location, (int) (range.location + range.length) }; | |||
| } | |||
| inline NSRange juceRangeToNS (Range<int> range) | |||
| { | |||
| return NSMakeRange ((NSUInteger) range.getStart(), (NSUInteger) range.getLength()); | |||
| } | |||
| inline String nsStringToJuce (NSString* s) | |||
| { | |||
| return CharPointer_UTF8 ([s UTF8String]); | |||
| @@ -60,6 +60,7 @@ | |||
| #include <numeric> | |||
| #include <queue> | |||
| #include <sstream> | |||
| #include <typeindex> | |||
| #include <unordered_set> | |||
| #include <vector> | |||
| @@ -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 <class PointType> | |||
| Point<int> convertToIntPoint (PointType p) noexcept | |||
| { | |||
| return Point<int> (roundToInt (p.x), roundToInt (p.y)); | |||
| } | |||
| #endif | |||
| } | |||
| CGImageRef juce_createCoreGraphicsImage (const Image&, CGColorSpaceRef, bool mustOutliveSource); | |||
| @@ -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<void()> 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<AccessibilityActionType, std::function<void()>> actionMap; | |||
| }; | |||
| } // namespace juce | |||
| @@ -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 | |||
| }; | |||
| } | |||
| @@ -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 | |||
| }; | |||
| } | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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<int> getSelection() const = 0; | |||
| /** Selects a section of the text. */ | |||
| virtual void setSelection (Range<int> newRange) = 0; | |||
| /** Gets the current text insertion position, if supported. */ | |||
| virtual int getTextInsertionOffset() const = 0; | |||
| /** Returns a section of text. */ | |||
| virtual String getText (Range<int> 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<int> getTextBounds (Range<int> textRange) const = 0; | |||
| /** Returns the index of the character at a given position in screen coordinates. */ | |||
| virtual int getOffsetAtPoint (Point<int> point) const = 0; | |||
| }; | |||
| } // namespace juce | |||
| @@ -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 | |||
| @@ -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<AccessibilityHandler*>& 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*> AccessibilityHandler::getChildren() const | |||
| { | |||
| if (! component.isFocusContainer() && component.getParentComponent() != nullptr) | |||
| return {}; | |||
| std::vector<AccessibilityHandler*> 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<int> 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<Component> 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 | |||
| @@ -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<AccessibilityValueInterface> ptr) : value (std::move (ptr)) {} | |||
| Interfaces (std::unique_ptr<AccessibilityTextInterface> ptr) : text (std::move (ptr)) {} | |||
| Interfaces (std::unique_ptr<AccessibilityTableInterface> ptr) : table (std::move (ptr)) {} | |||
| Interfaces (std::unique_ptr<AccessibilityCellInterface> ptr) : cell (std::move (ptr)) {} | |||
| Interfaces (std::unique_ptr<AccessibilityValueInterface> valueIn, | |||
| std::unique_ptr<AccessibilityTextInterface> textIn, | |||
| std::unique_ptr<AccessibilityTableInterface> tableIn, | |||
| std::unique_ptr<AccessibilityCellInterface> cellIn) | |||
| : value (std::move (valueIn)), | |||
| text (std::move (textIn)), | |||
| table (std::move (tableIn)), | |||
| cell (std::move (cellIn)) | |||
| { | |||
| } | |||
| std::unique_ptr<AccessibilityValueInterface> value; | |||
| std::unique_ptr<AccessibilityTextInterface> text; | |||
| std::unique_ptr<AccessibilityTableInterface> table; | |||
| std::unique_ptr<AccessibilityCellInterface> 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<AccessibilityHandler*> 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<int> 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<AccessibilityNativeImpl, DestroyNativeImpl> nativeImpl; | |||
| //============================================================================== | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityHandler) | |||
| }; | |||
| } // namespace juce | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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<SliderValueInterface> (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 | |||
| @@ -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<TableListBoxTableInterface> (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 | |||
| @@ -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<TextEditorTextInterface> (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<int> getSelection() const override { return textEditor.getHighlightedRegion(); } | |||
| void setSelection (Range<int> r) override { textEditor.setHighlightedRegion (r); } | |||
| String getText (Range<int> 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<int> getTextBounds (Range<int> textRange) const override | |||
| { | |||
| auto localRects = textEditor.getTextBounds (textRange); | |||
| RectangleList<int> globalRects; | |||
| std::for_each (localRects.begin(), localRects.end(), | |||
| [&] (const Rectangle<int>& r) { globalRects.add (textEditor.localAreaToGlobal (r)); }); | |||
| return globalRects; | |||
| } | |||
| int getOffsetAtPoint (Point<int> 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 | |||
| @@ -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<TreeViewTableInterface> (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 | |||
| @@ -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<AccessibilityHandler> Button::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<ButtonAccessibilityHandler> (*this); | |||
| } | |||
| } // namespace juce | |||
| @@ -470,6 +470,8 @@ protected: | |||
| void focusLost (FocusChangeType) override; | |||
| /** @internal */ | |||
| void enablementChanged() override; | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| private: | |||
| //============================================================================== | |||
| @@ -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)) | |||
| { | |||
| if (sendParentEvents) | |||
| { | |||
| const WeakReference<Component> thisPointer (this); | |||
| const WeakReference<Component> safeThis (this); | |||
| giveAwayFocus (sendChildEvents || currentlyFocusedComponent != child); | |||
| child->giveAwayKeyboardFocusInternal (sendChildEvents || currentlyFocusedComponent != child); | |||
| if (thisPointer == nullptr) | |||
| if (sendParentEvents) | |||
| { | |||
| 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<float> 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<Component> (this)); | |||
| internalKeyboardFocusGain (cause, WeakReference<Component> (this)); | |||
| } | |||
| void Component::internalFocusGain (FocusChangeType cause, const WeakReference<Component>& safePointer) | |||
| void Component::internalKeyboardFocusGain (FocusChangeType cause, | |||
| const WeakReference<Component>& 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<Component> 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<Component>& safePointer) | |||
| void Component::internalChildKeyboardFocusChange (FocusChangeType cause, | |||
| const WeakReference<Component>& 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<Component> (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 <typename FocusContainerFn> | |||
| 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<ComponentTraverser> Component::createFocusTraverser() | |||
| { | |||
| if (flags.isFocusContainerFlag || parentComponent == nullptr) | |||
| return new KeyboardFocusTraverser(); | |||
| return std::make_unique<FocusTraverser>(); | |||
| return parentComponent->createFocusTraverser(); | |||
| } | |||
| std::unique_ptr<ComponentTraverser> Component::createKeyboardFocusTraverser() | |||
| { | |||
| if (flags.isKeyboardFocusContainerFlag || parentComponent == nullptr) | |||
| return std::make_unique<KeyboardFocusTraverser>(); | |||
| 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<Component> safePointer (this); | |||
| peer->grabFocus(); | |||
| const WeakReference<Component> safePointer (this); | |||
| peer->grabFocus(); | |||
| if (peer->isFocused() && currentlyFocusedComponent != this) | |||
| { | |||
| WeakReference<Component> componentLosingFocus (currentlyFocusedComponent); | |||
| currentlyFocusedComponent = this; | |||
| if (! peer->isFocused() || currentlyFocusedComponent == this) | |||
| return; | |||
| Desktop::getInstance().triggerFocusCallback(); | |||
| WeakReference<Component> 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 (flags.wantsFocusFlag && (isEnabled() || parentComponent == nullptr)) | |||
| { | |||
| 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<KeyboardFocusTraverser> traverser (createFocusTraverser()); | |||
| if (! isShowing()) | |||
| return; | |||
| if (traverser != nullptr) | |||
| { | |||
| auto* defaultComp = traverser->getDefaultComponent (this); | |||
| traverser.reset(); | |||
| if (flags.wantsKeyboardFocusFlag | |||
| && (isEnabled() || parentComponent == nullptr)) | |||
| { | |||
| takeKeyboardFocus (cause); | |||
| return; | |||
| } | |||
| if (defaultComp != nullptr) | |||
| { | |||
| defaultComp->grabFocusInternal (cause, false); | |||
| return; | |||
| } | |||
| } | |||
| if (isParentOf (currentlyFocusedComponent) && currentlyFocusedComponent->isShowing()) | |||
| 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); | |||
| } | |||
| } | |||
| if (auto traverser = createKeyboardFocusTraverser()) | |||
| { | |||
| if (auto* defaultComp = traverser->getDefaultComponent (this)) | |||
| { | |||
| 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<KeyboardFocusTraverser> 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 (auto* focusContainer = findKeyboardFocusContainer()) | |||
| { | |||
| auto allFocusableComponents = traverser->getAllComponents (focusContainer); | |||
| if (! allFocusableComponents.empty()) | |||
| return moveToNext ? allFocusableComponents.front() | |||
| : allFocusableComponents.back(); | |||
| } | |||
| return nullptr; | |||
| }; | |||
| if (nextComp != 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<AccessibilityHandler> Component::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*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 | |||
| @@ -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,97 +1385,61 @@ 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; | |||
| /** Returns the component that currently has the keyboard focus. | |||
| @returns the focused component, or null if nothing is focused. | |||
| */ | |||
| static Component* JUCE_CALLTYPE getCurrentlyFocusedComponent() noexcept; | |||
| /** If any component has keyboard focus, this will defocus it. */ | |||
| static void JUCE_CALLTYPE unfocusAllComponents(); | |||
| //============================================================================== | |||
| /** 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. (This | |||
| is the method that is used when shifting focus by pressing the tab key). | |||
| 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(). | |||
| Components for which getWantsKeyboardFocus() returns false are not looked at. | |||
| 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, setFocusContainer, setWantsKeyboardFocus | |||
| @see grabKeyboardFocus, giveAwayKeyboardFocus, setFocusContainer, setWantsKeyboardFocus | |||
| */ | |||
| void moveKeyboardFocusToSibling (bool moveToNext); | |||
| /** Creates a KeyboardFocusTraverser object to use to determine the logic by | |||
| which 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. | |||
| 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. | |||
| */ | |||
| 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. | |||
| /** Returns the component that currently has the keyboard focus. | |||
| @see getExplicitFocusOrder, moveKeyboardFocusToSibling | |||
| @returns the focused component, or nullptr if nothing is focused. | |||
| */ | |||
| void setExplicitFocusOrder (int newFocusOrderIndex); | |||
| static Component* JUCE_CALLTYPE getCurrentlyFocusedComponent() noexcept; | |||
| /** Indicates whether this component is a parent for components that can have | |||
| their focus traversed. | |||
| /** If any component has keyboard focus, this will defocus it. */ | |||
| static void JUCE_CALLTYPE unfocusAllComponents(); | |||
| 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. | |||
| //============================================================================== | |||
| /** Creates a ComponentTraverser object to determine the logic by which focus should be | |||
| passed from this component. | |||
| 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. | |||
| 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. | |||
| @see isFocusContainer, createFocusTraverser, moveKeyboardFocusToSibling | |||
| 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 setFocusContainer (bool shouldBeFocusContainer) noexcept; | |||
| virtual std::unique_ptr<ComponentTraverser> createFocusTraverser(); | |||
| /** Returns true if this component has been marked as a focus container. | |||
| /** Creates a ComponentTraverser object to use to determine the logic by which keyboard | |||
| focus should be passed from this component. | |||
| See setFocusContainer() for more details. | |||
| 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. | |||
| @see setFocusContainer, moveKeyboardFocusToSibling, createFocusTraverser | |||
| 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. | |||
| */ | |||
| bool isFocusContainer() const noexcept; | |||
| virtual std::unique_ptr<ComponentTraverser> 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<AccessibilityHandler> 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<int> boundsRelativeToParent; | |||
| std::unique_ptr<Positioner> positioner; | |||
| @@ -2314,29 +2491,33 @@ private: | |||
| friend class WeakReference<Component>; | |||
| WeakReference<Component>::Master masterReference; | |||
| std::unique_ptr<AccessibilityHandler> 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<float>, Time, const MouseWheelDetails&); | |||
| void internalMagnifyGesture (MouseInputSource, Point<float>, Time, float); | |||
| void internalBroughtToFront(); | |||
| void internalFocusGain (FocusChangeType, const WeakReference<Component>&); | |||
| void internalFocusGain (FocusChangeType); | |||
| void internalFocusLoss (FocusChangeType); | |||
| void internalChildFocusChange (FocusChangeType, const WeakReference<Component>&); | |||
| void internalKeyboardFocusGain (FocusChangeType, const WeakReference<Component>&); | |||
| void internalKeyboardFocusGain (FocusChangeType); | |||
| void internalKeyboardFocusLoss (FocusChangeType); | |||
| void internalChildKeyboardFocusChange (FocusChangeType, const WeakReference<Component>&); | |||
| 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(); | |||
| @@ -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<Component*> getAllComponents (Component* parentComponent) = 0; | |||
| }; | |||
| } // namespace juce | |||
| @@ -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<int>::max(); | |||
| } | |||
| template <typename FocusContainerFn> | |||
| static void findAllComponents (Component* parent, | |||
| std::vector<Component*>& components, | |||
| FocusContainerFn isFocusContainer) | |||
| { | |||
| if (parent == nullptr || parent->getNumChildComponents() == 0) | |||
| return; | |||
| std::vector<Component*> 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 <typename FocusContainerFn> | |||
| static Component* navigateFocus (Component* current, | |||
| Component* focusContainer, | |||
| NavigationDirection direction, | |||
| FocusContainerFn isFocusContainer) | |||
| { | |||
| if (focusContainer != nullptr) | |||
| { | |||
| std::vector<Component*> 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<Component*> components; | |||
| FocusHelpers::findAllComponents (parentComponent, | |||
| components, | |||
| &Component::isFocusContainer); | |||
| if (! components.empty()) | |||
| return components.front(); | |||
| } | |||
| return nullptr; | |||
| } | |||
| std::vector<Component*> FocusTraverser::getAllComponents (Component* parentComponent) | |||
| { | |||
| std::vector<Component*> 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<Component, 10> children; | |||
| }; | |||
| void checkComponentProperties (std::function<void (Component&)>&& childFn, | |||
| std::function<bool (const Component&, const Component&)>&& 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<void(Component&)>& 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 | |||
| @@ -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<Component*> getAllComponents (Component* parentComponent) override; | |||
| }; | |||
| } // namespace juce | |||
| @@ -204,4 +204,10 @@ std::unique_ptr<Drawable> Drawable::createFromImageFile (const File& file) | |||
| return {}; | |||
| } | |||
| //============================================================================== | |||
| std::unique_ptr<AccessibilityHandler> Drawable::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::ignored); | |||
| } | |||
| } // namespace juce | |||
| @@ -199,6 +199,8 @@ protected: | |||
| void setBoundsToEnclose (Rectangle<float>); | |||
| /** @internal */ | |||
| void applyDrawableClipPath (Graphics&); | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| Point<int> originRelativeToComponent; | |||
| std::unique_ptr<Drawable> drawableClipPath; | |||
| @@ -133,4 +133,10 @@ Path DrawableImage::getOutlineAsPath() const | |||
| return {}; // not applicable for images | |||
| } | |||
| //============================================================================== | |||
| std::unique_ptr<AccessibilityHandler> DrawableImage::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::image); | |||
| } | |||
| } // namespace juce | |||
| @@ -94,6 +94,8 @@ public: | |||
| Rectangle<float> getDrawableBounds() const override; | |||
| /** @internal */ | |||
| Path getOutlineAsPath() const override; | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| private: | |||
| //============================================================================== | |||
| @@ -208,4 +208,25 @@ bool DrawableText::replaceColour (Colour originalColour, Colour replacementColou | |||
| return true; | |||
| } | |||
| //============================================================================== | |||
| std::unique_ptr<AccessibilityHandler> 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<DrawableTextAccessibilityHandler> (*this); | |||
| } | |||
| } // namespace juce | |||
| @@ -98,6 +98,8 @@ public: | |||
| Path getOutlineAsPath() const override; | |||
| /** @internal */ | |||
| bool replaceColour (Colour originalColour, Colour replacementColour) override; | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| private: | |||
| //============================================================================== | |||
| @@ -616,4 +616,10 @@ void FileBrowserComponent::timerCallback() | |||
| } | |||
| } | |||
| //============================================================================== | |||
| std::unique_ptr<AccessibilityHandler> FileBrowserComponent::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||
| } | |||
| } // namespace juce | |||
| @@ -252,6 +252,8 @@ public: | |||
| FilePreviewComponent* getPreviewComponent() const noexcept; | |||
| /** @internal */ | |||
| DirectoryContentsDisplayComponent* getDisplayComponent() const noexcept; | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| protected: | |||
| /** Returns a list of names and paths for the default places the user might want to look. | |||
| @@ -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<AccessibilityHandler> 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) | |||
| { | |||
| } | |||
| @@ -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; | |||
| @@ -190,6 +190,11 @@ public: | |||
| indexInContentsList, owner); | |||
| } | |||
| String getAccessibilityName() override | |||
| { | |||
| return file.getFileName(); | |||
| } | |||
| void itemClicked (const MouseEvent& e) override | |||
| { | |||
| owner.sendMouseClickMessage (file, e); | |||
| @@ -70,11 +70,11 @@ void FilenameComponent::resized() | |||
| getLookAndFeel().layoutFilenameComponent (*this, &filenameBox, browseButton.get()); | |||
| } | |||
| KeyboardFocusTraverser* FilenameComponent::createFocusTraverser() | |||
| std::unique_ptr<ComponentTraverser> 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) | |||
| @@ -212,7 +212,7 @@ public: | |||
| /** @internal */ | |||
| void fileDragExit (const StringArray&) override; | |||
| /** @internal */ | |||
| KeyboardFocusTraverser* createFocusTraverser() override; | |||
| std::unique_ptr<ComponentTraverser> createKeyboardFocusTraverser() override; | |||
| private: | |||
| //============================================================================== | |||
| @@ -117,4 +117,10 @@ void ImagePreviewComponent::paint (Graphics& g) | |||
| } | |||
| } | |||
| //============================================================================== | |||
| std::unique_ptr<AccessibilityHandler> ImagePreviewComponent::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::image); | |||
| } | |||
| } // namespace juce | |||
| @@ -53,6 +53,8 @@ public: | |||
| void paint (Graphics&) override; | |||
| /** @internal */ | |||
| void timerCallback() override; | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| private: | |||
| File fileToLoad; | |||
| @@ -66,6 +66,8 @@ | |||
| #include <windowsx.h> | |||
| #include <vfw.h> | |||
| #include <commdlg.h> | |||
| #include <UIAutomation.h> | |||
| #include <sapi.h> | |||
| #if JUCE_WEB_BROWSER | |||
| #include <exdisp.h> | |||
| @@ -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" | |||
| @@ -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 | |||
| @@ -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) | |||
| { | |||
| return comp->getWantsKeyboardFocus() && container->isParentOf (comp); | |||
| } | |||
| static Component* traverse (Component* current, Component* container, | |||
| FocusHelpers::NavigationDirection direction) | |||
| { | |||
| auto order = c->getExplicitFocusOrder(); | |||
| return order > 0 ? order : (std::numeric_limits<int>::max() / 2); | |||
| if (auto* comp = FocusHelpers::navigateFocus (current, container, direction, | |||
| &Component::isKeyboardFocusContainer)) | |||
| { | |||
| if (isKeyboardFocusable (comp, container)) | |||
| return comp; | |||
| return traverse (comp, container, direction); | |||
| } | |||
| return nullptr; | |||
| } | |||
| } | |||
| Component* KeyboardFocusTraverser::getNextComponent (Component* current) | |||
| { | |||
| return KeyboardFocusTraverserHelpers::traverse (current, current->findKeyboardFocusContainer(), | |||
| FocusHelpers::NavigationDirection::forwards); | |||
| } | |||
| Component* KeyboardFocusTraverser::getPreviousComponent (Component* current) | |||
| { | |||
| return KeyboardFocusTraverserHelpers::traverse (current, current->findKeyboardFocusContainer(), | |||
| FocusHelpers::NavigationDirection::backwards); | |||
| } | |||
| Component* KeyboardFocusTraverser::getDefaultComponent (Component* parentComponent) | |||
| { | |||
| for (auto* comp : getAllComponents (parentComponent)) | |||
| if (KeyboardFocusTraverserHelpers::isKeyboardFocusable (comp, parentComponent)) | |||
| return comp; | |||
| return nullptr; | |||
| } | |||
| static void findAllFocusableComponents (Component* parent, Array<Component*>& comps) | |||
| std::vector<Component*> KeyboardFocusTraverser::getAllComponents (Component* parentComponent) | |||
| { | |||
| std::vector<Component*> components; | |||
| FocusHelpers::findAllComponents (parentComponent, | |||
| components, | |||
| &Component::isKeyboardFocusContainer); | |||
| auto removePredicate = [parentComponent] (const Component* comp) | |||
| { | |||
| if (parent->getNumChildComponents() != 0) | |||
| 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"); | |||
| { | |||
| Array<Component*> localComps; | |||
| 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* c : parent->getChildren()) | |||
| if (c->isVisible() && c->isEnabled()) | |||
| localComps.add (c); | |||
| for (auto* focusChild : focusChildren) | |||
| expect (std::find (allComponents.cbegin(), allComponents.cend(), focusChild) != allComponents.cend()); | |||
| // 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* componentToTest = traverser.getDefaultComponent (&parent); | |||
| for (;;) | |||
| { | |||
| auto explicitOrder1 = getOrder (a); | |||
| auto explicitOrder2 = getOrder (b); | |||
| expect (componentToTest->getWantsKeyboardFocus()); | |||
| expect (std::find (std::begin (focusChildren), std::end (focusChildren), componentToTest) != std::end (focusChildren)); | |||
| componentToTest = traverser.getNextComponent (componentToTest); | |||
| if (explicitOrder1 != explicitOrder2) | |||
| return explicitOrder1 < explicitOrder2; | |||
| if (componentToTest == nullptr) | |||
| break; | |||
| } | |||
| if (a->getY() != b->getY()) | |||
| return a->getY() < b->getY(); | |||
| int focusOrder = 1; | |||
| for (auto* focusChild : focusChildren) | |||
| focusChild->setExplicitFocusOrder (focusOrder++); | |||
| return a->getX() < b->getX(); | |||
| }); | |||
| componentToTest = traverser.getDefaultComponent (&parent); | |||
| for (auto* c : localComps) | |||
| for (auto* focusChild : focusChildren) | |||
| { | |||
| if (c->getWantsKeyboardFocus()) | |||
| comps.add (c); | |||
| expect (componentToTest == focusChild); | |||
| expect (componentToTest->getWantsKeyboardFocus()); | |||
| if (! c->isFocusContainer()) | |||
| findAllFocusableComponents (c, comps); | |||
| componentToTest = traverser.getNextComponent (componentToTest); | |||
| } | |||
| } | |||
| } | |||
| static Component* findFocusContainer (Component* c) | |||
| { | |||
| c = c->getParentComponent(); | |||
| beginTest ("Single nested child wants keyboard focus"); | |||
| { | |||
| TestComponent parent; | |||
| Component grandparent; | |||
| if (c != nullptr) | |||
| while (c->getParentComponent() != nullptr && ! c->isFocusContainer()) | |||
| c = c->getParentComponent(); | |||
| grandparent.addAndMakeVisible (parent); | |||
| return c; | |||
| } | |||
| auto& focusChild = parent.children[5]; | |||
| static Component* getIncrementedComponent (Component* current, int delta) | |||
| { | |||
| if (auto* focusContainer = findFocusContainer (current)) | |||
| 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"); | |||
| { | |||
| Array<Component*> comps; | |||
| KeyboardFocusHelpers::findAllFocusableComponents (focusContainer, comps); | |||
| 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; })); | |||
| if (! comps.isEmpty()) | |||
| 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<Component, 3> otherParents; | |||
| for (auto& p : otherParents) | |||
| { | |||
| auto index = comps.indexOf (current); | |||
| return comps [(index + comps.size() + delta) % comps.size()]; | |||
| grandparent.addAndMakeVisible (p); | |||
| p.setWantsKeyboardFocus (true); | |||
| } | |||
| } | |||
| return nullptr; | |||
| } | |||
| } | |||
| 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)); | |||
| //============================================================================== | |||
| KeyboardFocusTraverser::KeyboardFocusTraverser() {} | |||
| KeyboardFocusTraverser::~KeyboardFocusTraverser() {} | |||
| for (auto* focusChild : focusChildren) | |||
| focusChild->setWantsKeyboardFocus (false); | |||
| Component* KeyboardFocusTraverser::getNextComponent (Component* current) | |||
| { | |||
| jassert (current != nullptr); | |||
| return KeyboardFocusHelpers::getIncrementedComponent (current, 1); | |||
| } | |||
| expect (traverser.getDefaultComponent (&grandparent) == &otherParents.front()); | |||
| expect (traverser.getDefaultComponent (&parent) == nullptr); | |||
| expect (traverser.getAllComponents (&grandparent).size() == otherParents.size()); | |||
| expect (traverser.getAllComponents (&parent).empty()); | |||
| } | |||
| } | |||
| Component* KeyboardFocusTraverser::getPreviousComponent (Component* current) | |||
| { | |||
| jassert (current != nullptr); | |||
| return KeyboardFocusHelpers::getIncrementedComponent (current, -1); | |||
| } | |||
| private: | |||
| struct TestComponent : public Component | |||
| { | |||
| TestComponent() | |||
| { | |||
| for (auto& child : children) | |||
| addAndMakeVisible (child); | |||
| } | |||
| Component* KeyboardFocusTraverser::getDefaultComponent (Component* parentComponent) | |||
| { | |||
| Array<Component*> comps; | |||
| std::array<Component, 10> children; | |||
| }; | |||
| if (parentComponent != nullptr) | |||
| KeyboardFocusHelpers::findAllFocusableComponents (parentComponent, comps); | |||
| KeyboardFocusTraverser traverser; | |||
| }; | |||
| return comps.getFirst(); | |||
| } | |||
| static KeyboardFocusTraverserTests keyboardFocusTraverserTests; | |||
| #endif | |||
| } // namespace juce | |||
| @@ -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(); | |||
| /** Returns the component that should be given focus after the specified one | |||
| when moving "forwards". | |||
| ~KeyboardFocusTraverser() override = default; | |||
| The default implementation will return the next component which is to the | |||
| right of or below this one. | |||
| /** Returns the component that should receive keyboard focus by default within the | |||
| given parent component. | |||
| 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); | |||
| /** Returns the component that should be given focus after the specified one | |||
| when moving "backwards". | |||
| Component* getDefaultComponent (Component* parentComponent) override; | |||
| The default implementation will return the next component which is to the | |||
| left of or above this one. | |||
| /** Returns the component that should be given keyboard focus after the specified | |||
| one when moving "forwards". | |||
| 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 return the previous focusable component (as | |||
| determined by FocusTraverser) that also wants keyboard focus, or nullptr if | |||
| there is no suitable component. | |||
| */ | |||
| Component* getPreviousComponent (Component* current) override; | |||
| The default implementation will just return the foremost child component that | |||
| wants focus. | |||
| /** Returns all of the components that can receive keyboard focus within the given | |||
| parent component in traversal order. | |||
| This may return nullptr if there's no suitable candidate. | |||
| The default implementation will return all focusable child components (as | |||
| determined by FocusTraverser) that also wants keyboard focus. | |||
| */ | |||
| virtual Component* getDefaultComponent (Component* parentComponent); | |||
| std::vector<Component*> getAllComponents (Component* parentComponent) override; | |||
| }; | |||
| } // namespace juce | |||
| @@ -150,6 +150,7 @@ public: | |||
| { | |||
| ProxyComponent (Component& c) | |||
| { | |||
| setAccessible (false); | |||
| setWantsKeyboardFocus (false); | |||
| setBounds (c.getBounds()); | |||
| setTransform (c.getTransform()); | |||
| @@ -459,4 +459,10 @@ void ConcertinaPanel::panelHeaderDoubleClicked (Component* component) | |||
| setPanelSize (component, 0, true); | |||
| } | |||
| //============================================================================== | |||
| std::unique_ptr<AccessibilityHandler> ConcertinaPanel::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||
| } | |||
| } // namespace juce | |||
| @@ -119,6 +119,10 @@ public: | |||
| ConcertinaPanel&, Component&) = 0; | |||
| }; | |||
| //============================================================================== | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| private: | |||
| void resized() override; | |||
| @@ -69,4 +69,10 @@ void GroupComponent::paint (Graphics& g) | |||
| void GroupComponent::enablementChanged() { repaint(); } | |||
| void GroupComponent::colourChanged() { repaint(); } | |||
| //============================================================================== | |||
| std::unique_ptr<AccessibilityHandler> GroupComponent::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||
| } | |||
| } // namespace juce | |||
| @@ -98,6 +98,8 @@ public: | |||
| void enablementChanged() override; | |||
| /** @internal */ | |||
| void colourChanged() override; | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| private: | |||
| String text; | |||
| @@ -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<AccessibilityHandler> 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<AccessibilityHandler> (*this, | |||
| AccessibilityRole::scrollBar, | |||
| AccessibilityActions{}, | |||
| AccessibilityHandler::Interfaces { std::make_unique<ScrollBarValueInterface> (*this) }); | |||
| } | |||
| } // namespace juce | |||
| @@ -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<AccessibilityHandler> createAccessibilityHandler() override; | |||
| private: | |||
| //============================================================================== | |||
| @@ -294,4 +294,10 @@ bool SidePanel::isMouseEventInThisOrChildren (Component* eventComponent) | |||
| return false; | |||
| } | |||
| //============================================================================== | |||
| std::unique_ptr<AccessibilityHandler> SidePanel::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||
| } | |||
| } // namespace juce | |||
| @@ -195,6 +195,8 @@ public: | |||
| void mouseDrag (const MouseEvent&) override; | |||
| /** @internal */ | |||
| void mouseUp (const MouseEvent&) override; | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| private: | |||
| //============================================================================== | |||
| @@ -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<AccessibilityHandler> TabbedButtonBar::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||
| } | |||
| } // namespace juce | |||
| @@ -334,6 +334,8 @@ public: | |||
| void resized() override; | |||
| /** @internal */ | |||
| void lookAndFeelChanged() override; | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| protected: | |||
| //============================================================================== | |||
| @@ -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<AccessibilityHandler> TabbedComponent::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||
| } | |||
| } // namespace juce | |||
| @@ -197,6 +197,8 @@ public: | |||
| void resized() override; | |||
| /** @internal */ | |||
| void lookAndFeelChanged() override; | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| protected: | |||
| //============================================================================== | |||
| @@ -293,4 +293,10 @@ void BurgerMenuComponent::lookAndFeelChanged() | |||
| listBox.setRowHeight (roundToInt (getLookAndFeel().getPopupMenuFont().getHeight() * 2.0f)); | |||
| } | |||
| //============================================================================== | |||
| std::unique_ptr<AccessibilityHandler> BurgerMenuComponent::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::menuBar); | |||
| } | |||
| } // namespace juce | |||
| @@ -71,6 +71,8 @@ public: | |||
| /** @internal */ | |||
| void lookAndFeelChanged() override; | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| private: | |||
| //============================================================================== | |||
| @@ -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<AccessibilityHandler> 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<ComponentHandler> (*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); | |||
| g.setOrigin (xPositions [i], 0); | |||
| g.reduceClipRegion (0, 0, xPositions[i + 1] - xPositions[i], getHeight()); | |||
| getLookAndFeel().drawMenuBarItem (g, | |||
| xPositions[i + 1] - xPositions[i], | |||
| getHeight(), | |||
| i, | |||
| menuNames[i], | |||
| i == itemUnderMouse, | |||
| i == currentPopupIndex, | |||
| isMouseOverBar, | |||
| *this); | |||
| } | |||
| const auto& itemComponent = itemComponents[i]; | |||
| const auto itemBounds = itemComponent->getBounds(); | |||
| Graphics::ScopedSaveState ss (g); | |||
| 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<int> 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<int> 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<int> 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<AccessibleItemComponent> (*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<AccessibleItemComponent>& 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<AccessibilityHandler> 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<MenuBarComponentAccessibilityHandler> (*this); | |||
| } | |||
| } // namespace juce | |||
| @@ -95,24 +95,32 @@ public: | |||
| void menuBarItemsChanged (MenuBarModel*) override; | |||
| /** @internal */ | |||
| void menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo&) override; | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| private: | |||
| //============================================================================== | |||
| MenuBarModel* model; | |||
| class AccessibleItemComponent; | |||
| StringArray menuNames; | |||
| Array<int> xPositions; | |||
| Point<int> lastMousePos; | |||
| int itemUnderMouse, currentPopupIndex, topLevelIndexClicked; | |||
| //============================================================================== | |||
| void timerCallback() override; | |||
| int getItemAt (Point<int>); | |||
| void setItemUnderMouse (int index); | |||
| void setOpenItem (int index); | |||
| void setItemUnderMouse (int); | |||
| void setOpenItem (int); | |||
| void updateItemUnderMouse (Point<int>); | |||
| 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<std::unique_ptr<AccessibleItemComponent>> itemComponents; | |||
| Point<int> lastMousePos; | |||
| int itemUnderMouse = -1, currentPopupIndex = -1, topLevelIndexClicked = 0; | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuBarComponent) | |||
| }; | |||
| @@ -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<AccessibilityHandler> createAccessibilityHandler() override | |||
| { | |||
| return item.isSeparator ? nullptr : std::make_unique<ItemAccessibilityHandler> (*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<CustomComponent> 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(); | |||
| ensureItemComponentIsVisible (*item, isPositiveAndBelow (y, windowPos.getHeight()) ? y : -1); | |||
| auto y = targetPosition.getY() - windowPos.getY(); | |||
| ensureItemIsVisible (visibleID, 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)) | |||
| { | |||
| if (m->item.itemID == itemID | |||
| && windowPos.getHeight() > PopupMenuSettings::scrollZone * 4) | |||
| { | |||
| auto currentY = m->getY(); | |||
| auto currentY = itemComp.getY(); | |||
| 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; | |||
| if (wantedY > 0 || currentY < 0 || itemComp.getBottom() > windowPos.getHeight()) | |||
| { | |||
| if (wantedY < 0) | |||
| wantedY = jlimit (PopupMenuSettings::scrollZone, | |||
| jmax (PopupMenuSettings::scrollZone, | |||
| windowPos.getHeight() - (PopupMenuSettings::scrollZone + itemComp.getHeight())), | |||
| currentY); | |||
| windowPos.setSize (jmin (windowPos.getWidth(), parentArea.getWidth()), | |||
| jmin (windowPos.getHeight(), parentArea.getHeight())); | |||
| auto parentArea = getParentArea (windowPos.getPosition(), parentComponent) / scaleFactor; | |||
| auto deltaY = wantedY - currentY; | |||
| auto newY = jlimit (parentArea.getY(), | |||
| parentArea.getBottom() - windowPos.getHeight(), | |||
| windowPos.getY() + deltaY); | |||
| windowPos.setSize (jmin (windowPos.getWidth(), parentArea.getWidth()), | |||
| jmin (windowPos.getHeight(), parentArea.getHeight())); | |||
| deltaY -= newY - windowPos.getY(); | |||
| auto newY = jlimit (parentArea.getY(), | |||
| parentArea.getBottom() - windowPos.getHeight(), | |||
| windowPos.getY() + deltaY); | |||
| childYOffset -= deltaY; | |||
| windowPos.setPosition (windowPos.getX(), newY); | |||
| deltaY -= newY - windowPos.getY(); | |||
| updateYPositions(); | |||
| } | |||
| childYOffset -= deltaY; | |||
| windowPos.setPosition (windowPos.getX(), newY); | |||
| 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<AccessibilityHandler> createAccessibilityHandler() override | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*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(); | |||
| @@ -156,6 +156,10 @@ public: | |||
| const Rectangle<float>& 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<int> content; | |||
| Point<int> arrowTip; | |||
| @@ -33,6 +33,7 @@ public: | |||
| : target (comp), shadow (ds) | |||
| { | |||
| setVisible (true); | |||
| setAccessible (false); | |||
| setInterceptsMouseClicks (false, false); | |||
| if (comp->isOnDesktop()) | |||
| @@ -188,6 +188,12 @@ void JUCESplashScreen::mouseUp (const MouseEvent&) | |||
| juceWebsite.launchInDefaultBrowser(); | |||
| } | |||
| //============================================================================== | |||
| std::unique_ptr<AccessibilityHandler> JUCESplashScreen::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::splashScreen); | |||
| } | |||
| // END SECTION A | |||
| } // namespace juce | |||
| @@ -55,6 +55,10 @@ public: | |||
| static std::unique_ptr<Drawable> getSplashScreenLogo(); | |||
| //============================================================================== | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| private: | |||
| void paint (Graphics&) override; | |||
| void timerCallback() override; | |||
| @@ -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<IRawElementProviderSimple> provider; | |||
| accessibilityElement->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress())); | |||
| wrapper->disconnectProvider (provider); | |||
| if (--providerCount == 0) | |||
| wrapper->disconnectAllProviders(); | |||
| } | |||
| } | |||
| //============================================================================== | |||
| ComSmartPtr<AccessibilityNativeHandle> 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 <typename Callback> | |||
| void getProviderWithCheckedWrapper (const AccessibilityHandler& handler, Callback&& callback) | |||
| { | |||
| if (isStartingUpOrShuttingDown() || ! isHandlerValid (handler)) | |||
| return; | |||
| if (auto* wrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) | |||
| { | |||
| if (! wrapper->clientsAreListening()) | |||
| return; | |||
| ComSmartPtr<IRawElementProviderSimple> 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<IRawElementProviderSimple>& provider) | |||
| { | |||
| wrapper->raiseAutomationEvent (provider, event); | |||
| }); | |||
| } | |||
| void sendAccessibilityPropertyChangedEvent (const AccessibilityHandler& handler, PROPERTYID property, VARIANT newValue) | |||
| { | |||
| jassert (property != PROPERTYID{}); | |||
| getProviderWithCheckedWrapper (handler, [property, newValue] (WindowsUIAWrapper* wrapper, ComSmartPtr<IRawElementProviderSimple>& 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<ISpVoice> 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<long> (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<IRawElementProviderSimple> 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 | |||
| @@ -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 : "<empty>"); | |||
| } | |||
| 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<int> (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 | |||
| @@ -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<IRawElementProviderSimple, | |||
| IRawElementProviderFragment, | |||
| IRawElementProviderFragmentRoot> | |||
| { | |||
| 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<int, 2> rtid { UiaAppendRuntimeId, ++idCounter }; | |||
| bool valid = true; | |||
| //============================================================================== | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityNativeHandle) | |||
| }; | |||
| } | |||
| @@ -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<IExpandCollapseProvider> | |||
| { | |||
| 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 | |||
| @@ -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<IGridItemProvider> | |||
| { | |||
| 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 <typename Value, typename Callback> | |||
| 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 | |||
| @@ -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<IGridProvider> | |||
| { | |||
| 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 <typename Value, typename Callback> | |||
| 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 | |||
| @@ -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<const AccessibilityHandler*>& 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<IRawElementProviderSimple> 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 <typename Value, typename Object, typename Callback> | |||
| 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 | |||
| @@ -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<IInvokeProvider> | |||
| { | |||
| 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 | |||
| @@ -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<AccessibilityNativeHandle> nativeHandle; | |||
| //============================================================================== | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAProviderBase) | |||
| }; | |||
| } // namespace juce | |||
| @@ -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" | |||
| @@ -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<IRangeValueProvider> | |||
| { | |||
| 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 <typename Value, typename Callback> | |||
| 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 | |||
| @@ -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<ISelectionItemProvider> | |||
| { | |||
| 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<ISelectionProvider2> | |||
| { | |||
| public: | |||
| explicit UIASelectionProvider (AccessibilityNativeHandle* nativeHandle) | |||
| : UIAProviderBase (nativeHandle) | |||
| { | |||
| } | |||
| //============================================================================== | |||
| JUCE_COMRESULT QueryInterface (REFIID iid, void** result) override | |||
| { | |||
| if (iid == _uuidof (IUnknown) || iid == _uuidof (ISelectionProvider)) | |||
| return castToType<ISelectionProvider> (result); | |||
| if (iid == _uuidof (ISelectionProvider2)) | |||
| return castToType<ISelectionProvider2> (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<const AccessibilityHandler*> getSelectedChildren() const | |||
| { | |||
| std::vector<const AccessibilityHandler*> 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 | |||
| @@ -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<ITextProvider2> | |||
| { | |||
| public: | |||
| explicit UIATextProvider (AccessibilityNativeHandle* nativeHandle) | |||
| : UIAProviderBase (nativeHandle) | |||
| { | |||
| } | |||
| //============================================================================== | |||
| JUCE_COMRESULT QueryInterface (REFIID iid, void** result) override | |||
| { | |||
| if (iid == _uuidof (IUnknown) || iid == _uuidof (ITextProvider)) | |||
| return castToType<ITextProvider> (result); | |||
| if (iid == _uuidof (ITextProvider2)) | |||
| return castToType<ITextProvider2> (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<IUnknown*> (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<IUnknown*> (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 <typename Value, typename Callback> | |||
| 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<ITextRangeProvider> | |||
| { | |||
| public: | |||
| UIATextRangeProvider (UIATextProvider& textProvider, Range<int> range) | |||
| : UIAProviderBase (textProvider.getHandler().getNativeImplementation()), | |||
| owner (&textProvider), | |||
| selectionRange (range) | |||
| { | |||
| } | |||
| //============================================================================== | |||
| Range<int> 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<UIATextRangeProvider*> (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<UIATextRangeProvider*> (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<int> (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<void**> (&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<UIATextRangeProvider*> (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<UIATextProvider> owner; | |||
| Range<int> selectionRange; | |||
| //============================================================================== | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIATextRangeProvider) | |||
| }; | |||
| //============================================================================== | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIATextProvider) | |||
| }; | |||
| } // namespace juce | |||
| @@ -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<IToggleProvider> | |||
| { | |||
| 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 | |||
| @@ -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<ITransformProvider> | |||
| { | |||
| 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<int>::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 | |||
| @@ -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<IValueProvider> | |||
| { | |||
| 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 | |||
| @@ -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<IWindowProvider> | |||
| { | |||
| 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 | |||
| @@ -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<IRawElementProviderSimple*> disconnectingProviderSetter (disconnectingProvider, provider); | |||
| return uiaDisconnectProvider (provider); | |||
| } | |||
| return UIA_E_NOTSUPPORTED; | |||
| } | |||
| JUCE_COMRESULT disconnectAllProviders() | |||
| { | |||
| if (uiaDisconnectAllProviders != nullptr) | |||
| { | |||
| const ScopedValueSetter<bool> 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<typename FuncType> | |||
| 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<UiaReturnRawElementProviderFunc> (uiaHandle, "UiaReturnRawElementProvider"); | |||
| UiaHostProviderFromHwndFunc uiaHostProviderFromHwnd = getUiaFunction<UiaHostProviderFromHwndFunc> (uiaHandle, "UiaHostProviderFromHwnd"); | |||
| UiaRaiseAutomationPropertyChangedEventFunc uiaRaiseAutomationPropertyChangedEvent = getUiaFunction<UiaRaiseAutomationPropertyChangedEventFunc> (uiaHandle, "UiaRaiseAutomationPropertyChangedEvent"); | |||
| UiaRaiseAutomationEventFunc uiaRaiseAutomationEvent = getUiaFunction<UiaRaiseAutomationEventFunc> (uiaHandle, "UiaRaiseAutomationEvent"); | |||
| UiaClientsAreListeningFunc uiaClientsAreListening = getUiaFunction<UiaClientsAreListeningFunc> (uiaHandle, "UiaClientsAreListening"); | |||
| UiaDisconnectProviderFunc uiaDisconnectProvider = getUiaFunction<UiaDisconnectProviderFunc> (uiaHandle, "UiaDisconnectProvider"); | |||
| UiaDisconnectAllProvidersFunc uiaDisconnectAllProviders = getUiaFunction<UiaDisconnectAllProvidersFunc> (uiaHandle, "UiaDisconnectAllProviders"); | |||
| IRawElementProviderSimple* disconnectingProvider = nullptr; | |||
| bool disconnectingAllProviders = false; | |||
| //============================================================================== | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WindowsUIAWrapper) | |||
| }; | |||
| } // namespace juce | |||
| @@ -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<NSView> | |||
| template <typename Base> | |||
| struct NSViewComponentPeerWrapper : public Base | |||
| { | |||
| JuceNSViewClass() : ObjCClass<NSView> ("JUCEView_") | |||
| { | |||
| addIvar<NSViewComponentPeer*> ("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@:@"); | |||
| 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@:@"); | |||
| explicit NSViewComponentPeerWrapper (const char* baseName) | |||
| : Base (baseName) | |||
| { | |||
| Base::template addIvar<NSViewComponentPeer*> ("owner"); | |||
| } | |||
| static NSViewComponentPeer* getOwner (id self) | |||
| { | |||
| return Base::template getIvar<NSViewComponentPeer*> (self, "owner"); | |||
| } | |||
| static id getAccessibleChild (id self) | |||
| { | |||
| if (auto* owner = getOwner (self)) | |||
| if (auto* handler = owner->getComponent().getAccessibilityHandler()) | |||
| return (id) handler->getNativeImplementation(); | |||
| return nil; | |||
| } | |||
| }; | |||
| struct JuceNSViewClass : public NSViewComponentPeerWrapper<ObjCClass<NSView>> | |||
| { | |||
| 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<NSView> | |||
| } | |||
| private: | |||
| static NSViewComponentPeer* getOwner (id self) | |||
| { | |||
| return getIvar<NSViewComponentPeer*> (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<NSDraggingInfo>) {} | |||
| //============================================================================== | |||
| 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<id> (self, @selector (accessibilityAttributeValue:), attribute); | |||
| } | |||
| }; | |||
| //============================================================================== | |||
| struct JuceNSWindowClass : public ObjCClass<NSWindow> | |||
| struct JuceNSWindowClass : public NSViewComponentPeerWrapper<ObjCClass<NSWindow>> | |||
| { | |||
| JuceNSWindowClass() : ObjCClass<NSWindow> ("JUCEWindow_") | |||
| JuceNSWindowClass() : NSViewComponentPeerWrapper ("JUCEWindow_") | |||
| { | |||
| addIvar<NSViewComponentPeer*> ("owner"); | |||
| addMethod (@selector (canBecomeKeyWindow), canBecomeKeyWindow, "c@:"); | |||
| addMethod (@selector (canBecomeMainWindow), canBecomeMainWindow, "c@:"); | |||
| addMethod (@selector (becomeKeyWindow), becomeKeyWindow, "v@:"); | |||
| @@ -2096,6 +2135,12 @@ struct JuceNSWindowClass : public ObjCClass<NSWindow> | |||
| 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<NSWindow> | |||
| } | |||
| private: | |||
| static NSViewComponentPeer* getOwner (id self) | |||
| { | |||
| return getIvar<NSViewComponentPeer*> (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() | |||
| @@ -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<DWORD> currentTouches; | |||
| @@ -3907,6 +3921,24 @@ private: | |||
| case WM_GETDLGCODE: | |||
| return DLGC_WANTALLKEYS; | |||
| case WM_GETOBJECT: | |||
| { | |||
| if (static_cast<long> (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; | |||
| } | |||
| @@ -204,7 +204,7 @@ void PropertyPanel::init() | |||
| addAndMakeVisible (viewport); | |||
| viewport.setViewedComponent (propertyHolderComponent = new PropertyHolderComponent()); | |||
| viewport.setFocusContainer (true); | |||
| viewport.setFocusContainerType (FocusContainerType::keyboardFocusContainer); | |||
| } | |||
| PropertyPanel::~PropertyPanel() | |||
| @@ -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<AccessibilityHandler> ComboBox::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<ComboBoxAccessibilityHandler> (*this); | |||
| } | |||
| } // namespace juce | |||
| @@ -419,6 +419,8 @@ public: | |||
| void valueChanged (Value&) override; | |||
| /** @internal */ | |||
| void parentHierarchyChanged() override; | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| // These methods' bool parameters have changed: see their new method signatures. | |||
| JUCE_DEPRECATED (void clear (bool)); | |||
| @@ -80,4 +80,10 @@ void ImageComponent::paint (Graphics& g) | |||
| g.drawImage (image, getLocalBounds().toFloat(), placement); | |||
| } | |||
| //============================================================================== | |||
| std::unique_ptr<AccessibilityHandler> ImageComponent::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::image); | |||
| } | |||
| } // namespace juce | |||
| @@ -68,6 +68,8 @@ public: | |||
| //============================================================================== | |||
| /** @internal */ | |||
| void paint (Graphics&) override; | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| private: | |||
| Image image; | |||
| @@ -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* getDefaultComponent (Component* parent) override | |||
| { | |||
| auto getContainer = [&] | |||
| { | |||
| if (owner.getCurrentTextEditor() != nullptr && parent == &owner) | |||
| return owner.findKeyboardFocusContainer(); | |||
| return parent; | |||
| }; | |||
| Component* getNextComponent (Component* c) override { return KeyboardFocusTraverser::getNextComponent (getComp (c)); } | |||
| Component* getPreviousComponent (Component* c) override { return KeyboardFocusTraverser::getPreviousComponent (getComp (c)); } | |||
| if (auto* container = getContainer()) | |||
| KeyboardFocusTraverser::getDefaultComponent (container); | |||
| static Component* getComp (Component* current) | |||
| 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 | |||
| { | |||
| return dynamic_cast<TextEditor*> (current) != nullptr | |||
| ? current->getParentComponent() : current; | |||
| 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<ComponentTraverser> Label::createKeyboardFocusTraverser() | |||
| { | |||
| return new LabelKeyboardFocusTraverser(); | |||
| return std::make_unique<LabelKeyboardFocusTraverser> (*this); | |||
| } | |||
| //============================================================================== | |||
| @@ -480,4 +515,9 @@ void Label::textEditorFocusLost (TextEditor& ed) | |||
| textEditorTextChanged (ed); | |||
| } | |||
| std::unique_ptr<AccessibilityHandler> Label::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<LabelAccessibilityHandler> (*this); | |||
| } | |||
| } // namespace juce | |||
| @@ -323,7 +323,7 @@ protected: | |||
| /** @internal */ | |||
| void enablementChanged() override; | |||
| /** @internal */ | |||
| KeyboardFocusTraverser* createFocusTraverser() override; | |||
| std::unique_ptr<ComponentTraverser> createKeyboardFocusTraverser() override; | |||
| /** @internal */ | |||
| void textEditorTextChanged (TextEditor&) override; | |||
| /** @internal */ | |||
| @@ -338,6 +338,8 @@ protected: | |||
| void valueChanged (Value&) override; | |||
| /** @internal */ | |||
| void callChangeListeners(); | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| private: | |||
| //============================================================================== | |||
| @@ -26,6 +26,34 @@ | |||
| namespace juce | |||
| { | |||
| template<typename RowHandlerType, typename RowComponent> | |||
| 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<AccessibilityHandler> createAccessibilityHandler() override | |||
| { | |||
| if (customComponent != nullptr && customComponent->getAccessibilityHandler() != nullptr) | |||
| return nullptr; | |||
| return std::make_unique<RowAccessibilityHandler> (*this); | |||
| } | |||
| //============================================================================== | |||
| ListBox& owner; | |||
| std::unique_ptr<Component> 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<Component>(); | |||
| 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<int>& rowsT | |||
| } | |||
| } | |||
| std::unique_ptr<AccessibilityHandler> ListBox::createAccessibilityHandler() | |||
| { | |||
| return std::make_unique<AccessibilityHandler> (*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&) {} | |||
| @@ -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<int>& rowsToDrag, | |||
| const var& dragDescription, bool allowDraggingToOtherWindows); | |||
| /** @internal */ | |||
| std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
| private: | |||
| //============================================================================== | |||