@@ -4,6 +4,32 @@ JUCE breaking changes | |||||
Develop | 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 | Change | ||||
------ | ------ | ||||
PluginDescription::uid has been deprecated and replaced with a new 'uniqueId' | 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) | inline String nsStringToJuce (NSString* s) | ||||
{ | { | ||||
return CharPointer_UTF8 ([s UTF8String]); | return CharPointer_UTF8 ([s UTF8String]); | ||||
@@ -60,6 +60,7 @@ | |||||
#include <numeric> | #include <numeric> | ||||
#include <queue> | #include <queue> | ||||
#include <sstream> | #include <sstream> | ||||
#include <typeindex> | |||||
#include <unordered_set> | #include <unordered_set> | ||||
#include <vector> | #include <vector> | ||||
@@ -64,6 +64,34 @@ namespace | |||||
{ | { | ||||
return CGPointMake ((CGFloat) p.x, (CGFloat) p.y); | 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); | 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(); | sendStateMessage(); | ||||
else | else | ||||
buttonStateChanged(); | 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 | // it is that this button represents, and the button will update its state to reflect this | ||||
// in the applicationCommandListChanged() method. | // in the applicationCommandListChanged() method. | ||||
jassert (commandManagerToUse == nullptr || ! clickTogglesState); | jassert (commandManagerToUse == nullptr || ! clickTogglesState); | ||||
invalidateAccessibilityHandler(); | |||||
} | } | ||||
bool Button::getClickingTogglesState() const noexcept | bool Button::getClickingTogglesState() const noexcept | ||||
@@ -220,6 +225,8 @@ void Button::setRadioGroupId (int newGroupId, NotificationType notification) | |||||
if (lastToggleState) | if (lastToggleState) | ||||
turnOffOtherButtonsInGroup (notification, notification); | 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 | } // namespace juce |
@@ -470,6 +470,8 @@ protected: | |||||
void focusLost (FocusChangeType) override; | void focusLost (FocusChangeType) override; | ||||
/** @internal */ | /** @internal */ | ||||
void enablementChanged() override; | void enablementChanged() override; | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
private: | private: | ||||
//============================================================================== | //============================================================================== | ||||
@@ -482,15 +482,15 @@ Component::~Component() | |||||
componentListeners.call ([this] (ComponentListener& l) { l.componentBeingDeleted (*this); }); | componentListeners.call ([this] (ComponentListener& l) { l.componentBeingDeleted (*this); }); | ||||
masterReference.clear(); | |||||
while (childComponentList.size() > 0) | while (childComponentList.size() > 0) | ||||
removeChildComponent (childComponentList.size() - 1, false, true); | removeChildComponent (childComponentList.size() - 1, false, true); | ||||
masterReference.clear(); | |||||
if (parentComponent != nullptr) | if (parentComponent != nullptr) | ||||
parentComponent->removeChildComponent (parentComponent->childComponentList.indexOf (this), true, false); | parentComponent->removeChildComponent (parentComponent->childComponentList.indexOf (this), true, false); | ||||
else if (hasKeyboardFocus (true)) | |||||
giveAwayFocus (currentlyFocusedComponent != this); | |||||
else | |||||
giveAwayKeyboardFocusInternal (isParentOf (currentlyFocusedComponent)); | |||||
if (flags.hasHeavyweightPeerFlag) | if (flags.hasHeavyweightPeerFlag) | ||||
removeFromDesktop(); | removeFromDesktop(); | ||||
@@ -551,8 +551,8 @@ void Component::setVisible (bool shouldBeVisible) | |||||
if (parentComponent != nullptr) | if (parentComponent != nullptr) | ||||
parentComponent->grabKeyboardFocus(); | 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(); | repaint(); | ||||
internalHierarchyChanged(); | internalHierarchyChanged(); | ||||
if (auto* handler = getAccessibilityHandler()) | |||||
notifyAccessibilityEventInternal (*handler, InternalAccessibilityEvent::windowOpened); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -716,6 +719,9 @@ void Component::removeFromDesktop() | |||||
if (flags.hasHeavyweightPeerFlag) | if (flags.hasHeavyweightPeerFlag) | ||||
{ | { | ||||
if (auto* handler = getAccessibilityHandler()) | |||||
notifyAccessibilityEventInternal (*handler, InternalAccessibilityEvent::windowClosed); | |||||
ComponentHelpers::releaseAllCachedImageResources (*this); | ComponentHelpers::releaseAllCachedImageResources (*this); | ||||
auto* peer = ComponentPeer::getPeerFor (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 | // 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. | // 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()) | if (auto* peer = getPeer()) | ||||
{ | { | ||||
peer->toFront (setAsForeground); | |||||
peer->toFront (shouldGrabKeyboardFocus); | |||||
if (setAsForeground && ! hasKeyboardFocus (true)) | |||||
if (shouldGrabKeyboardFocus && ! hasKeyboardFocus (true)) | |||||
grabKeyboardFocus(); | grabKeyboardFocus(); | ||||
} | } | ||||
} | } | ||||
@@ -926,7 +932,7 @@ void Component::toFront (bool setAsForeground) | |||||
} | } | ||||
} | } | ||||
if (setAsForeground) | |||||
if (shouldGrabKeyboardFocus) | |||||
{ | { | ||||
internalBroughtToFront(); | 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. | // thread, you'll need to use a MessageManagerLock object to make sure it's thread-safe. | ||||
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED_OR_OFFSCREEN | JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED_OR_OFFSCREEN | ||||
auto* child = childComponentList [index]; | |||||
if (child != nullptr) | |||||
if (auto* child = childComponentList [index]) | |||||
{ | { | ||||
sendParentEvents = sendParentEvents && child->isShowing(); | sendParentEvents = sendParentEvents && child->isShowing(); | ||||
@@ -1518,23 +1522,19 @@ Component* Component::removeChildComponent (int index, bool sendParentEvents, bo | |||||
ComponentHelpers::releaseAllCachedImageResources (*child); | ComponentHelpers::releaseAllCachedImageResources (*child); | ||||
// (NB: there are obscure situations where child->isShowing() = false, but it still has the focus) | // (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; | return child; | ||||
grabKeyboardFocus(); | grabKeyboardFocus(); | ||||
} | } | ||||
else | |||||
{ | |||||
giveAwayFocus (sendChildEvents || currentlyFocusedComponent != child); | |||||
} | |||||
} | } | ||||
if (sendChildEvents) | if (sendChildEvents) | ||||
@@ -1542,9 +1542,11 @@ Component* Component::removeChildComponent (int index, bool sendParentEvents, bo | |||||
if (sendParentEvents) | if (sendParentEvents) | ||||
internalChildrenChanged(); | internalChildrenChanged(); | ||||
return child; | |||||
} | } | ||||
return child; | |||||
return nullptr; | |||||
} | } | ||||
//============================================================================== | //============================================================================== | ||||
@@ -1656,6 +1658,10 @@ void Component::internalHierarchyChanged() | |||||
i = jmin (i, childComponentList.size()); | 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) | if (! flags.dontFocusOnMouseClickFlag) | ||||
{ | { | ||||
grabFocusInternal (focusChangedByMouseClick, true); | |||||
grabKeyboardFocusInternal (focusChangedByMouseClick, true); | |||||
if (checker.shouldBailOut()) | if (checker.shouldBailOut()) | ||||
return; | return; | ||||
@@ -2644,36 +2650,48 @@ void Component::focusGained (FocusChangeType) {} | |||||
void Component::focusLost (FocusChangeType) {} | void Component::focusLost (FocusChangeType) {} | ||||
void Component::focusOfChildComponentChanged (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); | focusGained (cause); | ||||
if (safePointer != nullptr) | 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); | const WeakReference<Component> safePointer (this); | ||||
focusLost (cause); | focusLost (cause); | ||||
if (safePointer != nullptr) | 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); | focusOfChildComponentChanged (cause); | ||||
@@ -2682,12 +2700,12 @@ void Component::internalChildFocusChange (FocusChangeType cause, const WeakRefer | |||||
} | } | ||||
if (parentComponent != nullptr) | if (parentComponent != nullptr) | ||||
parentComponent->internalChildFocusChange (cause, WeakReference<Component> (parentComponent)); | |||||
parentComponent->internalChildKeyboardFocusChange (cause, parentComponent); | |||||
} | } | ||||
void Component::setWantsKeyboardFocus (bool wantsFocus) noexcept | void Component::setWantsKeyboardFocus (bool wantsFocus) noexcept | ||||
{ | { | ||||
flags.wantsFocusFlag = wantsFocus; | |||||
flags.wantsKeyboardFocusFlag = wantsFocus; | |||||
} | } | ||||
void Component::setMouseClickGrabsKeyboardFocus (bool shouldGrabFocus) | void Component::setMouseClickGrabsKeyboardFocus (bool shouldGrabFocus) | ||||
@@ -2702,12 +2720,15 @@ bool Component::getMouseClickGrabsKeyboardFocus() const noexcept | |||||
bool Component::getWantsKeyboardFocus() 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 | bool Component::isFocusContainer() const noexcept | ||||
@@ -2715,6 +2736,35 @@ bool Component::isFocusContainer() const noexcept | |||||
return flags.isFocusContainerFlag; | 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"); | static const Identifier juce_explicitFocusOrderId ("_jexfo"); | ||||
int Component::getExplicitFocusOrder() const | int Component::getExplicitFocusOrder() const | ||||
@@ -2727,85 +2777,78 @@ void Component::setExplicitFocusOrder (int newFocusOrderIndex) | |||||
properties.set (juce_explicitFocusOrderId, newFocusOrderIndex); | properties.set (juce_explicitFocusOrderId, newFocusOrderIndex); | ||||
} | } | ||||
KeyboardFocusTraverser* Component::createFocusTraverser() | |||||
std::unique_ptr<ComponentTraverser> Component::createFocusTraverser() | |||||
{ | { | ||||
if (flags.isFocusContainerFlag || parentComponent == nullptr) | if (flags.isFocusContainerFlag || parentComponent == nullptr) | ||||
return new KeyboardFocusTraverser(); | |||||
return std::make_unique<FocusTraverser>(); | |||||
return parentComponent->createFocusTraverser(); | 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) | 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() | 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. | // thread, you'll need to use a MessageManagerLock object to make sure it's thread-safe. | ||||
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | 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! | // 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 | // 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()); | 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) | void Component::moveKeyboardFocusToSibling (bool moveToNext) | ||||
{ | { | ||||
// if component methods are being called from threads other than the message | // if component methods are being called from threads other than the message | ||||
@@ -2831,15 +2899,27 @@ void Component::moveKeyboardFocusToSibling (bool moveToNext) | |||||
if (parentComponent != nullptr) | 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()) | if (nextComp->isCurrentlyBlockedByAnotherModalComponent()) | ||||
{ | { | ||||
@@ -2850,7 +2930,7 @@ void Component::moveKeyboardFocusToSibling (bool moveToNext) | |||||
return; | return; | ||||
} | } | ||||
nextComp->grabFocusInternal (focusChangedByTabKey, true); | |||||
nextComp->grabKeyboardFocusInternal (focusChangedByTabKey, true); | |||||
return; | return; | ||||
} | } | ||||
} | } | ||||
@@ -2872,19 +2952,8 @@ Component* JUCE_CALLTYPE Component::getCurrentlyFocusedComponent() noexcept | |||||
void JUCE_CALLTYPE Component::unfocusAllComponents() | 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; | 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 | } // namespace juce |
@@ -73,7 +73,7 @@ public: | |||||
/** Returns the name of this component. | /** Returns the name of this component. | ||||
@see setName | @see setName | ||||
*/ | */ | ||||
const String& getName() const noexcept { return componentName; } | |||||
String getName() const noexcept { return componentName; } | |||||
/** Sets the name of this component. | /** Sets the name of this component. | ||||
@@ -87,7 +87,7 @@ public: | |||||
/** Returns the ID string that was set by setComponentID(). | /** Returns the ID string that was set by setComponentID(). | ||||
@see setComponentID, findChildWithID | @see setComponentID, findChildWithID | ||||
*/ | */ | ||||
const String& getComponentID() const noexcept { return componentID; } | |||||
String getComponentID() const noexcept { return componentID; } | |||||
/** Sets the component's ID string. | /** Sets the component's ID string. | ||||
You can retrieve the ID using getComponentID(). | 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 | then they will still be kept in front of this one (unless of course this | ||||
one is also 'always-on-top'). | 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 | @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. | /** Changes this component's z-order to be at the back of all its siblings. | ||||
@@ -1203,55 +1204,156 @@ public: | |||||
bool isBroughtToFrontOnMouseClick() const noexcept; | 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. | focus, but this method can be used to turn this on. | ||||
See the grabKeyboardFocus() method for details about the way a component | See the grabKeyboardFocus() method for details about the way a component | ||||
is chosen to receive the focus. | is chosen to receive the focus. | ||||
@see grabKeyboardFocus, getWantsKeyboardFocus | |||||
@see grabKeyboardFocus, giveAwayKeyboardFocus, getWantsKeyboardFocus | |||||
*/ | */ | ||||
void setWantsKeyboardFocus (bool wantsFocus) noexcept; | void setWantsKeyboardFocus (bool wantsFocus) noexcept; | ||||
/** Returns true if the component is interested in getting keyboard focus. | /** 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 | @see setWantsKeyboardFocus | ||||
*/ | */ | ||||
bool getWantsKeyboardFocus() const noexcept; | bool getWantsKeyboardFocus() const noexcept; | ||||
//============================================================================== | |||||
/** Chooses whether a click on this component automatically grabs the focus. | /** 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 | 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); | void setMouseClickGrabsKeyboardFocus (bool shouldGrabFocus); | ||||
/** Returns the last value set with setMouseClickGrabsKeyboardFocus(). | /** Returns the last value set with setMouseClickGrabsKeyboardFocus(). | ||||
See setMouseClickGrabsKeyboardFocus() for more info. | |||||
@see setMouseClickGrabsKeyboardFocus | |||||
*/ | */ | ||||
bool getMouseClickGrabsKeyboardFocus() const noexcept; | bool getMouseClickGrabsKeyboardFocus() const noexcept; | ||||
//============================================================================== | |||||
/** Tries to give keyboard focus to this component. | /** 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 | - if the component that was clicked on actually wants focus (as indicated | ||||
by calling getWantsKeyboardFocus), it gets it. | by calling getWantsKeyboardFocus), it gets it. | ||||
- if the component itself doesn't want focus, it will try to pass 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 | 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 | - 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, | parent instead, unless it's a top-level component without a parent, | ||||
in which case it just takes the focus itself. | 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 | 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. | 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 | keyPressed, keyStateChanged | ||||
*/ | */ | ||||
void grabKeyboardFocus(); | 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. | /** Returns true if this component currently has the keyboard focus. | ||||
@param trueIfChildIsFocused if this is true, then the method returns true if | @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 | have the focus. If false, the method only returns true if | ||||
this component has the focus. | this component has the focus. | ||||
@see grabKeyboardFocus, setWantsKeyboardFocus, getCurrentlyFocusedComponent, | |||||
focusGained, focusLost | |||||
@see grabKeyboardFocus, giveAwayKeyboardFocus, setWantsKeyboardFocus, | |||||
getCurrentlyFocusedComponent, focusGained, focusLost | |||||
*/ | */ | ||||
bool hasKeyboardFocus (bool trueIfChildIsFocused) const; | 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. | /** 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 | @param moveToNext if true, the focus will move forwards; if false, it will | ||||
move backwards | move backwards | ||||
@see grabKeyboardFocus, setFocusContainer, setWantsKeyboardFocus | |||||
@see grabKeyboardFocus, giveAwayKeyboardFocus, setFocusContainer, setWantsKeyboardFocus | |||||
*/ | */ | ||||
void moveKeyboardFocusToSibling (bool moveToNext); | 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. | /** Returns true if the component (and all its parents) are enabled. | ||||
@@ -2284,7 +2359,109 @@ public: | |||||
*/ | */ | ||||
bool getViewportIgnoreDragFlag() const noexcept { return flags.viewportIgnoreDragFlag; } | 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: | 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 ComponentPeer; | ||||
friend class MouseInputSource; | friend class MouseInputSource; | ||||
@@ -2294,7 +2471,7 @@ private: | |||||
static Component* currentlyFocusedComponent; | static Component* currentlyFocusedComponent; | ||||
//============================================================================== | //============================================================================== | ||||
String componentName, componentID; | |||||
String componentName, componentID, componentTitle, componentDescription, componentHelpText; | |||||
Component* parentComponent = nullptr; | Component* parentComponent = nullptr; | ||||
Rectangle<int> boundsRelativeToParent; | Rectangle<int> boundsRelativeToParent; | ||||
std::unique_ptr<Positioner> positioner; | std::unique_ptr<Positioner> positioner; | ||||
@@ -2314,29 +2491,33 @@ private: | |||||
friend class WeakReference<Component>; | friend class WeakReference<Component>; | ||||
WeakReference<Component>::Master masterReference; | WeakReference<Component>::Master masterReference; | ||||
std::unique_ptr<AccessibilityHandler> accessibilityHandler; | |||||
struct ComponentFlags | 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 | #if JUCE_DEBUG | ||||
bool isInsidePaintCall : 1; | |||||
bool isInsidePaintCall : 1; | |||||
#endif | #endif | ||||
}; | }; | ||||
@@ -2358,10 +2539,10 @@ private: | |||||
void internalMouseWheel (MouseInputSource, Point<float>, Time, const MouseWheelDetails&); | void internalMouseWheel (MouseInputSource, Point<float>, Time, const MouseWheelDetails&); | ||||
void internalMagnifyGesture (MouseInputSource, Point<float>, Time, float); | void internalMagnifyGesture (MouseInputSource, Point<float>, Time, float); | ||||
void internalBroughtToFront(); | 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 internalModalInputAttempt(); | ||||
void internalModifierKeysChanged(); | void internalModifierKeysChanged(); | ||||
void internalChildrenChanged(); | void internalChildrenChanged(); | ||||
@@ -2377,8 +2558,8 @@ private: | |||||
void repaintParent(); | void repaintParent(); | ||||
void sendFakeMouseMove() const; | void sendFakeMouseMove() const; | ||||
void takeKeyboardFocus (FocusChangeType); | 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 sendEnablementChangeMessage(); | ||||
void sendVisibilityChangeMessage(); | 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 {}; | return {}; | ||||
} | } | ||||
//============================================================================== | |||||
std::unique_ptr<AccessibilityHandler> Drawable::createAccessibilityHandler() | |||||
{ | |||||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::ignored); | |||||
} | |||||
} // namespace juce | } // namespace juce |
@@ -199,6 +199,8 @@ protected: | |||||
void setBoundsToEnclose (Rectangle<float>); | void setBoundsToEnclose (Rectangle<float>); | ||||
/** @internal */ | /** @internal */ | ||||
void applyDrawableClipPath (Graphics&); | void applyDrawableClipPath (Graphics&); | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
Point<int> originRelativeToComponent; | Point<int> originRelativeToComponent; | ||||
std::unique_ptr<Drawable> drawableClipPath; | std::unique_ptr<Drawable> drawableClipPath; | ||||
@@ -133,4 +133,10 @@ Path DrawableImage::getOutlineAsPath() const | |||||
return {}; // not applicable for images | return {}; // not applicable for images | ||||
} | } | ||||
//============================================================================== | |||||
std::unique_ptr<AccessibilityHandler> DrawableImage::createAccessibilityHandler() | |||||
{ | |||||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::image); | |||||
} | |||||
} // namespace juce | } // namespace juce |
@@ -94,6 +94,8 @@ public: | |||||
Rectangle<float> getDrawableBounds() const override; | Rectangle<float> getDrawableBounds() const override; | ||||
/** @internal */ | /** @internal */ | ||||
Path getOutlineAsPath() const override; | Path getOutlineAsPath() const override; | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
private: | private: | ||||
//============================================================================== | //============================================================================== | ||||
@@ -208,4 +208,25 @@ bool DrawableText::replaceColour (Colour originalColour, Colour replacementColou | |||||
return true; | 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 | } // namespace juce |
@@ -98,6 +98,8 @@ public: | |||||
Path getOutlineAsPath() const override; | Path getOutlineAsPath() const override; | ||||
/** @internal */ | /** @internal */ | ||||
bool replaceColour (Colour originalColour, Colour replacementColour) override; | bool replaceColour (Colour originalColour, Colour replacementColour) override; | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
private: | private: | ||||
//============================================================================== | //============================================================================== | ||||
@@ -616,4 +616,10 @@ void FileBrowserComponent::timerCallback() | |||||
} | } | ||||
} | } | ||||
//============================================================================== | |||||
std::unique_ptr<AccessibilityHandler> FileBrowserComponent::createAccessibilityHandler() | |||||
{ | |||||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||||
} | |||||
} // namespace juce | } // namespace juce |
@@ -252,6 +252,8 @@ public: | |||||
FilePreviewComponent* getPreviewComponent() const noexcept; | FilePreviewComponent* getPreviewComponent() const noexcept; | ||||
/** @internal */ | /** @internal */ | ||||
DirectoryContentsDisplayComponent* getDisplayComponent() const noexcept; | DirectoryContentsDisplayComponent* getDisplayComponent() const noexcept; | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
protected: | protected: | ||||
/** Returns a list of names and paths for the default places the user might want to look. | /** 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), | DirectoryContentsDisplayComponent (listToShow), | ||||
lastDirectory (listToShow.getDirectory()) | lastDirectory (listToShow.getDirectory()) | ||||
{ | { | ||||
setTitle ("Files"); | |||||
setModel (this); | setModel (this); | ||||
directoryContentsList.addChangeListener (this); | directoryContentsList.addChangeListener (this); | ||||
} | } | ||||
@@ -68,7 +69,7 @@ void FileListComponent::setSelectedFile (const File& f) | |||||
{ | { | ||||
for (int i = directoryContentsList.getNumFiles(); --i >= 0;) | for (int i = directoryContentsList.getNumFiles(); --i >= 0;) | ||||
{ | { | ||||
if (directoryContentsList.getFile(i) == f) | |||||
if (directoryContentsList.getFile (i) == f) | |||||
{ | { | ||||
fileWaitingToBeSelected = File(); | fileWaitingToBeSelected = File(); | ||||
@@ -189,6 +190,11 @@ public: | |||||
repaint(); | repaint(); | ||||
} | } | ||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override | |||||
{ | |||||
return nullptr; | |||||
} | |||||
private: | private: | ||||
//============================================================================== | //============================================================================== | ||||
FileListComponent& owner; | FileListComponent& owner; | ||||
@@ -231,6 +237,11 @@ int FileListComponent::getNumRows() | |||||
return directoryContentsList.getNumFiles(); | return directoryContentsList.getNumFiles(); | ||||
} | } | ||||
String FileListComponent::getNameForRow (int rowNumber) | |||||
{ | |||||
return directoryContentsList.getFile (rowNumber).getFileName(); | |||||
} | |||||
void FileListComponent::paintListBoxItem (int, Graphics&, int, int, bool) | void FileListComponent::paintListBoxItem (int, Graphics&, int, int, bool) | ||||
{ | { | ||||
} | } | ||||
@@ -82,6 +82,7 @@ private: | |||||
void changeListenerCallback (ChangeBroadcaster*) override; | void changeListenerCallback (ChangeBroadcaster*) override; | ||||
int getNumRows() override; | int getNumRows() override; | ||||
String getNameForRow (int rowNumber) override; | |||||
void paintListBoxItem (int, Graphics&, int, int, bool) override; | void paintListBoxItem (int, Graphics&, int, int, bool) override; | ||||
Component* refreshComponentForRow (int rowNumber, bool isRowSelected, Component*) override; | Component* refreshComponentForRow (int rowNumber, bool isRowSelected, Component*) override; | ||||
void selectedRowsChanged (int row) override; | void selectedRowsChanged (int row) override; | ||||
@@ -190,6 +190,11 @@ public: | |||||
indexInContentsList, owner); | indexInContentsList, owner); | ||||
} | } | ||||
String getAccessibilityName() override | |||||
{ | |||||
return file.getFileName(); | |||||
} | |||||
void itemClicked (const MouseEvent& e) override | void itemClicked (const MouseEvent& e) override | ||||
{ | { | ||||
owner.sendMouseClickMessage (file, e); | owner.sendMouseClickMessage (file, e); | ||||
@@ -70,11 +70,11 @@ void FilenameComponent::resized() | |||||
getLookAndFeel().layoutFilenameComponent (*this, &filenameBox, browseButton.get()); | 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 | // This prevents the sub-components from grabbing focus if the | ||||
// FilenameComponent has been set to refuse focus. | // FilenameComponent has been set to refuse focus. | ||||
return getWantsKeyboardFocus() ? Component::createFocusTraverser() : nullptr; | |||||
return getWantsKeyboardFocus() ? Component::createKeyboardFocusTraverser() : nullptr; | |||||
} | } | ||||
void FilenameComponent::setBrowseButtonText (const String& newBrowseButtonText) | void FilenameComponent::setBrowseButtonText (const String& newBrowseButtonText) | ||||
@@ -212,7 +212,7 @@ public: | |||||
/** @internal */ | /** @internal */ | ||||
void fileDragExit (const StringArray&) override; | void fileDragExit (const StringArray&) override; | ||||
/** @internal */ | /** @internal */ | ||||
KeyboardFocusTraverser* createFocusTraverser() override; | |||||
std::unique_ptr<ComponentTraverser> createKeyboardFocusTraverser() override; | |||||
private: | 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 | } // namespace juce |
@@ -53,6 +53,8 @@ public: | |||||
void paint (Graphics&) override; | void paint (Graphics&) override; | ||||
/** @internal */ | /** @internal */ | ||||
void timerCallback() override; | void timerCallback() override; | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
private: | private: | ||||
File fileToLoad; | File fileToLoad; | ||||
@@ -66,6 +66,8 @@ | |||||
#include <windowsx.h> | #include <windowsx.h> | ||||
#include <vfw.h> | #include <vfw.h> | ||||
#include <commdlg.h> | #include <commdlg.h> | ||||
#include <UIAutomation.h> | |||||
#include <sapi.h> | |||||
#if JUCE_WEB_BROWSER | #if JUCE_WEB_BROWSER | ||||
#include <exdisp.h> | #include <exdisp.h> | ||||
@@ -103,8 +105,10 @@ namespace juce | |||||
extern bool juce_areThereAnyAlwaysOnTopWindows(); | extern bool juce_areThereAnyAlwaysOnTopWindows(); | ||||
} | } | ||||
#include "accessibility/juce_AccessibilityHandler.cpp" | |||||
#include "components/juce_Component.cpp" | #include "components/juce_Component.cpp" | ||||
#include "components/juce_ComponentListener.cpp" | #include "components/juce_ComponentListener.cpp" | ||||
#include "components/juce_FocusTraverser.cpp" | |||||
#include "mouse/juce_MouseInputSource.cpp" | #include "mouse/juce_MouseInputSource.cpp" | ||||
#include "desktop/juce_Displays.cpp" | #include "desktop/juce_Displays.cpp" | ||||
#include "desktop/juce_Desktop.cpp" | #include "desktop/juce_Desktop.cpp" | ||||
@@ -240,6 +244,7 @@ namespace juce | |||||
#endif | #endif | ||||
#else | #else | ||||
#include "native/accessibility/juce_mac_Accessibility.mm" | |||||
#include "native/juce_mac_NSViewComponentPeer.mm" | #include "native/juce_mac_NSViewComponentPeer.mm" | ||||
#include "native/juce_mac_Windowing.mm" | #include "native/juce_mac_Windowing.mm" | ||||
#include "native/juce_mac_MainMenu.mm" | #include "native/juce_mac_MainMenu.mm" | ||||
@@ -249,6 +254,12 @@ namespace juce | |||||
#include "native/juce_mac_MouseCursor.mm" | #include "native/juce_mac_MouseCursor.mm" | ||||
#elif JUCE_WINDOWS | #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_Windowing.cpp" | ||||
#include "native/juce_win32_DragAndDrop.cpp" | #include "native/juce_win32_DragAndDrop.cpp" | ||||
#include "native/juce_win32_FileChooser.cpp" | #include "native/juce_win32_FileChooser.cpp" | ||||
@@ -155,6 +155,8 @@ namespace juce | |||||
class ApplicationCommandManagerListener; | class ApplicationCommandManagerListener; | ||||
class DrawableButton; | class DrawableButton; | ||||
class Displays; | class Displays; | ||||
class AccessibilityHandler; | |||||
class KeyboardFocusTraverser; | |||||
class FlexBox; | class FlexBox; | ||||
class Grid; | class Grid; | ||||
@@ -167,7 +169,8 @@ namespace juce | |||||
#include "mouse/juce_MouseEvent.h" | #include "mouse/juce_MouseEvent.h" | ||||
#include "keyboard/juce_KeyPress.h" | #include "keyboard/juce_KeyPress.h" | ||||
#include "keyboard/juce_KeyListener.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_ModalComponentManager.h" | ||||
#include "components/juce_ComponentListener.h" | #include "components/juce_ComponentListener.h" | ||||
#include "components/juce_CachedComponentImage.h" | #include "components/juce_CachedComponentImage.h" | ||||
@@ -185,6 +188,7 @@ namespace juce | |||||
#include "mouse/juce_TextDragAndDropTarget.h" | #include "mouse/juce_TextDragAndDropTarget.h" | ||||
#include "mouse/juce_TooltipClient.h" | #include "mouse/juce_TooltipClient.h" | ||||
#include "keyboard/juce_CaretComponent.h" | #include "keyboard/juce_CaretComponent.h" | ||||
#include "keyboard/juce_KeyboardFocusTraverser.h" | |||||
#include "keyboard/juce_SystemClipboard.h" | #include "keyboard/juce_SystemClipboard.h" | ||||
#include "keyboard/juce_TextEditorKeyMapper.h" | #include "keyboard/juce_TextEditorKeyMapper.h" | ||||
#include "keyboard/juce_TextInputTarget.h" | #include "keyboard/juce_TextInputTarget.h" | ||||
@@ -293,6 +297,22 @@ namespace juce | |||||
#include "lookandfeel/juce_LookAndFeel_V3.h" | #include "lookandfeel/juce_LookAndFeel_V3.h" | ||||
#include "lookandfeel/juce_LookAndFeel_V4.h" | #include "lookandfeel/juce_LookAndFeel_V4.h" | ||||
#include "mouse/juce_LassoComponent.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_LINUX || JUCE_BSD | ||||
#if JUCE_GUI_BASICS_INCLUDE_XHEADERS | #if JUCE_GUI_BASICS_INCLUDE_XHEADERS | ||||
@@ -26,105 +26,248 @@ | |||||
namespace juce | 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 | } // 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} | @tags{GUI} | ||||
*/ | */ | ||||
class JUCE_API KeyboardFocusTraverser | |||||
class JUCE_API KeyboardFocusTraverser : public ComponentTraverser | |||||
{ | { | ||||
public: | public: | ||||
KeyboardFocusTraverser(); | |||||
/** Destructor. */ | /** 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 | } // namespace juce |
@@ -150,6 +150,7 @@ public: | |||||
{ | { | ||||
ProxyComponent (Component& c) | ProxyComponent (Component& c) | ||||
{ | { | ||||
setAccessible (false); | |||||
setWantsKeyboardFocus (false); | setWantsKeyboardFocus (false); | ||||
setBounds (c.getBounds()); | setBounds (c.getBounds()); | ||||
setTransform (c.getTransform()); | setTransform (c.getTransform()); | ||||
@@ -459,4 +459,10 @@ void ConcertinaPanel::panelHeaderDoubleClicked (Component* component) | |||||
setPanelSize (component, 0, true); | setPanelSize (component, 0, true); | ||||
} | } | ||||
//============================================================================== | |||||
std::unique_ptr<AccessibilityHandler> ConcertinaPanel::createAccessibilityHandler() | |||||
{ | |||||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||||
} | |||||
} // namespace juce | } // namespace juce |
@@ -119,6 +119,10 @@ public: | |||||
ConcertinaPanel&, Component&) = 0; | ConcertinaPanel&, Component&) = 0; | ||||
}; | }; | ||||
//============================================================================== | |||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
private: | private: | ||||
void resized() override; | void resized() override; | ||||
@@ -69,4 +69,10 @@ void GroupComponent::paint (Graphics& g) | |||||
void GroupComponent::enablementChanged() { repaint(); } | void GroupComponent::enablementChanged() { repaint(); } | ||||
void GroupComponent::colourChanged() { repaint(); } | void GroupComponent::colourChanged() { repaint(); } | ||||
//============================================================================== | |||||
std::unique_ptr<AccessibilityHandler> GroupComponent::createAccessibilityHandler() | |||||
{ | |||||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||||
} | |||||
} // namespace juce | } // namespace juce |
@@ -98,6 +98,8 @@ public: | |||||
void enablementChanged() override; | void enablementChanged() override; | ||||
/** @internal */ | /** @internal */ | ||||
void colourChanged() override; | void colourChanged() override; | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
private: | private: | ||||
String text; | String text; | ||||
@@ -61,7 +61,7 @@ private: | |||||
ScrollBar::ScrollBar (bool shouldBeVertical) : vertical (shouldBeVertical) | ScrollBar::ScrollBar (bool shouldBeVertical) : vertical (shouldBeVertical) | ||||
{ | { | ||||
setRepaintsOnMouseActivity (true); | setRepaintsOnMouseActivity (true); | ||||
setFocusContainer (true); | |||||
setFocusContainerType (FocusContainerType::keyboardFocusContainer); | |||||
} | } | ||||
ScrollBar::~ScrollBar() | ScrollBar::~ScrollBar() | ||||
@@ -440,4 +440,46 @@ bool ScrollBar::getVisibility() const noexcept | |||||
&& visibleRange.getLength() > 0.0); | && 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 | } // namespace juce |
@@ -211,6 +211,11 @@ public: | |||||
*/ | */ | ||||
void setSingleStepSize (double newSingleStepSize) noexcept; | 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. | /** Moves the scrollbar by a number of single-steps. | ||||
This will move the bar by a multiple of its single-step interval (as | This will move the bar by a multiple of its single-step interval (as | ||||
@@ -409,6 +414,8 @@ public: | |||||
void parentHierarchyChanged() override; | void parentHierarchyChanged() override; | ||||
/** @internal */ | /** @internal */ | ||||
void setVisible (bool) override; | void setVisible (bool) override; | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
private: | private: | ||||
//============================================================================== | //============================================================================== | ||||
@@ -294,4 +294,10 @@ bool SidePanel::isMouseEventInThisOrChildren (Component* eventComponent) | |||||
return false; | return false; | ||||
} | } | ||||
//============================================================================== | |||||
std::unique_ptr<AccessibilityHandler> SidePanel::createAccessibilityHandler() | |||||
{ | |||||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||||
} | |||||
} // namespace juce | } // namespace juce |
@@ -195,6 +195,8 @@ public: | |||||
void mouseDrag (const MouseEvent&) override; | void mouseDrag (const MouseEvent&) override; | ||||
/** @internal */ | /** @internal */ | ||||
void mouseUp (const MouseEvent&) override; | void mouseUp (const MouseEvent&) override; | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
private: | private: | ||||
//============================================================================== | //============================================================================== | ||||
@@ -202,7 +202,7 @@ TabbedButtonBar::TabbedButtonBar (Orientation orientationToUse) | |||||
setInterceptsMouseClicks (false, true); | setInterceptsMouseClicks (false, true); | ||||
behindFrontTab.reset (new BehindFrontTabComp (*this)); | behindFrontTab.reset (new BehindFrontTabComp (*this)); | ||||
addAndMakeVisible (behindFrontTab.get()); | addAndMakeVisible (behindFrontTab.get()); | ||||
setFocusContainer (true); | |||||
setFocusContainerType (FocusContainerType::keyboardFocusContainer); | |||||
} | } | ||||
TabbedButtonBar::~TabbedButtonBar() | TabbedButtonBar::~TabbedButtonBar() | ||||
@@ -574,4 +574,10 @@ void TabbedButtonBar::showExtraItemsMenu() | |||||
void TabbedButtonBar::currentTabChanged (int, const String&) {} | void TabbedButtonBar::currentTabChanged (int, const String&) {} | ||||
void TabbedButtonBar::popupMenuClickOnTab (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 | } // namespace juce |
@@ -334,6 +334,8 @@ public: | |||||
void resized() override; | void resized() override; | ||||
/** @internal */ | /** @internal */ | ||||
void lookAndFeelChanged() override; | void lookAndFeelChanged() override; | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
protected: | protected: | ||||
//============================================================================== | //============================================================================== | ||||
@@ -310,4 +310,10 @@ void TabbedComponent::changeCallback (int newCurrentTabIndex, const String& newT | |||||
void TabbedComponent::currentTabChanged (int, const String&) {} | void TabbedComponent::currentTabChanged (int, const String&) {} | ||||
void TabbedComponent::popupMenuClickOnTab (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 | } // namespace juce |
@@ -197,6 +197,8 @@ public: | |||||
void resized() override; | void resized() override; | ||||
/** @internal */ | /** @internal */ | ||||
void lookAndFeelChanged() override; | void lookAndFeelChanged() override; | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
protected: | protected: | ||||
//============================================================================== | //============================================================================== | ||||
@@ -293,4 +293,10 @@ void BurgerMenuComponent::lookAndFeelChanged() | |||||
listBox.setRowHeight (roundToInt (getLookAndFeel().getPopupMenuFont().getHeight() * 2.0f)); | listBox.setRowHeight (roundToInt (getLookAndFeel().getPopupMenuFont().getHeight() * 2.0f)); | ||||
} | } | ||||
//============================================================================== | |||||
std::unique_ptr<AccessibilityHandler> BurgerMenuComponent::createAccessibilityHandler() | |||||
{ | |||||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::menuBar); | |||||
} | |||||
} // namespace juce | } // namespace juce |
@@ -71,6 +71,8 @@ public: | |||||
/** @internal */ | /** @internal */ | ||||
void lookAndFeelChanged() override; | void lookAndFeelChanged() override; | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
private: | private: | ||||
//============================================================================== | //============================================================================== | ||||
@@ -26,11 +26,63 @@ | |||||
namespace juce | 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) | MenuBarComponent::MenuBarComponent (MenuBarModel* m) | ||||
: model (nullptr), | |||||
itemUnderMouse (-1), | |||||
currentPopupIndex (-1), | |||||
topLevelIndexClicked (0) | |||||
{ | { | ||||
setRepaintsOnMouseActivity (true); | setRepaintsOnMouseActivity (true); | ||||
setWantsKeyboardFocus (false); | setWantsKeyboardFocus (false); | ||||
@@ -70,77 +122,83 @@ void MenuBarComponent::setModel (MenuBarModel* const newModel) | |||||
//============================================================================== | //============================================================================== | ||||
void MenuBarComponent::paint (Graphics& g) | 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() | void MenuBarComponent::resized() | ||||
{ | { | ||||
xPositions.clear(); | |||||
int x = 0; | 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) | 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; | return -1; | ||||
} | } | ||||
void MenuBarComponent::repaintMenuItem (int index) | 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) | void MenuBarComponent::setOpenItem (int index) | ||||
@@ -156,7 +214,7 @@ void MenuBarComponent::setOpenItem (int index) | |||||
currentPopupIndex = index; | currentPopupIndex = index; | ||||
repaintMenuItem (currentPopupIndex); | repaintMenuItem (currentPopupIndex); | ||||
Desktop& desktop = Desktop::getInstance(); | |||||
auto& desktop = Desktop::getInstance(); | |||||
if (index >= 0) | if (index >= 0) | ||||
desktop.addGlobalMouseListener (this); | desktop.addGlobalMouseListener (this); | ||||
@@ -180,30 +238,24 @@ void MenuBarComponent::showMenu (int index) | |||||
setOpenItem (index); | setOpenItem (index); | ||||
setItemUnderMouse (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) | if (m.lookAndFeel == nullptr) | ||||
m.setLookAndFeel (&getLookAndFeel()); | 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) | 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) | void MenuBarComponent::menuDismissed (int topLevelIndex, int itemId) | ||||
{ | { | ||||
topLevelIndexClicked = topLevelIndex; | topLevelIndexClicked = topLevelIndex; | ||||
@@ -212,8 +264,7 @@ void MenuBarComponent::menuDismissed (int topLevelIndex, int itemId) | |||||
void MenuBarComponent::handleCommandMessage (int commandId) | void MenuBarComponent::handleCommandMessage (int commandId) | ||||
{ | { | ||||
const Point<int> mousePos (getMouseXYRelative()); | |||||
updateItemUnderMouse (mousePos); | |||||
updateItemUnderMouse (getMouseXYRelative()); | |||||
if (currentPopupIndex == topLevelIndexClicked) | if (currentPopupIndex == topLevelIndexClicked) | ||||
setOpenItem (-1); | setOpenItem (-1); | ||||
@@ -239,8 +290,7 @@ void MenuBarComponent::mouseDown (const MouseEvent& e) | |||||
{ | { | ||||
if (currentPopupIndex < 0) | if (currentPopupIndex < 0) | ||||
{ | { | ||||
const MouseEvent e2 (e.getEventRelativeTo (this)); | |||||
updateItemUnderMouse (e2.getPosition()); | |||||
updateItemUnderMouse (e.getEventRelativeTo (this).getPosition()); | |||||
currentPopupIndex = -2; | currentPopupIndex = -2; | ||||
showMenu (itemUnderMouse); | showMenu (itemUnderMouse); | ||||
@@ -249,8 +299,7 @@ void MenuBarComponent::mouseDown (const MouseEvent& e) | |||||
void MenuBarComponent::mouseDrag (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) | if (item >= 0) | ||||
showMenu (item); | showMenu (item); | ||||
@@ -258,7 +307,7 @@ void MenuBarComponent::mouseDrag (const MouseEvent& e) | |||||
void MenuBarComponent::mouseUp (const MouseEvent& e) | void MenuBarComponent::mouseUp (const MouseEvent& e) | ||||
{ | { | ||||
const MouseEvent e2 (e.getEventRelativeTo (this)); | |||||
const auto e2 = e.getEventRelativeTo (this); | |||||
updateItemUnderMouse (e2.getPosition()); | updateItemUnderMouse (e2.getPosition()); | ||||
@@ -271,13 +320,13 @@ void MenuBarComponent::mouseUp (const MouseEvent& e) | |||||
void MenuBarComponent::mouseMove (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 (lastMousePos != e2.getPosition()) | ||||
{ | { | ||||
if (currentPopupIndex >= 0) | if (currentPopupIndex >= 0) | ||||
{ | { | ||||
const int item = getItemAt (e2.getPosition()); | |||||
const auto item = getItemAt (e2.getPosition()); | |||||
if (item >= 0) | if (item >= 0) | ||||
showMenu (item); | showMenu (item); | ||||
@@ -293,11 +342,11 @@ void MenuBarComponent::mouseMove (const MouseEvent& e) | |||||
bool MenuBarComponent::keyPressed (const KeyPress& key) | bool MenuBarComponent::keyPressed (const KeyPress& key) | ||||
{ | { | ||||
const int numMenus = menuNames.size(); | |||||
const auto numMenus = (int) itemComponents.size(); | |||||
if (numMenus > 0) | if (numMenus > 0) | ||||
{ | { | ||||
const int currentIndex = jlimit (0, numMenus - 1, currentPopupIndex); | |||||
const auto currentIndex = jlimit (0, numMenus - 1, currentPopupIndex); | |||||
if (key.isKeyCode (KeyPress::leftKey)) | if (key.isKeyCode (KeyPress::leftKey)) | ||||
{ | { | ||||
@@ -315,34 +364,69 @@ bool MenuBarComponent::keyPressed (const KeyPress& key) | |||||
return false; | return false; | ||||
} | } | ||||
void MenuBarComponent::menuBarItemsChanged (MenuBarModel* /*menuBarModel*/) | |||||
void MenuBarComponent::menuBarItemsChanged (MenuBarModel*) | |||||
{ | { | ||||
StringArray newNames; | StringArray newNames; | ||||
if (model != nullptr) | if (model != nullptr) | ||||
newNames = model->getMenuBarNames(); | 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(); | repaint(); | ||||
resized(); | 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) | if (model == nullptr || (info.commandFlags & ApplicationCommandInfo::dontTriggerVisualFeedback) != 0) | ||||
return; | 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)) | if (menu.containsCommandItem (info.commandID)) | ||||
{ | { | ||||
setItemUnderMouse (i); | |||||
setItemUnderMouse ((int) i); | |||||
startTimer (200); | startTimer (200); | ||||
break; | break; | ||||
} | } | ||||
@@ -355,4 +439,20 @@ void MenuBarComponent::timerCallback() | |||||
updateItemUnderMouse (getMouseXYRelative()); | 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 | } // namespace juce |
@@ -95,24 +95,32 @@ public: | |||||
void menuBarItemsChanged (MenuBarModel*) override; | void menuBarItemsChanged (MenuBarModel*) override; | ||||
/** @internal */ | /** @internal */ | ||||
void menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo&) override; | void menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo&) override; | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
private: | private: | ||||
//============================================================================== | //============================================================================== | ||||
MenuBarModel* model; | |||||
class AccessibleItemComponent; | |||||
StringArray menuNames; | |||||
Array<int> xPositions; | |||||
Point<int> lastMousePos; | |||||
int itemUnderMouse, currentPopupIndex, topLevelIndexClicked; | |||||
//============================================================================== | |||||
void timerCallback() override; | |||||
int getItemAt (Point<int>); | int getItemAt (Point<int>); | ||||
void setItemUnderMouse (int index); | |||||
void setOpenItem (int index); | |||||
void setItemUnderMouse (int); | |||||
void setOpenItem (int); | |||||
void updateItemUnderMouse (Point<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) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuBarComponent) | ||||
}; | }; | ||||
@@ -84,7 +84,7 @@ struct ItemComponent : public Component | |||||
ItemComponent (const PopupMenu::Item& i, | ItemComponent (const PopupMenu::Item& i, | ||||
const PopupMenu::Options& o, | const PopupMenu::Options& o, | ||||
MenuWindow& parent) | MenuWindow& parent) | ||||
: item (i), options (o), customComp (i.customComponent) | |||||
: item (i), parentWindow (parent), options (o), customComp (i.customComponent) | |||||
{ | { | ||||
if (item.isSectionHeader) | if (item.isSectionHeader) | ||||
customComp = *new HeaderItemComponent (item.text, options); | customComp = *new HeaderItemComponent (item.text, options); | ||||
@@ -156,13 +156,99 @@ struct ItemComponent : public Component | |||||
if (customComp != nullptr) | if (customComp != nullptr) | ||||
customComp->setHighlighted (shouldBeHighlighted); | customComp->setHighlighted (shouldBeHighlighted); | ||||
if (isHighlighted) | |||||
if (auto* handler = getAccessibilityHandler()) | |||||
handler->grabFocus(); | |||||
repaint(); | repaint(); | ||||
} | } | ||||
} | } | ||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override | |||||
{ | |||||
return item.isSeparator ? nullptr : std::make_unique<ItemAccessibilityHandler> (*this); | |||||
} | |||||
PopupMenu::Item item; | PopupMenu::Item item; | ||||
private: | 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; | 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 | // NB: we use a copy of the one from the item info in case we're using our own section comp | ||||
ReferenceCountedObjectPtr<CustomComponent> customComp; | ReferenceCountedObjectPtr<CustomComponent> customComp; | ||||
@@ -223,6 +309,7 @@ struct MenuWindow : public Component | |||||
setWantsKeyboardFocus (false); | setWantsKeyboardFocus (false); | ||||
setMouseClickGrabsKeyboardFocus (false); | setMouseClickGrabsKeyboardFocus (false); | ||||
setAlwaysOnTop (true); | setAlwaysOnTop (true); | ||||
setFocusContainerType (FocusContainerType::focusContainer); | |||||
setLookAndFeel (parent != nullptr ? &(parent->getLookAndFeel()) | setLookAndFeel (parent != nullptr ? &(parent->getLookAndFeel()) | ||||
: menu.lookAndFeel.get()); | : menu.lookAndFeel.get()); | ||||
@@ -275,11 +362,19 @@ struct MenuWindow : public Component | |||||
if (auto visibleID = options.getItemThatMustBeVisible()) | 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(); | resizeToBestWindowPos(); | ||||
@@ -887,47 +982,36 @@ struct MenuWindow : public Component | |||||
return correctColumnWidths (maxMenuW); | 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) | void setCurrentlyHighlightedChild (ItemComponent* child) | ||||
{ | { | ||||
if (currentChild == child) | |||||
return; | |||||
if (currentChild != nullptr) | if (currentChild != nullptr) | ||||
currentChild->setHighlighted (false); | currentChild->setHighlighted (false); | ||||
@@ -1026,6 +1113,9 @@ struct MenuWindow : public Component | |||||
currentChild->setHighlighted (true); | currentChild->setHighlighted (true); | ||||
timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter(); | timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter(); | ||||
} | } | ||||
if (auto* handler = getAccessibilityHandler()) | |||||
handler->notifyAccessibilityEvent (AccessibilityEvent::rowSelectionChanged); | |||||
} | } | ||||
bool isSubMenuVisible() const noexcept { return activeSubMenu != nullptr && activeSubMenu->isVisible(); } | 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 isTopScrollZoneActive() const noexcept { return canScroll() && childYOffset > 0; } | ||||
bool isBottomScrollZoneActive() const noexcept { return canScroll() && childYOffset < contentHeight - windowPos.getHeight(); } | 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; | MenuWindow* parent; | ||||
const Options options; | 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 | window->toFront (false); // need to do this after making it modal, or it could | ||||
// be stuck behind other comps that are already modal.. | // be stuck behind other comps that are already modal.. | ||||
if (auto* handler = window->getAccessibilityHandler()) | |||||
handler->grabFocus(); | |||||
#if JUCE_MODAL_LOOPS_PERMITTED | #if JUCE_MODAL_LOOPS_PERMITTED | ||||
if (userCallback == nullptr && canBeModal) | if (userCallback == nullptr && canBeModal) | ||||
return window->runModalLoop(); | return window->runModalLoop(); | ||||
@@ -156,6 +156,10 @@ public: | |||||
const Rectangle<float>& body) = 0; | const Rectangle<float>& body) = 0; | ||||
}; | }; | ||||
//============================================================================== | |||||
/** @internal */ | |||||
void paint (Graphics&) override; | |||||
protected: | protected: | ||||
//============================================================================== | //============================================================================== | ||||
/** Subclasses should override this to return the size of the content they | /** 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; | virtual void paintContent (Graphics& g, int width, int height) = 0; | ||||
public: | |||||
/** @internal */ | |||||
void paint (Graphics&) override; | |||||
private: | private: | ||||
Rectangle<int> content; | Rectangle<int> content; | ||||
Point<int> arrowTip; | Point<int> arrowTip; | ||||
@@ -33,6 +33,7 @@ public: | |||||
: target (comp), shadow (ds) | : target (comp), shadow (ds) | ||||
{ | { | ||||
setVisible (true); | setVisible (true); | ||||
setAccessible (false); | |||||
setInterceptsMouseClicks (false, false); | setInterceptsMouseClicks (false, false); | ||||
if (comp->isOnDesktop()) | if (comp->isOnDesktop()) | ||||
@@ -188,6 +188,12 @@ void JUCESplashScreen::mouseUp (const MouseEvent&) | |||||
juceWebsite.launchInDefaultBrowser(); | juceWebsite.launchInDefaultBrowser(); | ||||
} | } | ||||
//============================================================================== | |||||
std::unique_ptr<AccessibilityHandler> JUCESplashScreen::createAccessibilityHandler() | |||||
{ | |||||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::splashScreen); | |||||
} | |||||
// END SECTION A | // END SECTION A | ||||
} // namespace juce | } // namespace juce |
@@ -55,6 +55,10 @@ public: | |||||
static std::unique_ptr<Drawable> getSplashScreenLogo(); | static std::unique_ptr<Drawable> getSplashScreenLogo(); | ||||
//============================================================================== | |||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
private: | private: | ||||
void paint (Graphics&) override; | void paint (Graphics&) override; | ||||
void timerCallback() 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 | 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, | class NSViewComponentPeer : public ComponentPeer, | ||||
private Timer | private Timer | ||||
@@ -124,7 +104,7 @@ public: | |||||
{ | { | ||||
r.origin.x = (CGFloat) component.getX(); | r.origin.x = (CGFloat) component.getX(); | ||||
r.origin.y = (CGFloat) component.getY(); | r.origin.y = (CGFloat) component.getY(); | ||||
flipScreenRect (r); | |||||
r = flippedScreenRect (r); | |||||
window = [createWindowInstance() initWithContentRect: r | window = [createWindowInstance() initWithContentRect: r | ||||
styleMask: getNSWindowStyleMask (windowStyleFlags) | styleMask: getNSWindowStyleMask (windowStyleFlags) | ||||
@@ -323,7 +303,7 @@ public: | |||||
r = [[view superview] convertRect: r toView: nil]; | r = [[view superview] convertRect: r toView: nil]; | ||||
r = [viewWindow convertRectToScreen: r]; | r = [viewWindow convertRectToScreen: r]; | ||||
flipScreenRect (r); | |||||
r = flippedScreenRect (r); | |||||
} | } | ||||
return convertToRectInt (r); | return convertToRectInt (r); | ||||
@@ -1669,68 +1649,100 @@ const SEL NSViewComponentPeer::becomeKeySelector = @selector (becomeKey:); | |||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE | 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@:"); | addMethod (@selector (isFlipped), isFlipped, "c@:"); | ||||
@@ -1746,11 +1758,6 @@ struct JuceNSViewClass : public ObjCClass<NSView> | |||||
} | } | ||||
private: | private: | ||||
static NSViewComponentPeer* getOwner (id self) | |||||
{ | |||||
return getIvar<NSViewComponentPeer*> (self, "owner"); | |||||
} | |||||
static void mouseDown (id self, SEL s, NSEvent* ev) | static void mouseDown (id self, SEL s, NSEvent* ev) | ||||
{ | { | ||||
if (JUCEApplicationBase::isStandaloneApp()) | if (JUCEApplicationBase::isStandaloneApp()) | ||||
@@ -2072,15 +2079,47 @@ private: | |||||
} | } | ||||
static void concludeDragOperation (id, SEL, id<NSDraggingInfo>) {} | 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 (canBecomeKeyWindow), canBecomeKeyWindow, "c@:"); | ||||
addMethod (@selector (canBecomeMainWindow), canBecomeMainWindow, "c@:"); | addMethod (@selector (canBecomeMainWindow), canBecomeMainWindow, "c@:"); | ||||
addMethod (@selector (becomeKeyWindow), becomeKeyWindow, "v@:"); | addMethod (@selector (becomeKeyWindow), becomeKeyWindow, "v@:"); | ||||
@@ -2096,6 +2135,12 @@ struct JuceNSWindowClass : public ObjCClass<NSWindow> | |||||
addMethod (@selector (window:shouldPopUpDocumentPathMenu:), shouldPopUpPathMenu, "B@:@", @encode (NSMenu*)); | addMethod (@selector (window:shouldPopUpDocumentPathMenu:), shouldPopUpPathMenu, "B@:@", @encode (NSMenu*)); | ||||
addMethod (@selector (isFlipped), isFlipped, "c@:"); | 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:), | addMethod (@selector (window:shouldDragDocumentWithEvent:from:withPasteboard:), | ||||
shouldAllowIconDrag, "B@:@", @encode (NSEvent*), @encode (NSPoint), @encode (NSPasteboard*)); | shouldAllowIconDrag, "B@:@", @encode (NSEvent*), @encode (NSPoint), @encode (NSPasteboard*)); | ||||
@@ -2105,11 +2150,6 @@ struct JuceNSWindowClass : public ObjCClass<NSWindow> | |||||
} | } | ||||
private: | private: | ||||
static NSViewComponentPeer* getOwner (id self) | |||||
{ | |||||
return getIvar<NSViewComponentPeer*> (self, "owner"); | |||||
} | |||||
//============================================================================== | //============================================================================== | ||||
static BOOL isFlipped (id, SEL) { return true; } | static BOOL isFlipped (id, SEL) { return true; } | ||||
@@ -2249,6 +2289,26 @@ private: | |||||
return false; | 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() | NSView* NSViewComponentPeer::createViewInstance() | ||||
@@ -63,6 +63,14 @@ static bool shouldDeactivateTitleBar = true; | |||||
void* getUser32Function (const char*); | void* getUser32Function (const char*); | ||||
namespace WindowsAccessibility | |||||
{ | |||||
void initialiseUIAWrapper(); | |||||
long getUiaRootObjectId(); | |||||
bool handleWmGetObject (AccessibilityHandler*, WPARAM, LPARAM, LRESULT*); | |||||
void revokeUIAMapEntriesForWindow (HWND); | |||||
} | |||||
#if JUCE_DEBUG | #if JUCE_DEBUG | ||||
int numActiveScopedDpiAwarenessDisablers = 0; | int numActiveScopedDpiAwarenessDisablers = 0; | ||||
bool isInScopedDPIAwarenessDisabler() { return numActiveScopedDpiAwarenessDisablers > 0; } | bool isInScopedDPIAwarenessDisabler() { return numActiveScopedDpiAwarenessDisablers > 0; } | ||||
@@ -1372,12 +1380,14 @@ public: | |||||
parentToAddTo (parent), | parentToAddTo (parent), | ||||
currentRenderingEngine (softwareRenderingEngine) | currentRenderingEngine (softwareRenderingEngine) | ||||
{ | { | ||||
// make sure that the UIA wrapper singleton is loaded | |||||
WindowsAccessibility::initialiseUIAWrapper(); | |||||
callFunctionIfNotLocked (&createWindowCallback, this); | callFunctionIfNotLocked (&createWindowCallback, this); | ||||
setTitle (component.getName()); | setTitle (component.getName()); | ||||
updateShadower(); | updateShadower(); | ||||
// make sure that the on-screen keyboard code is loaded | |||||
OnScreenKeyboard::getInstance(); | OnScreenKeyboard::getInstance(); | ||||
getNativeRealtimeModifiers = [] | getNativeRealtimeModifiers = [] | ||||
@@ -1397,13 +1407,15 @@ public: | |||||
~HWNDComponentPeer() | ~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; | shadower = nullptr; | ||||
currentTouches.deleteAllTouchesForPeer (this); | 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); | callFunctionIfNotLocked (&destroyWindowCallback, (void*) hwnd); | ||||
if (currentWindowIcon != nullptr) | if (currentWindowIcon != nullptr) | ||||
@@ -1989,6 +2001,8 @@ private: | |||||
double scaleFactor = 1.0; | double scaleFactor = 1.0; | ||||
bool isInDPIChange = false; | bool isInDPIChange = false; | ||||
bool isAccessibilityActive = false; | |||||
//============================================================================== | //============================================================================== | ||||
static MultiTouchMapper<DWORD> currentTouches; | static MultiTouchMapper<DWORD> currentTouches; | ||||
@@ -3907,6 +3921,24 @@ private: | |||||
case WM_GETDLGCODE: | case WM_GETDLGCODE: | ||||
return DLGC_WANTALLKEYS; | 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: | default: | ||||
break; | break; | ||||
} | } | ||||
@@ -204,7 +204,7 @@ void PropertyPanel::init() | |||||
addAndMakeVisible (viewport); | addAndMakeVisible (viewport); | ||||
viewport.setViewedComponent (propertyHolderComponent = new PropertyHolderComponent()); | viewport.setViewedComponent (propertyHolderComponent = new PropertyHolderComponent()); | ||||
viewport.setFocusContainer (true); | |||||
viewport.setFocusContainerType (FocusContainerType::keyboardFocusContainer); | |||||
} | } | ||||
PropertyPanel::~PropertyPanel() | PropertyPanel::~PropertyPanel() | ||||
@@ -424,6 +424,7 @@ void ComboBox::lookAndFeelChanged() | |||||
label->onTextChange = [this] { triggerAsyncUpdate(); }; | label->onTextChange = [this] { triggerAsyncUpdate(); }; | ||||
label->addMouseListener (this, false); | label->addMouseListener (this, false); | ||||
label->setAccessible (labelEditableState == labelIsEditable); | |||||
label->setColour (Label::backgroundColourId, Colours::transparentBlack); | label->setColour (Label::backgroundColourId, Colours::transparentBlack); | ||||
label->setColour (Label::textColourId, findColour (ComboBox::textColourId)); | 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::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); } | 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 | } // namespace juce |
@@ -419,6 +419,8 @@ public: | |||||
void valueChanged (Value&) override; | void valueChanged (Value&) override; | ||||
/** @internal */ | /** @internal */ | ||||
void parentHierarchyChanged() override; | void parentHierarchyChanged() override; | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
// These methods' bool parameters have changed: see their new method signatures. | // These methods' bool parameters have changed: see their new method signatures. | ||||
JUCE_DEPRECATED (void clear (bool)); | JUCE_DEPRECATED (void clear (bool)); | ||||
@@ -80,4 +80,10 @@ void ImageComponent::paint (Graphics& g) | |||||
g.drawImage (image, getLocalBounds().toFloat(), placement); | g.drawImage (image, getLocalBounds().toFloat(), placement); | ||||
} | } | ||||
//============================================================================== | |||||
std::unique_ptr<AccessibilityHandler> ImageComponent::createAccessibilityHandler() | |||||
{ | |||||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::image); | |||||
} | |||||
} // namespace juce | } // namespace juce |
@@ -68,6 +68,8 @@ public: | |||||
//============================================================================== | //============================================================================== | ||||
/** @internal */ | /** @internal */ | ||||
void paint (Graphics&) override; | void paint (Graphics&) override; | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
private: | private: | ||||
Image image; | Image image; | ||||
@@ -105,8 +105,13 @@ void Label::setEditable (bool editOnSingleClick, | |||||
editDoubleClick = editOnDoubleClick; | editDoubleClick = editOnDoubleClick; | ||||
lossOfFocusDiscardsChanges = lossOfFocusDiscards; | 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) | void Label::setJustificationType (Justification newJustification) | ||||
@@ -221,6 +226,7 @@ void Label::showEditor() | |||||
if (editor == nullptr) | if (editor == nullptr) | ||||
{ | { | ||||
editor.reset (createEditorComponent()); | editor.reset (createEditorComponent()); | ||||
editor->setSize (10, 10); | |||||
addAndMakeVisible (editor.get()); | addAndMakeVisible (editor.get()); | ||||
editor->setText (getText(), false); | editor->setText (getText(), false); | ||||
editor->setKeyboardType (keyboardType); | editor->setKeyboardType (keyboardType); | ||||
@@ -351,7 +357,9 @@ void Label::mouseDoubleClick (const MouseEvent& e) | |||||
if (editDoubleClick | if (editDoubleClick | ||||
&& isEnabled() | && isEnabled() | ||||
&& ! e.mods.isPopupMenu()) | && ! e.mods.isPopupMenu()) | ||||
{ | |||||
showEditor(); | showEditor(); | ||||
} | |||||
} | } | ||||
void Label::resized() | void Label::resized() | ||||
@@ -364,8 +372,11 @@ void Label::focusGained (FocusChangeType cause) | |||||
{ | { | ||||
if (editSingleClick | if (editSingleClick | ||||
&& isEnabled() | && isEnabled() | ||||
&& cause == focusChangedByTabKey) | |||||
&& (cause == focusChangedByTabKey | |||||
|| (cause == focusChangedDirectly && ! isCurrentlyModal()))) | |||||
{ | |||||
showEditor(); | showEditor(); | ||||
} | |||||
} | } | ||||
void Label::enablementChanged() | void Label::enablementChanged() | ||||
@@ -393,21 +404,45 @@ void Label::setMinimumHorizontalScale (const float newScale) | |||||
class LabelKeyboardFocusTraverser : public KeyboardFocusTraverser | class LabelKeyboardFocusTraverser : public KeyboardFocusTraverser | ||||
{ | { | ||||
public: | 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); | textEditorTextChanged (ed); | ||||
} | } | ||||
std::unique_ptr<AccessibilityHandler> Label::createAccessibilityHandler() | |||||
{ | |||||
return std::make_unique<LabelAccessibilityHandler> (*this); | |||||
} | |||||
} // namespace juce | } // namespace juce |
@@ -323,7 +323,7 @@ protected: | |||||
/** @internal */ | /** @internal */ | ||||
void enablementChanged() override; | void enablementChanged() override; | ||||
/** @internal */ | /** @internal */ | ||||
KeyboardFocusTraverser* createFocusTraverser() override; | |||||
std::unique_ptr<ComponentTraverser> createKeyboardFocusTraverser() override; | |||||
/** @internal */ | /** @internal */ | ||||
void textEditorTextChanged (TextEditor&) override; | void textEditorTextChanged (TextEditor&) override; | ||||
/** @internal */ | /** @internal */ | ||||
@@ -338,6 +338,8 @@ protected: | |||||
void valueChanged (Value&) override; | void valueChanged (Value&) override; | ||||
/** @internal */ | /** @internal */ | ||||
void callChangeListeners(); | void callChangeListeners(); | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
private: | private: | ||||
//============================================================================== | //============================================================================== | ||||
@@ -26,6 +26,34 @@ | |||||
namespace juce | 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, | class ListBox::RowComponent : public Component, | ||||
public TooltipClient | public TooltipClient | ||||
{ | { | ||||
@@ -35,16 +63,28 @@ public: | |||||
void paint (Graphics& g) override | void paint (Graphics& g) override | ||||
{ | { | ||||
if (auto* m = owner.getModel()) | 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) | 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(); | 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()) | if (auto* m = owner.getModel()) | ||||
@@ -57,6 +97,9 @@ public: | |||||
{ | { | ||||
addAndMakeVisible (customComponent.get()); | addAndMakeVisible (customComponent.get()); | ||||
customComponent->setBounds (getLocalBounds()); | customComponent->setBounds (getLocalBounds()); | ||||
if (customComponent->getAccessibilityHandler() != nullptr) | |||||
invalidateAccessibilityHandler(); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -85,7 +128,7 @@ public: | |||||
if (isEnabled()) | if (isEnabled()) | ||||
{ | { | ||||
if (owner.selectOnMouseDown && ! (selected || isInDragToScrollViewport())) | |||||
if (owner.selectOnMouseDown && ! (isSelected || isInDragToScrollViewport())) | |||||
performSelection (e, false); | performSelection (e, false); | ||||
else | else | ||||
selectRowOnMouseUp = true; | selectRowOnMouseUp = true; | ||||
@@ -150,10 +193,62 @@ public: | |||||
return {}; | 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; | ListBox& owner; | ||||
std::unique_ptr<Component> customComponent; | std::unique_ptr<Component> customComponent; | ||||
int row = -1; | 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) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RowComponent) | ||||
}; | }; | ||||
@@ -166,10 +261,13 @@ public: | |||||
ListViewport (ListBox& lb) : owner (lb) | ListViewport (ListBox& lb) : owner (lb) | ||||
{ | { | ||||
setWantsKeyboardFocus (false); | setWantsKeyboardFocus (false); | ||||
setAccessible (false); | |||||
auto content = new Component(); | |||||
setViewedComponent (content); | |||||
auto content = std::make_unique<Component>(); | |||||
content->setWantsKeyboardFocus (false); | content->setWantsKeyboardFocus (false); | ||||
content->setAccessible (false); | |||||
setViewedComponent (content.release()); | |||||
} | } | ||||
RowComponent* getComponentForRow (const int row) const noexcept | RowComponent* getComponentForRow (const int row) const noexcept | ||||
@@ -233,13 +331,12 @@ public: | |||||
auto y = getViewPositionY(); | auto y = getViewPositionY(); | ||||
auto w = content.getWidth(); | auto w = content.getWidth(); | ||||
const int numNeeded = 2 + getMaximumVisibleHeight() / rowH; | |||||
const int numNeeded = 4 + getMaximumVisibleHeight() / rowH; | |||||
rows.removeRange (numNeeded, rows.size()); | rows.removeRange (numNeeded, rows.size()); | ||||
while (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); | content.addAndMakeVisible (newRow); | ||||
} | } | ||||
@@ -247,9 +344,11 @@ public: | |||||
firstWholeIndex = (y + rowH - 1) / rowH; | firstWholeIndex = (y + rowH - 1) / rowH; | ||||
lastWholeIndex = (y + getMaximumVisibleHeight() - 1) / rowH; | lastWholeIndex = (y + getMaximumVisibleHeight() - 1) / rowH; | ||||
auto startIndex = jmax (0, firstIndex - 1); | |||||
for (int i = 0; i < numNeeded; ++i) | for (int i = 0; i < numNeeded; ++i) | ||||
{ | { | ||||
const int row = i + firstIndex; | |||||
const int row = i + startIndex; | |||||
if (auto* rowComp = getComponentForRow (row)) | if (auto* rowComp = getComponentForRow (row)) | ||||
{ | { | ||||
@@ -379,8 +478,9 @@ ListBox::ListBox (const String& name, ListBoxModel* const m) | |||||
viewport.reset (new ListViewport (*this)); | viewport.reset (new ListViewport (*this)); | ||||
addAndMakeVisible (viewport.get()); | addAndMakeVisible (viewport.get()); | ||||
ListBox::setWantsKeyboardFocus (true); | |||||
ListBox::colourChanged(); | |||||
setWantsKeyboardFocus (true); | |||||
setFocusContainerType (FocusContainerType::focusContainer); | |||||
colourChanged(); | |||||
} | } | ||||
ListBox::~ListBox() | 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) | Component* ListBoxModel::refreshComponentForRow (int, bool, Component* existingComponentToUpdate) | ||||
{ | { | ||||
@@ -946,6 +1051,7 @@ Component* ListBoxModel::refreshComponentForRow (int, bool, Component* existingC | |||||
return nullptr; | return nullptr; | ||||
} | } | ||||
String ListBoxModel::getNameForRow (int rowNumber) { return "Row " + String (rowNumber + 1); } | |||||
void ListBoxModel::listBoxItemClicked (int, const MouseEvent&) {} | void ListBoxModel::listBoxItemClicked (int, const MouseEvent&) {} | ||||
void ListBoxModel::listBoxItemDoubleClicked (int, const MouseEvent&) {} | void ListBoxModel::listBoxItemDoubleClicked (int, const MouseEvent&) {} | ||||
void ListBoxModel::backgroundClicked (const MouseEvent&) {} | void ListBoxModel::backgroundClicked (const MouseEvent&) {} | ||||
@@ -86,6 +86,12 @@ public: | |||||
virtual Component* refreshComponentForRow (int rowNumber, bool isRowSelected, | virtual Component* refreshComponentForRow (int rowNumber, bool isRowSelected, | ||||
Component* existingComponentToUpdate); | 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. | /** This can be overridden to react to the user clicking on a row. | ||||
@see listBoxItemDoubleClicked | @see listBoxItemDoubleClicked | ||||
*/ | */ | ||||
@@ -565,6 +571,8 @@ public: | |||||
/** @internal */ | /** @internal */ | ||||
void startDragAndDrop (const MouseEvent&, const SparseSet<int>& rowsToDrag, | void startDragAndDrop (const MouseEvent&, const SparseSet<int>& rowsToDrag, | ||||
const var& dragDescription, bool allowDraggingToOtherWindows); | const var& dragDescription, bool allowDraggingToOtherWindows); | ||||
/** @internal */ | |||||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||||
private: | private: | ||||
//============================================================================== | //============================================================================== | ||||