@@ -4,6 +4,32 @@ JUCE breaking changes | |||
Develop | |||
======= | |||
Change | |||
------ | |||
`Component::createFocusTraverser()` has been renamed to | |||
`Component::createKeyboardFocusTraverser()` and now returns a `std::unique_ptr` | |||
instead of a raw pointer. `Component::createFocusTraverser()` is a new method | |||
for controlling basic focus traversal and not keyboard focus traversal. | |||
Possible Issues | |||
--------------- | |||
Derived Components that override the old method will no longer compile. | |||
Workaround | |||
---------- | |||
Override the new method. Be careful to override | |||
`createKeyboardFocusTraverser()` and not `createFocusTraverser()` to ensure | |||
that the behaviour is the same. | |||
Rationale | |||
--------- | |||
The ownership of this method is now clearer as the previous code relied on the | |||
caller deleting the object. The name has changed to accomodate the new | |||
`Component::createFocusTraverser()` method that returns an object for | |||
determining basic focus traversal, of which keyboard focus is generally a | |||
subset. | |||
Change | |||
------ | |||
PluginDescription::uid has been deprecated and replaced with a new 'uniqueId' | |||
@@ -0,0 +1,51 @@ | |||
# JUCE Accessibility | |||
## What is supported? | |||
Currently JUCE supports VoiceOver on macOS and Narrator on Windows. The JUCE | |||
accessibility API exposes the following to these clients: | |||
- Title, description, and help text for UI elements | |||
- Programmatic access to UI elements and text | |||
- Interaction with UI elements | |||
- Full UI keyboard navigation | |||
- Posting notifications to listening clients | |||
## Customising Behaviour | |||
By default any visible and enabled `Component` is accessible to screen reader | |||
clients and exposes some basic information such as title, description, help | |||
text and its position in the hierarchy of UI elements. | |||
The `setTitle()`, `setDescription()` and `setHelpText()` methods can be used | |||
to customise the text that will be read out by accessibility clients when | |||
interacting with UI elements and the `setExplicitFocusOrder()`, | |||
`setFocusContainer()` and `createFocusTraverser()` methods can be used to | |||
control the parent/child relationships and the order of navigation between UI | |||
elements. | |||
## Custom Components | |||
For further customisation of accessibility behaviours the `AccessibilityHandler` | |||
class provides a unified API to the underlying native accessibility libraries. | |||
This class wraps a component with a given role specified by the | |||
`AccessibilityRole` enum and takes a list of optional actions and interfaces to | |||
provide programmatic access and control over the UI element. Its state is used | |||
to convey further information to accessibility clients via the | |||
`getCurrentState()` method. | |||
To implement the desired behaviours for a custom component, subclass | |||
`AccessibilityHandler` and return an instance of this from the | |||
`Component::createAccessibilityHandler()` method. | |||
Examples of some common UI element handlers for existing JUCE widgets can be | |||
found in the [`widget_handlers`](/modules/juce_gui_basics/accessibility/widget_handlers) directory. | |||
## Further Reading | |||
- [NSAccessibility protocol](https://developer.apple.com/documentation/appkit/nsaccessibility?language=objc) | |||
- [UI Automation for Win32 applications](https://docs.microsoft.com/en-us/windows/win32/winauto/entry-uiauto-win32) | |||
- A talk giving an overview of this feature from ADC 2020 can be found on | |||
YouTube at https://youtu.be/BqrEv4ApH3U | |||
@@ -29,6 +29,16 @@ namespace juce | |||
{ | |||
//============================================================================== | |||
inline Range<int> nsRangeToJuce (NSRange range) | |||
{ | |||
return { (int) range.location, (int) (range.location + range.length) }; | |||
} | |||
inline NSRange juceRangeToNS (Range<int> range) | |||
{ | |||
return NSMakeRange ((NSUInteger) range.getStart(), (NSUInteger) range.getLength()); | |||
} | |||
inline String nsStringToJuce (NSString* s) | |||
{ | |||
return CharPointer_UTF8 ([s UTF8String]); | |||
@@ -60,6 +60,7 @@ | |||
#include <numeric> | |||
#include <queue> | |||
#include <sstream> | |||
#include <typeindex> | |||
#include <unordered_set> | |||
#include <vector> | |||
@@ -64,6 +64,34 @@ namespace | |||
{ | |||
return CGPointMake ((CGFloat) p.x, (CGFloat) p.y); | |||
} | |||
#if JUCE_MAC | |||
inline CGFloat getMainScreenHeight() noexcept | |||
{ | |||
if ([[NSScreen screens] count] == 0) | |||
return 0.0f; | |||
return [[[NSScreen screens] objectAtIndex: 0] frame].size.height; | |||
} | |||
inline NSRect flippedScreenRect (NSRect r) noexcept | |||
{ | |||
r.origin.y = getMainScreenHeight() - (r.origin.y + r.size.height); | |||
return r; | |||
} | |||
inline NSPoint flippedScreenPoint (NSPoint p) noexcept | |||
{ | |||
p.y = getMainScreenHeight() - p.y; | |||
return p; | |||
} | |||
template <class PointType> | |||
Point<int> convertToIntPoint (PointType p) noexcept | |||
{ | |||
return Point<int> (roundToInt (p.x), roundToInt (p.y)); | |||
} | |||
#endif | |||
} | |||
CGImageRef juce_createCoreGraphicsImage (const Image&, CGColorSpaceRef, bool mustOutliveSource); | |||
@@ -0,0 +1,119 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** An action that can be performed by an accessible UI element. | |||
@tags{Accessibility} | |||
*/ | |||
enum class AccessibilityActionType | |||
{ | |||
/** Represents a "press" action. | |||
This will be called when the user "clicks" the UI element using an | |||
accessibility client. | |||
*/ | |||
press, | |||
/** Represents a "toggle" action. | |||
This will be called when the user toggles the state of a UI element, | |||
for example a toggle button or the selection of a list item. | |||
*/ | |||
toggle, | |||
/** Indicates that the UI element has received focus. | |||
This will be called when a UI element receives focus from an accessibility | |||
client, or keyboard focus from the application. | |||
*/ | |||
focus, | |||
/** Represents the user showing a contextual menu for a UI element. | |||
This will be called for UI elements which expand and collapse to | |||
show contextual information or menus, or show a popup. | |||
*/ | |||
showMenu | |||
}; | |||
/** A simple wrapper for building a collection of supported accessibility actions | |||
and corresponding callbacks for a UI element. | |||
Pass one of these when constructing an `AccessibilityHandler` to enable users | |||
to interact with a UI element via the supported actions. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API AccessibilityActions | |||
{ | |||
public: | |||
/** Constructor. | |||
Creates a default AccessibilityActions object with no action callbacks. | |||
*/ | |||
AccessibilityActions() = default; | |||
/** Adds an action. | |||
When the user performs this action with an accessibility client | |||
`actionCallback` will be called. | |||
Returns a reference to itself so that several calls can be chained. | |||
*/ | |||
AccessibilityActions& addAction (AccessibilityActionType type, | |||
std::function<void()> actionCallback) | |||
{ | |||
actionMap[type] = std::move (actionCallback); | |||
return *this; | |||
} | |||
/** Returns true if the specified action is supported. */ | |||
bool contains (AccessibilityActionType type) const | |||
{ | |||
return actionMap.find (type) != actionMap.end(); | |||
} | |||
/** If an action has been registered for the provided action type, invokes the | |||
action and returns true. Otherwise, returns false. | |||
*/ | |||
bool invoke (AccessibilityActionType type) const | |||
{ | |||
auto iter = actionMap.find (type); | |||
if (iter == actionMap.end()) | |||
return false; | |||
iter->second(); | |||
return true; | |||
} | |||
private: | |||
std::map<AccessibilityActionType, std::function<void()>> actionMap; | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,75 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** A list of events that can be notified to any subscribed accessibility clients. | |||
To post a notification, call `AccessibilityHandler::notifyAccessibilityEvent` | |||
on the associated handler with the appropriate `AccessibilityEvent` type and | |||
listening clients will be notified. | |||
@tags{Accessibility} | |||
*/ | |||
enum class AccessibilityEvent | |||
{ | |||
/** Indicates that the UI element's value has changed. | |||
This should be called on the handler that implements `AccessibilityValueInterface` | |||
for the UI element that has changed. | |||
*/ | |||
valueChanged, | |||
/** Indicates that the structure of the UI elements has changed in a | |||
significant way. | |||
This should be posted on the top-level handler whose structure has changed. | |||
*/ | |||
structureChanged, | |||
/** Indicates that the selection of a text element has changed. | |||
This should be called on the handler that implements `AccessibilityTextInterface` | |||
for the text element that has changed. | |||
*/ | |||
textSelectionChanged, | |||
/** Indicates that the visible text of a text element has changed. | |||
This should be called on the handler that implements `AccessibilityTextInterface` | |||
for the text element that has changed. | |||
*/ | |||
textChanged, | |||
/** Indicates that the selection of rows in a list or table has changed. | |||
This should be called on the handler that implements `AccessibilityTableInterface` | |||
for the UI element that has changed. | |||
*/ | |||
rowSelectionChanged | |||
}; | |||
} |
@@ -0,0 +1,70 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** The list of available roles for an AccessibilityHandler object. | |||
When creating a custom AccessibilityHandler you should select the role that | |||
best describes the UI element being represented. | |||
@tags{Accessibility} | |||
*/ | |||
enum class AccessibilityRole | |||
{ | |||
button, | |||
toggleButton, | |||
radioButton, | |||
comboBox, | |||
image, | |||
slider, | |||
staticText, | |||
editableText, | |||
menuItem, | |||
menuBar, | |||
popupMenu, | |||
table, | |||
tableHeader, | |||
column, | |||
row, | |||
cell, | |||
hyperlink, | |||
list, | |||
listItem, | |||
tree, | |||
treeItem, | |||
progressBar, | |||
group, | |||
dialogWindow, | |||
window, | |||
scrollBar, | |||
tooltip, | |||
splashScreen, | |||
ignored, | |||
unspecified | |||
}; | |||
} |
@@ -0,0 +1,61 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** An abstract interface which represents a UI element that supports a cell interface. | |||
This typically represents a single cell inside of a UI element which implements an | |||
AccessibilityTableInterface. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API AccessibilityCellInterface | |||
{ | |||
public: | |||
/** Destructor. */ | |||
virtual ~AccessibilityCellInterface() = default; | |||
/** Returns the column index of the cell in the table. */ | |||
virtual int getColumnIndex() const = 0; | |||
/** Returns the number of columns occupied by the cell in the table. */ | |||
virtual int getColumnSpan() const = 0; | |||
/** Returns the row index of the cell in the table. */ | |||
virtual int getRowIndex() const = 0; | |||
/** Returns the number of rows occupied by the cell in the table. */ | |||
virtual int getRowSpan() const = 0; | |||
/** Returns the indentation level for the cell. */ | |||
virtual int getDisclosureLevel() const = 0; | |||
/** Returns the AccessibilityHandler of the table which contains the cell. */ | |||
virtual const AccessibilityHandler* getTableHandler() const = 0; | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,54 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** An abstract interface which represents a UI element that supports a table interface. | |||
Examples of UI elements which typically support a table interface are lists, tables, | |||
and trees. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API AccessibilityTableInterface | |||
{ | |||
public: | |||
/** Destructor. */ | |||
virtual ~AccessibilityTableInterface() = default; | |||
/** Returns the total number of rows in the table. */ | |||
virtual int getNumRows() const = 0; | |||
/** Returns the total number of columns in the table. */ | |||
virtual int getNumColumns() const = 0; | |||
/** Returns the AccessibilityHandler for one of the cells in the table, or | |||
nullptr if there is no cell at the specified position. | |||
*/ | |||
virtual const AccessibilityHandler* getCellHandler (int row, int column) const = 0; | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,78 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** An abstract interface which represents a UI element that supports a text interface. | |||
A UI element can use this interface to provide extended textual information which | |||
cannot be conveyed using just the title, description, and help text properties of | |||
AccessibilityHandler. This is typically for text that an accessibility client might | |||
want to read line-by-line, or provide text selection and input for. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API AccessibilityTextInterface | |||
{ | |||
public: | |||
/** Destructor. */ | |||
virtual ~AccessibilityTextInterface() = default; | |||
/** Returns true if the text being displayed is protected and should not be | |||
exposed to the user, for example a password entry field. | |||
*/ | |||
virtual bool isDisplayingProtectedText() const = 0; | |||
/** Returns the total number of characters in the text element. */ | |||
virtual int getTotalNumCharacters() const = 0; | |||
/** Returns the range of characters that are currently selected, or an empty | |||
range if nothing is selected. | |||
*/ | |||
virtual Range<int> getSelection() const = 0; | |||
/** Selects a section of the text. */ | |||
virtual void setSelection (Range<int> newRange) = 0; | |||
/** Gets the current text insertion position, if supported. */ | |||
virtual int getTextInsertionOffset() const = 0; | |||
/** Returns a section of text. */ | |||
virtual String getText (Range<int> range) const = 0; | |||
/** Replaces the text with a new string. */ | |||
virtual void setText (const String& newText) = 0; | |||
/** Returns the bounding box in screen coordinates for a range of text. | |||
As the range may span multiple lines, this method returns a RectangleList. | |||
*/ | |||
virtual RectangleList<int> getTextBounds (Range<int> textRange) const = 0; | |||
/** Returns the index of the character at a given position in screen coordinates. */ | |||
virtual int getOffsetAtPoint (Point<int> point) const = 0; | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,222 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** An abstract interface representing the value of an accessibility element. | |||
Values should be used when information needs to be conveyed which cannot | |||
be represented by the accessibility element's label alone. For example, a | |||
gain slider with the label "Gain" needs to also provide a value for its | |||
position whereas a "Save" button does not. | |||
This class allows for full control over the value text/numeric conversion, | |||
ranged, and read-only properties but in most cases you'll want to use one | |||
of the derived classes below which handle some of this for you. | |||
@see AccessibilityTextValueInterface, AccessibilityNumericValueInterface, | |||
AccessibilityRangedNumericValueInterface | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API AccessibilityValueInterface | |||
{ | |||
public: | |||
/** Destructor. */ | |||
virtual ~AccessibilityValueInterface() = default; | |||
/** Returns true if the value is read-only and cannot be modified by an | |||
accessibility client. | |||
@see setValue, setValueAsString | |||
*/ | |||
virtual bool isReadOnly() const = 0; | |||
/** Returns the current value as a double. */ | |||
virtual double getCurrentValue() const = 0; | |||
/** Returns the current value as a String. */ | |||
virtual String getCurrentValueAsString() const = 0; | |||
/** Sets the current value to a new double value. */ | |||
virtual void setValue (double newValue) = 0; | |||
/** Sets the current value to a new String value. */ | |||
virtual void setValueAsString (const String& newValue) = 0; | |||
/** Represents the range of this value, if supported. | |||
Return one of these from the `getRange()` method, providing a minimum, | |||
maximum, and interval value for the range to indicate that this is a | |||
ranged value. | |||
The default state is an "invalid" range, indicating that the accessibility | |||
element does not support ranged values. | |||
@see AccessibilityRangedNumericValueInterface | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API AccessibleValueRange | |||
{ | |||
public: | |||
/** Constructor. | |||
Creates a default, "invalid" range that can be returned from | |||
`AccessibilityValueInterface::getRange()` to indicate that the value | |||
interface does not support ranged values. | |||
*/ | |||
AccessibleValueRange() = default; | |||
/** The minimum and maximum values for this range, inclusive. */ | |||
struct JUCE_API MinAndMax { double min, max; }; | |||
/** Constructor. | |||
Creates a valid AccessibleValueRange with the provided minimum, maximum, | |||
and interval values. | |||
*/ | |||
AccessibleValueRange (MinAndMax valueRange, double interval) | |||
: valid (true), | |||
range (valueRange), | |||
stepSize (interval) | |||
{ | |||
jassert (range.min < range.max); | |||
} | |||
/** Returns true if this represents a valid range. */ | |||
bool isValid() const noexcept { return valid; } | |||
/** Returns the minimum value for this range. */ | |||
double getMinimumValue() const noexcept { return range.min; } | |||
/** Returns the maxiumum value for this range. */ | |||
double getMaximumValue() const noexcept { return range.max; } | |||
/** Returns the interval for this range. */ | |||
double getInterval() const noexcept { return stepSize; } | |||
private: | |||
bool valid = false; | |||
MinAndMax range {}; | |||
double stepSize = 0.0; | |||
}; | |||
/** If this is a ranged value, this should return a valid AccessibleValueRange | |||
object representing the supported numerical range. | |||
*/ | |||
virtual AccessibleValueRange getRange() const = 0; | |||
}; | |||
//============================================================================== | |||
/** A value interface that represents a text value. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API AccessibilityTextValueInterface : public AccessibilityValueInterface | |||
{ | |||
public: | |||
/** Returns true if the value is read-only and cannot be modified by an | |||
accessibility client. | |||
@see setValueAsString | |||
*/ | |||
bool isReadOnly() const override = 0; | |||
/** Returns the current value. */ | |||
String getCurrentValueAsString() const override = 0; | |||
/** Sets the current value to a new value. */ | |||
void setValueAsString (const String& newValue) override = 0; | |||
/** @internal */ | |||
double getCurrentValue() const final { return getCurrentValueAsString().getDoubleValue(); } | |||
/** @internal */ | |||
void setValue (double newValue) final { setValueAsString (String (newValue)); } | |||
/** @internal */ | |||
AccessibleValueRange getRange() const final { return {}; } | |||
}; | |||
//============================================================================== | |||
/** A value interface that represents a non-ranged numeric value. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API AccessibilityNumericValueInterface : public AccessibilityValueInterface | |||
{ | |||
public: | |||
/** Returns true if the value is read-only and cannot be modified by an | |||
accessibility client. | |||
@see setValue | |||
*/ | |||
bool isReadOnly() const override = 0; | |||
/** Returns the current value. */ | |||
double getCurrentValue() const override = 0; | |||
/** Sets the current value to a new value. */ | |||
void setValue (double newValue) override = 0; | |||
/** @internal */ | |||
String getCurrentValueAsString() const final { return String (getCurrentValue()); } | |||
/** @internal */ | |||
void setValueAsString (const String& newValue) final { setValue (newValue.getDoubleValue()); } | |||
/** @internal */ | |||
AccessibleValueRange getRange() const final { return {}; } | |||
}; | |||
//============================================================================== | |||
/** A value interface that represents a ranged numeric value. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API AccessibilityRangedNumericValueInterface : public AccessibilityValueInterface | |||
{ | |||
public: | |||
/** Returns true if the value is read-only and cannot be modified by an | |||
accessibility client. | |||
@see setValueAsString | |||
*/ | |||
bool isReadOnly() const override = 0; | |||
/** Returns the current value. */ | |||
double getCurrentValue() const override = 0; | |||
/** Sets the current value to a new value. */ | |||
void setValue (double newValue) override = 0; | |||
/** Returns the range. */ | |||
AccessibleValueRange getRange() const override = 0; | |||
/** @internal */ | |||
String getCurrentValueAsString() const final { return String (getCurrentValue()); } | |||
/** @internal */ | |||
void setValueAsString (const String& newValue) final { setValue (newValue.getDoubleValue()); } | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,346 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
AccessibilityHandler* AccessibilityHandler::currentlyFocusedHandler = nullptr; | |||
enum class InternalAccessibilityEvent | |||
{ | |||
elementCreated, | |||
elementDestroyed, | |||
focusChanged, | |||
windowOpened, | |||
windowClosed | |||
}; | |||
void notifyAccessibilityEventInternal (const AccessibilityHandler& handler, InternalAccessibilityEvent event); | |||
inline String getAccessibleApplicationOrPluginName() | |||
{ | |||
#if defined (JucePlugin_Name) | |||
return JucePlugin_Name; | |||
#else | |||
if (auto* app = JUCEApplicationBase::getInstance()) | |||
return app->getApplicationName(); | |||
return "JUCE Application"; | |||
#endif | |||
} | |||
AccessibilityHandler::AccessibilityHandler (Component& comp, | |||
AccessibilityRole accessibilityRole, | |||
AccessibilityActions accessibilityActions, | |||
Interfaces interfacesIn) | |||
: component (comp), | |||
typeIndex (typeid (component)), | |||
role (accessibilityRole), | |||
actions (std::move (accessibilityActions)), | |||
interfaces (std::move (interfacesIn)), | |||
nativeImpl (createNativeImpl (*this)) | |||
{ | |||
notifyAccessibilityEventInternal (*this, InternalAccessibilityEvent::elementCreated); | |||
} | |||
AccessibilityHandler::~AccessibilityHandler() | |||
{ | |||
giveAwayFocus(); | |||
notifyAccessibilityEventInternal (*this, InternalAccessibilityEvent::elementDestroyed); | |||
} | |||
//============================================================================== | |||
AccessibleState AccessibilityHandler::getCurrentState() const | |||
{ | |||
auto state = AccessibleState().withFocusable(); | |||
return hasFocus (false) ? state.withFocused() : state; | |||
} | |||
static bool isComponentVisibleWithinWindow (const Component& comp) | |||
{ | |||
if (auto* peer = comp.getPeer()) | |||
return ! peer->getAreaCoveredBy (comp).getIntersection (peer->getComponent().getLocalBounds()).isEmpty(); | |||
return false; | |||
} | |||
static bool isComponentVisibleWithinParent (Component* comp) | |||
{ | |||
if (auto* parent = comp->getParentComponent()) | |||
{ | |||
if (comp->getBoundsInParent().getIntersection (parent->getLocalBounds()).isEmpty()) | |||
return false; | |||
return isComponentVisibleWithinParent (parent); | |||
} | |||
return true; | |||
} | |||
bool AccessibilityHandler::isIgnored() const | |||
{ | |||
const auto state = getCurrentState(); | |||
return role == AccessibilityRole::ignored | |||
|| state.isIgnored() | |||
|| ! component.isShowing() | |||
|| (! state.isAccessibleOffscreen() | |||
&& (! isComponentVisibleWithinParent (&component) | |||
|| ! isComponentVisibleWithinWindow (component))); | |||
} | |||
//============================================================================== | |||
const AccessibilityActions& AccessibilityHandler::getActions() const noexcept | |||
{ | |||
return actions; | |||
} | |||
AccessibilityValueInterface* AccessibilityHandler::getValueInterface() const | |||
{ | |||
return interfaces.value.get(); | |||
} | |||
AccessibilityTableInterface* AccessibilityHandler::getTableInterface() const | |||
{ | |||
return interfaces.table.get(); | |||
} | |||
AccessibilityCellInterface* AccessibilityHandler::getCellInterface() const | |||
{ | |||
return interfaces.cell.get(); | |||
} | |||
AccessibilityTextInterface* AccessibilityHandler::getTextInterface() const | |||
{ | |||
return interfaces.text.get(); | |||
} | |||
//============================================================================== | |||
static AccessibilityHandler* findEnclosingHandler (Component* comp) | |||
{ | |||
if (comp != nullptr) | |||
{ | |||
if (auto* handler = comp->getAccessibilityHandler()) | |||
return handler; | |||
return findEnclosingHandler (comp->getParentComponent()); | |||
} | |||
return nullptr; | |||
} | |||
static AccessibilityHandler* getUnignoredAncestor (AccessibilityHandler* handler) | |||
{ | |||
while (handler != nullptr | |||
&& handler->isIgnored() | |||
&& handler->getParent() != nullptr) | |||
{ | |||
handler = handler->getParent(); | |||
} | |||
return handler; | |||
} | |||
static AccessibilityHandler* findFirstUnignoredChild (const std::vector<AccessibilityHandler*>& handlers) | |||
{ | |||
if (! handlers.empty()) | |||
{ | |||
const auto iter = std::find_if (handlers.cbegin(), handlers.cend(), | |||
[] (const AccessibilityHandler* handler) { return ! handler->isIgnored(); }); | |||
if (iter != handlers.cend()) | |||
return *iter; | |||
for (auto* handler : handlers) | |||
if (auto* unignored = findFirstUnignoredChild (handler->getChildren())) | |||
return unignored; | |||
} | |||
return nullptr; | |||
} | |||
static AccessibilityHandler* getFirstUnignoredDescendant (AccessibilityHandler* handler) | |||
{ | |||
if (handler != nullptr && handler->isIgnored()) | |||
return findFirstUnignoredChild (handler->getChildren()); | |||
return handler; | |||
} | |||
AccessibilityHandler* AccessibilityHandler::getParent() const | |||
{ | |||
if (auto* focusContainer = component.findFocusContainer()) | |||
return getUnignoredAncestor (findEnclosingHandler (focusContainer)); | |||
return nullptr; | |||
} | |||
std::vector<AccessibilityHandler*> AccessibilityHandler::getChildren() const | |||
{ | |||
if (! component.isFocusContainer() && component.getParentComponent() != nullptr) | |||
return {}; | |||
std::vector<AccessibilityHandler*> children; | |||
if (auto traverser = component.createFocusTraverser()) | |||
{ | |||
for (auto* focusableChild : traverser->getAllComponents (&component)) | |||
{ | |||
if (auto* handler = findEnclosingHandler (focusableChild)) | |||
{ | |||
if (! isParentOf (handler)) | |||
continue; | |||
if (auto* unignored = getFirstUnignoredDescendant (handler)) | |||
if (std::find (children.cbegin(), children.cend(), unignored) == children.cend()) | |||
children.push_back (unignored); | |||
} | |||
} | |||
} | |||
return children; | |||
} | |||
bool AccessibilityHandler::isParentOf (const AccessibilityHandler* possibleChild) const noexcept | |||
{ | |||
while (possibleChild != nullptr) | |||
{ | |||
possibleChild = possibleChild->getParent(); | |||
if (possibleChild == this) | |||
return true; | |||
} | |||
return false; | |||
} | |||
AccessibilityHandler* AccessibilityHandler::getChildAt (Point<int> screenPoint) | |||
{ | |||
if (auto* comp = Desktop::getInstance().findComponentAt (screenPoint)) | |||
if (isParentOf (comp->getAccessibilityHandler())) | |||
return getUnignoredAncestor (findEnclosingHandler (comp)); | |||
return nullptr; | |||
} | |||
AccessibilityHandler* AccessibilityHandler::getChildFocus() | |||
{ | |||
return hasFocus (true) ? getUnignoredAncestor (currentlyFocusedHandler) | |||
: nullptr; | |||
} | |||
bool AccessibilityHandler::hasFocus (bool trueIfChildFocused) const | |||
{ | |||
return currentlyFocusedHandler != nullptr | |||
&& (currentlyFocusedHandler == this | |||
|| (trueIfChildFocused && isParentOf (currentlyFocusedHandler))); | |||
} | |||
void AccessibilityHandler::grabFocus() | |||
{ | |||
if (! hasFocus (false)) | |||
grabFocusInternal (true); | |||
} | |||
void AccessibilityHandler::giveAwayFocus() const | |||
{ | |||
if (hasFocus (true)) | |||
giveAwayFocusInternal(); | |||
} | |||
void AccessibilityHandler::grabFocusInternal (bool canTryParent) | |||
{ | |||
if (getCurrentState().isFocusable() && ! isIgnored()) | |||
{ | |||
takeFocus(); | |||
return; | |||
} | |||
if (isParentOf (currentlyFocusedHandler) && ! currentlyFocusedHandler->isIgnored()) | |||
return; | |||
if (component.isFocusContainer() || component.getParentComponent() == nullptr) | |||
{ | |||
if (auto traverser = component.createFocusTraverser()) | |||
{ | |||
if (auto* defaultComp = traverser->getDefaultComponent (&component)) | |||
{ | |||
if (auto* handler = getUnignoredAncestor (findEnclosingHandler (defaultComp))) | |||
{ | |||
if (isParentOf (handler)) | |||
{ | |||
handler->grabFocusInternal (false); | |||
return; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
if (canTryParent) | |||
if (auto* parent = getParent()) | |||
parent->grabFocusInternal (true); | |||
} | |||
void AccessibilityHandler::giveAwayFocusInternal() const | |||
{ | |||
currentlyFocusedHandler = nullptr; | |||
notifyAccessibilityEventInternal (*this, InternalAccessibilityEvent::focusChanged); | |||
if (auto* focusedComponent = Component::getCurrentlyFocusedComponent()) | |||
if (auto* handler = focusedComponent->getAccessibilityHandler()) | |||
handler->grabFocus(); | |||
} | |||
void AccessibilityHandler::takeFocus() | |||
{ | |||
currentlyFocusedHandler = this; | |||
WeakReference<Component> weakComponent (&component); | |||
actions.invoke (AccessibilityActionType::focus); | |||
if (weakComponent != nullptr | |||
&& component.getWantsKeyboardFocus() | |||
&& ! component.hasKeyboardFocus (true)) | |||
{ | |||
component.grabKeyboardFocus(); | |||
} | |||
notifyAccessibilityEventInternal (*this, InternalAccessibilityEvent::focusChanged); | |||
} | |||
//============================================================================== | |||
#if ! (JUCE_MAC || JUCE_WINDOWS) | |||
class AccessibilityHandler::AccessibilityNativeImpl { public: AccessibilityNativeImpl (AccessibilityHandler&) {} }; | |||
void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent) const {} | |||
void AccessibilityHandler::postAnnouncement (const String&, AnnouncementPriority) {} | |||
AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const { return nullptr; } | |||
AccessibilityHandler::AccessibilityNativeImpl* AccessibilityHandler::createNativeImpl (AccessibilityHandler&) { return nullptr; } | |||
void AccessibilityHandler::DestroyNativeImpl::operator() (AccessibilityHandler::AccessibilityNativeImpl*) const noexcept {} | |||
void notifyAccessibilityEventInternal (const AccessibilityHandler&, InternalAccessibilityEvent) {} | |||
#endif | |||
} // namespace juce |
@@ -0,0 +1,325 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
class AccessibilityNativeHandle; | |||
/** Base class for accessible Components. | |||
This class wraps a Component and provides methods that allow an accessibility client, | |||
such as VoiceOver on macOS, or Narrator on Windows, to control it. | |||
It handles hierarchical navigation, properties, state, and various interfaces. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API AccessibilityHandler | |||
{ | |||
public: | |||
/** Utility struct which holds one or more accessibility interfaces. | |||
The main purpose of this class is to provide convenience constructors from each | |||
of the four types of accessibility interface. | |||
*/ | |||
struct JUCE_API Interfaces | |||
{ | |||
Interfaces() = default; | |||
Interfaces (std::unique_ptr<AccessibilityValueInterface> ptr) : value (std::move (ptr)) {} | |||
Interfaces (std::unique_ptr<AccessibilityTextInterface> ptr) : text (std::move (ptr)) {} | |||
Interfaces (std::unique_ptr<AccessibilityTableInterface> ptr) : table (std::move (ptr)) {} | |||
Interfaces (std::unique_ptr<AccessibilityCellInterface> ptr) : cell (std::move (ptr)) {} | |||
Interfaces (std::unique_ptr<AccessibilityValueInterface> valueIn, | |||
std::unique_ptr<AccessibilityTextInterface> textIn, | |||
std::unique_ptr<AccessibilityTableInterface> tableIn, | |||
std::unique_ptr<AccessibilityCellInterface> cellIn) | |||
: value (std::move (valueIn)), | |||
text (std::move (textIn)), | |||
table (std::move (tableIn)), | |||
cell (std::move (cellIn)) | |||
{ | |||
} | |||
std::unique_ptr<AccessibilityValueInterface> value; | |||
std::unique_ptr<AccessibilityTextInterface> text; | |||
std::unique_ptr<AccessibilityTableInterface> table; | |||
std::unique_ptr<AccessibilityCellInterface> cell; | |||
}; | |||
/** Constructor. | |||
This will create a AccessibilityHandler which wraps the provided Component and makes | |||
it visible to accessibility clients. You must also specify a role for the UI element | |||
from the `AccessibilityRole` list which best describes it. | |||
To enable users to interact with the UI element you should provide the set of supported | |||
actions and their associated callbacks via the `accessibilityActions` parameter. | |||
For UI elements that support more complex interaction the value, text, table, and cell | |||
interfaces should be implemented as required and passed as the final argument of this | |||
constructor. See the documentation of these classes for more information about the | |||
types of control they represent and which methods need to be implemented. | |||
*/ | |||
AccessibilityHandler (Component& componentToWrap, | |||
AccessibilityRole accessibilityRole, | |||
AccessibilityActions actions = {}, | |||
Interfaces interfaces = {}); | |||
/** Destructor. */ | |||
virtual ~AccessibilityHandler(); | |||
//============================================================================== | |||
/** Returns the Component that this handler represents. */ | |||
const Component& getComponent() const noexcept { return component; } | |||
/** Returns the Component that this handler represents. */ | |||
Component& getComponent() noexcept { return component; } | |||
//============================================================================== | |||
/** The type of UI element that this accessibility handler represents. | |||
@see AccessibilityRole | |||
*/ | |||
AccessibilityRole getRole() const noexcept { return role; } | |||
/** The title of the UI element. | |||
This will be read out by the system and should be concise, preferably matching | |||
the visible title of the UI element (if any). For example, this might be the | |||
text of a button or a simple label. | |||
The default implementation will call `Component::getTitle()`, but you can override | |||
this to return a different string if required. | |||
If neither a name nor a description is provided then the UI element may be | |||
ignored by accessibility clients. | |||
This must be a localised string. | |||
*/ | |||
virtual String getTitle() const { return component.getTitle(); } | |||
/** A short description of the UI element. | |||
This may be read out by the system. It should not include the type of the UI | |||
element and should ideally be a single word, for example "Open" for a button | |||
that opens a window. | |||
The default implementation will call `Component::getDescription()`, but you | |||
can override this to return a different string if required. | |||
If neither a name nor a description is provided then the UI element may be | |||
ignored by accessibility clients. | |||
This must be a localised string. | |||
*/ | |||
virtual String getDescription() const { return component.getDescription(); } | |||
/** Some help text for the UI element (if required). | |||
This may be read out by the system. This string functions in a similar way to | |||
a tooltip, for example "Click to open window." for a button which opens a window. | |||
The default implementation will call `Component::getHelpText()`, but you can | |||
override this to return a different string if required. | |||
This must be a localised string. | |||
*/ | |||
virtual String getHelp() const { return component.getHelpText(); } | |||
/** Returns the current state of the UI element. | |||
The default implementation of this method will set the focusable flag and, if | |||
this UI element is currently focused, will also set the focused flag. | |||
*/ | |||
virtual AccessibleState getCurrentState() const; | |||
/** Returns true if this UI element should be ignored by accessibility clients. */ | |||
bool isIgnored() const; | |||
//============================================================================== | |||
/** Returns the set of actions that the UI element supports and the associated | |||
callbacks. | |||
*/ | |||
const AccessibilityActions& getActions() const noexcept; | |||
/** Returns the value interface for this UI element, or nullptr if it is not supported. | |||
@see AccessibilityValueInterface | |||
*/ | |||
AccessibilityValueInterface* getValueInterface() const; | |||
/** Returns the table interface for this UI element, or nullptr if it is not supported. | |||
@see AccessibilityTableInterface | |||
*/ | |||
AccessibilityTableInterface* getTableInterface() const; | |||
/** Returns the cell interface for this UI element, or nullptr if it is not supported. | |||
@see AccessibilityCellInterface | |||
*/ | |||
AccessibilityCellInterface* getCellInterface() const; | |||
/** Returns the text interface for this UI element, or nullptr if it is not supported. | |||
@see AccessibilityTextInterface | |||
*/ | |||
AccessibilityTextInterface* getTextInterface() const; | |||
//============================================================================== | |||
/** Returns the first unignored parent of this UI element in the accessibility hierarchy, | |||
or nullptr if this is a root element without a parent. | |||
*/ | |||
AccessibilityHandler* getParent() const; | |||
/** Returns the unignored children of this UI element in the accessibility hierarchy. */ | |||
std::vector<AccessibilityHandler*> getChildren() const; | |||
/** Checks whether a given UI element is a child of this one in the accessibility | |||
hierarchy. | |||
*/ | |||
bool isParentOf (const AccessibilityHandler* possibleChild) const noexcept; | |||
/** Returns the deepest child of this UI element in the accessibility hierarchy that | |||
contains the given screen point, or nullptr if there is no child at this point. | |||
*/ | |||
AccessibilityHandler* getChildAt (Point<int> screenPoint); | |||
/** Returns the deepest UI element which currently has focus. | |||
This can be a child of this UI element or, if no child is focused, | |||
this element itself. | |||
Note that this can be different to the value of the Component with keyboard | |||
focus returned by Component::getCurrentlyFocusedComponent(). | |||
@see hasFocus | |||
*/ | |||
AccessibilityHandler* getChildFocus(); | |||
/** Returns true if this UI element has the focus. | |||
@param trueIfChildFocused if this is true, this method will also return true | |||
if any child of this UI element in the accessibility | |||
hierarchy has focus | |||
*/ | |||
bool hasFocus (bool trueIfChildFocused) const; | |||
/** Tries to give focus to this UI element. | |||
If the UI element is focusable, as indicated by AccessibleState::isFocusable(), | |||
this will perform its AccessibilityActionType::focus action, try to give keyboard | |||
focus to the Component it represents, and notify any listening accessibility | |||
clients that the current focus has changed. | |||
@see hasFocus, giveAwayFocus | |||
*/ | |||
void grabFocus(); | |||
/** If this UI element or any of its children in the accessibility hierarchy currently | |||
have focus, this will defocus it. | |||
This will also give away the keyboard focus from the Component it represents, and | |||
notify any listening accessibility clients that the current focus has changed. | |||
@see hasFocus, grabFocus | |||
*/ | |||
void giveAwayFocus() const; | |||
//============================================================================== | |||
/** Used to send a notification to any observing accessibility clients that something | |||
has changed in the UI element. | |||
@see AccessibilityEvent | |||
*/ | |||
void notifyAccessibilityEvent (AccessibilityEvent event) const; | |||
/** A priority level that can help an accessibility client determine how to handle | |||
an announcement request. | |||
Exactly what this controls is platform-specific, but generally a low priority | |||
announcement will be read when the screen reader is free, whereas a high priority | |||
announcement will interrupt the current speech. | |||
*/ | |||
enum class AnnouncementPriority | |||
{ | |||
low, | |||
medium, | |||
high | |||
}; | |||
/** Posts an announcement to be made to the user. | |||
@param announcementString a localised string containing the announcement to be read out | |||
@param priority the appropriate priority level for the announcement | |||
*/ | |||
static void postAnnouncement (const String& announcementString, AnnouncementPriority priority); | |||
//============================================================================== | |||
/** @internal */ | |||
AccessibilityNativeHandle* getNativeImplementation() const; | |||
/** @internal */ | |||
std::type_index getTypeIndex() const { return typeIndex; } | |||
private: | |||
//============================================================================== | |||
friend class AccessibilityNativeHandle; | |||
//============================================================================== | |||
void grabFocusInternal (bool); | |||
void giveAwayFocusInternal() const; | |||
void takeFocus(); | |||
static AccessibilityHandler* currentlyFocusedHandler; | |||
//============================================================================== | |||
Component& component; | |||
std::type_index typeIndex; | |||
const AccessibilityRole role; | |||
AccessibilityActions actions; | |||
Interfaces interfaces; | |||
//============================================================================== | |||
class AccessibilityNativeImpl; | |||
struct DestroyNativeImpl | |||
{ | |||
void operator() (AccessibilityNativeImpl*) const noexcept; | |||
}; | |||
static AccessibilityNativeImpl* createNativeImpl (AccessibilityHandler&); | |||
std::unique_ptr<AccessibilityNativeImpl, DestroyNativeImpl> nativeImpl; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityHandler) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,227 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** Represents the state of an accessible UI element. | |||
An instance of this class is returned by `AccessibilityHandler::getCurrentState()` | |||
to convey its current state to an accessibility client. | |||
@see AccessibilityHandler | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API AccessibleState | |||
{ | |||
public: | |||
/** Constructor. | |||
Represents a "default" state with no flags set. To set a flag, use one of the | |||
`withX()` methods - these can be chained together to set multiple flags. | |||
*/ | |||
AccessibleState() = default; | |||
//============================================================================== | |||
/** Sets the checkable flag and returns the new state. | |||
@see isCheckable | |||
*/ | |||
AccessibleState withCheckable() const noexcept { return withFlag (Flags::checkable); } | |||
/** Sets the checked flag and returns the new state. | |||
@see isChecked | |||
*/ | |||
AccessibleState withChecked() const noexcept { return withFlag (Flags::checked); } | |||
/** Sets the collapsed flag and returns the new state. | |||
@see isCollapsed | |||
*/ | |||
AccessibleState withCollapsed() const noexcept { return withFlag (Flags::collapsed); } | |||
/** Sets the expandable flag and returns the new state. | |||
@see isExpandable | |||
*/ | |||
AccessibleState withExpandable() const noexcept { return withFlag (Flags::expandable); } | |||
/** Sets the expanded flag and returns the new state. | |||
@see isExpanded | |||
*/ | |||
AccessibleState withExpanded() const noexcept { return withFlag (Flags::expanded); } | |||
/** Sets the focusable flag and returns the new state. | |||
@see isFocusable | |||
*/ | |||
AccessibleState withFocusable() const noexcept { return withFlag (Flags::focusable); } | |||
/** Sets the focused flag and returns the new state. | |||
@see isFocused | |||
*/ | |||
AccessibleState withFocused() const noexcept { return withFlag (Flags::focused); } | |||
/** Sets the ignored flag and returns the new state. | |||
@see isIgnored | |||
*/ | |||
AccessibleState withIgnored() const noexcept { return withFlag (Flags::ignored); } | |||
/** Sets the selectable flag and returns the new state. | |||
@see isSelectable | |||
*/ | |||
AccessibleState withSelectable() const noexcept { return withFlag (Flags::selectable); } | |||
/** Sets the multiSelectable flag and returns the new state. | |||
@see isMultiSelectable | |||
*/ | |||
AccessibleState withMultiSelectable() const noexcept { return withFlag (Flags::multiSelectable); } | |||
/** Sets the selected flag and returns the new state. | |||
@see isSelected | |||
*/ | |||
AccessibleState withSelected() const noexcept { return withFlag (Flags::selected); } | |||
/** Sets the accessible offscreen flag and returns the new state. | |||
@see isSelected | |||
*/ | |||
AccessibleState withAccessibleOffscreen() const noexcept { return withFlag (Flags::accessibleOffscreen); } | |||
//============================================================================== | |||
/** Returns true if the UI element is checkable. | |||
@see withCheckable | |||
*/ | |||
bool isCheckable() const noexcept { return isFlagSet (Flags::checkable); } | |||
/** Returns true if the UI element is checked. | |||
@see withChecked | |||
*/ | |||
bool isChecked() const noexcept { return isFlagSet (Flags::checked); } | |||
/** Returns true if the UI element is collapsed. | |||
@see withCollapsed | |||
*/ | |||
bool isCollapsed() const noexcept { return isFlagSet (Flags::collapsed); } | |||
/** Returns true if the UI element is expandable. | |||
@see withExpandable | |||
*/ | |||
bool isExpandable() const noexcept { return isFlagSet (Flags::expandable); } | |||
/** Returns true if the UI element is expanded. | |||
@see withExpanded | |||
*/ | |||
bool isExpanded() const noexcept { return isFlagSet (Flags::expanded); } | |||
/** Returns true if the UI element is focusable. | |||
@see withFocusable | |||
*/ | |||
bool isFocusable() const noexcept { return isFlagSet (Flags::focusable); } | |||
/** Returns true if the UI element is focused. | |||
@see withFocused | |||
*/ | |||
bool isFocused() const noexcept { return isFlagSet (Flags::focused); } | |||
/** Returns true if the UI element is ignored. | |||
@see withIgnored | |||
*/ | |||
bool isIgnored() const noexcept { return isFlagSet (Flags::ignored); } | |||
/** Returns true if the UI element supports multiple item selection. | |||
@see withMultiSelectable | |||
*/ | |||
bool isMultiSelectable() const noexcept { return isFlagSet (Flags::multiSelectable); } | |||
/** Returns true if the UI element is selectable. | |||
@see withSelectable | |||
*/ | |||
bool isSelectable() const noexcept { return isFlagSet (Flags::selectable); } | |||
/** Returns true if the UI element is selected. | |||
@see withSelected | |||
*/ | |||
bool isSelected() const noexcept { return isFlagSet (Flags::selected); } | |||
/** Returns true if the UI element is accessible offscreen. | |||
@see withSelected | |||
*/ | |||
bool isAccessibleOffscreen() const noexcept { return isFlagSet (Flags::accessibleOffscreen); } | |||
private: | |||
enum Flags | |||
{ | |||
checkable = (1 << 0), | |||
checked = (1 << 1), | |||
collapsed = (1 << 2), | |||
expandable = (1 << 3), | |||
expanded = (1 << 4), | |||
focusable = (1 << 5), | |||
focused = (1 << 6), | |||
ignored = (1 << 7), | |||
multiSelectable = (1 << 8), | |||
selectable = (1 << 9), | |||
selected = (1 << 10), | |||
accessibleOffscreen = (1 << 11) | |||
}; | |||
AccessibleState withFlag (int flag) const noexcept | |||
{ | |||
auto copy = *this; | |||
copy.flags |= flag; | |||
return copy; | |||
} | |||
bool isFlagSet (int flag) const noexcept | |||
{ | |||
return (flags & flag) != 0; | |||
} | |||
int flags = 0; | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,96 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** Basic accessible interface for a Button that can be clicked or toggled. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API ButtonAccessibilityHandler : public AccessibilityHandler | |||
{ | |||
public: | |||
explicit ButtonAccessibilityHandler (Button& buttonToWrap) | |||
: AccessibilityHandler (buttonToWrap, | |||
getButtonRole (buttonToWrap), | |||
getAccessibilityActions (buttonToWrap)), | |||
button (buttonToWrap) | |||
{ | |||
} | |||
AccessibleState getCurrentState() const override | |||
{ | |||
auto state = AccessibilityHandler::getCurrentState(); | |||
if (button.getClickingTogglesState()) | |||
{ | |||
state = state.withCheckable(); | |||
if (button.getToggleState()) | |||
state = state.withChecked(); | |||
} | |||
return state; | |||
} | |||
String getTitle() const override | |||
{ | |||
auto title = AccessibilityHandler::getTitle(); | |||
if (title.isEmpty()) | |||
return button.getButtonText(); | |||
return title; | |||
} | |||
private: | |||
static AccessibilityRole getButtonRole (const Button& b) | |||
{ | |||
if (b.getRadioGroupId() != 0) return AccessibilityRole::radioButton; | |||
if (b.getClickingTogglesState()) return AccessibilityRole::toggleButton; | |||
return AccessibilityRole::button; | |||
} | |||
static AccessibilityActions getAccessibilityActions (Button& button) | |||
{ | |||
auto actions = AccessibilityActions().addAction (AccessibilityActionType::press, | |||
[&button] { button.triggerClick(); }); | |||
if (button.getClickingTogglesState()) | |||
actions = actions.addAction (AccessibilityActionType::toggle, | |||
[&button] { button.setToggleState (! button.getToggleState(), sendNotification); }); | |||
return actions; | |||
} | |||
Button& button; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ButtonAccessibilityHandler) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,66 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** Basic accessible interface for a ComboBox that can show a menu. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API ComboBoxAccessibilityHandler : public AccessibilityHandler | |||
{ | |||
public: | |||
explicit ComboBoxAccessibilityHandler (ComboBox& comboBoxToWrap) | |||
: AccessibilityHandler (comboBoxToWrap, | |||
AccessibilityRole::comboBox, | |||
getAccessibilityActions (comboBoxToWrap)), | |||
comboBox (comboBoxToWrap) | |||
{ | |||
} | |||
AccessibleState getCurrentState() const override | |||
{ | |||
auto state = AccessibilityHandler::getCurrentState().withExpandable(); | |||
return comboBox.isPopupActive() ? state.withExpanded() : state.withCollapsed(); | |||
} | |||
String getTitle() const override { return comboBox.getText(); } | |||
private: | |||
static AccessibilityActions getAccessibilityActions (ComboBox& comboBox) | |||
{ | |||
return AccessibilityActions().addAction (AccessibilityActionType::press, [&comboBox] { comboBox.showPopup(); }) | |||
.addAction (AccessibilityActionType::showMenu, [&comboBox] { comboBox.showPopup(); }); | |||
} | |||
ComboBox& comboBox; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComboBoxAccessibilityHandler) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,62 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** Basic accessible interface for a Label that can also show a TextEditor | |||
when clicked. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API LabelAccessibilityHandler : public AccessibilityHandler | |||
{ | |||
public: | |||
explicit LabelAccessibilityHandler (Label& labelToWrap) | |||
: AccessibilityHandler (labelToWrap, | |||
AccessibilityRole::staticText, | |||
getAccessibilityActions (labelToWrap)), | |||
label (labelToWrap) | |||
{ | |||
} | |||
String getTitle() const override { return label.getText(); } | |||
private: | |||
static AccessibilityActions getAccessibilityActions (Label& label) | |||
{ | |||
if (label.isEditable()) | |||
return AccessibilityActions().addAction (AccessibilityActionType::press, [&label] { label.showEditor(); }); | |||
return {}; | |||
} | |||
Label& label; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LabelAccessibilityHandler) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,100 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** Basic accessible interface for a Slider. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API SliderAccessibilityHandler : public AccessibilityHandler | |||
{ | |||
public: | |||
explicit SliderAccessibilityHandler (Slider& sliderToWrap) | |||
: AccessibilityHandler (sliderToWrap, | |||
AccessibilityRole::slider, | |||
{}, | |||
{ std::make_unique<SliderValueInterface> (sliderToWrap) }) | |||
{ | |||
} | |||
private: | |||
class SliderValueInterface : public AccessibilityValueInterface | |||
{ | |||
public: | |||
explicit SliderValueInterface (Slider& sliderToWrap) | |||
: slider (sliderToWrap) | |||
{ | |||
} | |||
bool isReadOnly() const override { return false; } | |||
double getCurrentValue() const override | |||
{ | |||
return slider.isTwoValue() ? slider.getMaxValue() : slider.getValue(); | |||
} | |||
void setValue (double newValue) override | |||
{ | |||
if (slider.isTwoValue()) | |||
slider.setMaxValue (newValue, sendNotification); | |||
else | |||
slider.setValue (newValue, sendNotification); | |||
} | |||
String getCurrentValueAsString() const override | |||
{ | |||
return slider.getTextFromValue (getCurrentValue()); | |||
} | |||
void setValueAsString (const String& newValue) override | |||
{ | |||
setValue (slider.getValueFromText (newValue)); | |||
} | |||
AccessibleValueRange getRange() const override | |||
{ | |||
return { { slider.getMinimum(), slider.getMaximum() }, | |||
getStepSize() }; | |||
} | |||
private: | |||
double getStepSize() const | |||
{ | |||
auto interval = slider.getInterval(); | |||
return interval != 0.0 ? interval | |||
: slider.proportionOfLengthToValue (0.01); | |||
} | |||
Slider& slider; | |||
}; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SliderAccessibilityHandler) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,83 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** Basic accessible interface for a TableListBox. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API TableListBoxAccessibilityHandler : public AccessibilityHandler | |||
{ | |||
public: | |||
explicit TableListBoxAccessibilityHandler (TableListBox& tableListBoxToWrap) | |||
: AccessibilityHandler (tableListBoxToWrap, | |||
AccessibilityRole::list, | |||
{}, | |||
{ std::make_unique<TableListBoxTableInterface> (tableListBoxToWrap) }) | |||
{ | |||
} | |||
private: | |||
class TableListBoxTableInterface : public AccessibilityTableInterface | |||
{ | |||
public: | |||
explicit TableListBoxTableInterface (TableListBox& tableListBoxToWrap) | |||
: tableListBox (tableListBoxToWrap) | |||
{ | |||
} | |||
int getNumRows() const override | |||
{ | |||
if (auto* model = tableListBox.getModel()) | |||
return model->getNumRows(); | |||
return 0; | |||
} | |||
int getNumColumns() const override | |||
{ | |||
return tableListBox.getHeader().getNumColumns (false); | |||
} | |||
const AccessibilityHandler* getCellHandler (int row, int column) const override | |||
{ | |||
if (isPositiveAndBelow (row, getNumRows()) && isPositiveAndBelow (column, getNumColumns())) | |||
if (auto* cellComponent = tableListBox.getCellComponent (tableListBox.getHeader().getColumnIdOfIndex (column, false), row)) | |||
return cellComponent->getAccessibilityHandler(); | |||
return nullptr; | |||
} | |||
private: | |||
TableListBox& tableListBox; | |||
}; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TableListBoxAccessibilityHandler) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,108 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** Basic accessible interface for a TextEditor. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API TextEditorAccessibilityHandler : public AccessibilityHandler | |||
{ | |||
public: | |||
explicit TextEditorAccessibilityHandler (TextEditor& textEditorToWrap) | |||
: AccessibilityHandler (textEditorToWrap, | |||
textEditorToWrap.isReadOnly() ? AccessibilityRole::staticText : AccessibilityRole::editableText, | |||
{}, | |||
{ textEditorToWrap.isReadOnly() ? nullptr : std::make_unique<TextEditorTextInterface> (textEditorToWrap) }), | |||
textEditor (textEditorToWrap) | |||
{ | |||
} | |||
String getTitle() const override | |||
{ | |||
return textEditor.isReadOnly() ? textEditor.getText() : textEditor.getTitle(); | |||
} | |||
private: | |||
class TextEditorTextInterface : public AccessibilityTextInterface | |||
{ | |||
public: | |||
explicit TextEditorTextInterface (TextEditor& editor) | |||
: textEditor (editor) | |||
{ | |||
} | |||
bool isDisplayingProtectedText() const override { return textEditor.getPasswordCharacter() != 0; } | |||
int getTotalNumCharacters() const override { return textEditor.getText().length(); } | |||
Range<int> getSelection() const override { return textEditor.getHighlightedRegion(); } | |||
void setSelection (Range<int> r) override { textEditor.setHighlightedRegion (r); } | |||
String getText (Range<int> r) const override | |||
{ | |||
if (isDisplayingProtectedText()) | |||
return String::repeatedString (String::charToString (textEditor.getPasswordCharacter()), | |||
getTotalNumCharacters()); | |||
return textEditor.getTextInRange (r); | |||
} | |||
void setText (const String& newText) override | |||
{ | |||
textEditor.setText (newText); | |||
} | |||
int getTextInsertionOffset() const override { return textEditor.getCaretPosition(); } | |||
RectangleList<int> getTextBounds (Range<int> textRange) const override | |||
{ | |||
auto localRects = textEditor.getTextBounds (textRange); | |||
RectangleList<int> globalRects; | |||
std::for_each (localRects.begin(), localRects.end(), | |||
[&] (const Rectangle<int>& r) { globalRects.add (textEditor.localAreaToGlobal (r)); }); | |||
return globalRects; | |||
} | |||
int getOffsetAtPoint (Point<int> point) const override | |||
{ | |||
auto localPoint = textEditor.getLocalPoint (nullptr, point); | |||
return textEditor.getTextIndexAt (localPoint.x, localPoint.y); | |||
} | |||
private: | |||
TextEditor& textEditor; | |||
}; | |||
TextEditor& textEditor; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TextEditorAccessibilityHandler) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,79 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
/** Basic accessible interface for a TreeView. | |||
@tags{Accessibility} | |||
*/ | |||
class JUCE_API TreeViewAccessibilityHandler : public AccessibilityHandler | |||
{ | |||
public: | |||
explicit TreeViewAccessibilityHandler (TreeView& treeViewToWrap) | |||
: AccessibilityHandler (treeViewToWrap, | |||
AccessibilityRole::tree, | |||
{}, | |||
{ std::make_unique<TreeViewTableInterface> (treeViewToWrap) }) | |||
{ | |||
} | |||
private: | |||
class TreeViewTableInterface : public AccessibilityTableInterface | |||
{ | |||
public: | |||
explicit TreeViewTableInterface (TreeView& treeViewToWrap) | |||
: treeView (treeViewToWrap) | |||
{ | |||
} | |||
int getNumRows() const override | |||
{ | |||
return treeView.getNumRowsInTree(); | |||
} | |||
int getNumColumns() const override | |||
{ | |||
return 1; | |||
} | |||
const AccessibilityHandler* getCellHandler (int row, int) const override | |||
{ | |||
if (auto* itemComp = treeView.getItemComponent (treeView.getItemOnRow (row))) | |||
return itemComp->getAccessibilityHandler(); | |||
return nullptr; | |||
} | |||
private: | |||
TreeView& treeView; | |||
}; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TreeViewAccessibilityHandler) | |||
}; | |||
} // namespace juce |
@@ -188,6 +188,9 @@ void Button::setToggleState (bool shouldBeOn, NotificationType clickNotification | |||
sendStateMessage(); | |||
else | |||
buttonStateChanged(); | |||
if (auto* handler = getAccessibilityHandler()) | |||
handler->notifyAccessibilityEvent (AccessibilityEvent::valueChanged); | |||
} | |||
} | |||
@@ -205,6 +208,8 @@ void Button::setClickingTogglesState (bool shouldToggle) noexcept | |||
// it is that this button represents, and the button will update its state to reflect this | |||
// in the applicationCommandListChanged() method. | |||
jassert (commandManagerToUse == nullptr || ! clickTogglesState); | |||
invalidateAccessibilityHandler(); | |||
} | |||
bool Button::getClickingTogglesState() const noexcept | |||
@@ -220,6 +225,8 @@ void Button::setRadioGroupId (int newGroupId, NotificationType notification) | |||
if (lastToggleState) | |||
turnOffOtherButtonsInGroup (notification, notification); | |||
invalidateAccessibilityHandler(); | |||
} | |||
} | |||
@@ -692,4 +699,10 @@ void Button::repeatTimerCallback() | |||
} | |||
} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> Button::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<ButtonAccessibilityHandler> (*this); | |||
} | |||
} // namespace juce |
@@ -470,6 +470,8 @@ protected: | |||
void focusLost (FocusChangeType) override; | |||
/** @internal */ | |||
void enablementChanged() override; | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
private: | |||
//============================================================================== | |||
@@ -482,15 +482,15 @@ Component::~Component() | |||
componentListeners.call ([this] (ComponentListener& l) { l.componentBeingDeleted (*this); }); | |||
masterReference.clear(); | |||
while (childComponentList.size() > 0) | |||
removeChildComponent (childComponentList.size() - 1, false, true); | |||
masterReference.clear(); | |||
if (parentComponent != nullptr) | |||
parentComponent->removeChildComponent (parentComponent->childComponentList.indexOf (this), true, false); | |||
else if (hasKeyboardFocus (true)) | |||
giveAwayFocus (currentlyFocusedComponent != this); | |||
else | |||
giveAwayKeyboardFocusInternal (isParentOf (currentlyFocusedComponent)); | |||
if (flags.hasHeavyweightPeerFlag) | |||
removeFromDesktop(); | |||
@@ -551,8 +551,8 @@ void Component::setVisible (bool shouldBeVisible) | |||
if (parentComponent != nullptr) | |||
parentComponent->grabKeyboardFocus(); | |||
if (hasKeyboardFocus (true)) | |||
giveAwayFocus (true); | |||
// ensure that keyboard focus is given away if it wasn't taken by parent | |||
giveAwayKeyboardFocus(); | |||
} | |||
} | |||
@@ -704,6 +704,9 @@ void Component::addToDesktop (int styleWanted, void* nativeWindowToAttachTo) | |||
repaint(); | |||
internalHierarchyChanged(); | |||
if (auto* handler = getAccessibilityHandler()) | |||
notifyAccessibilityEventInternal (*handler, InternalAccessibilityEvent::windowOpened); | |||
} | |||
} | |||
} | |||
@@ -716,6 +719,9 @@ void Component::removeFromDesktop() | |||
if (flags.hasHeavyweightPeerFlag) | |||
{ | |||
if (auto* handler = getAccessibilityHandler()) | |||
notifyAccessibilityEventInternal (*handler, InternalAccessibilityEvent::windowClosed); | |||
ComponentHelpers::releaseAllCachedImageResources (*this); | |||
auto* peer = ComponentPeer::getPeerFor (this); | |||
@@ -886,7 +892,7 @@ void Component::reorderChildInternal (int sourceIndex, int destIndex) | |||
} | |||
} | |||
void Component::toFront (bool setAsForeground) | |||
void Component::toFront (bool shouldGrabKeyboardFocus) | |||
{ | |||
// if component methods are being called from threads other than the message | |||
// thread, you'll need to use a MessageManagerLock object to make sure it's thread-safe. | |||
@@ -896,9 +902,9 @@ void Component::toFront (bool setAsForeground) | |||
{ | |||
if (auto* peer = getPeer()) | |||
{ | |||
peer->toFront (setAsForeground); | |||
peer->toFront (shouldGrabKeyboardFocus); | |||
if (setAsForeground && ! hasKeyboardFocus (true)) | |||
if (shouldGrabKeyboardFocus && ! hasKeyboardFocus (true)) | |||
grabKeyboardFocus(); | |||
} | |||
} | |||
@@ -926,7 +932,7 @@ void Component::toFront (bool setAsForeground) | |||
} | |||
} | |||
if (setAsForeground) | |||
if (shouldGrabKeyboardFocus) | |||
{ | |||
internalBroughtToFront(); | |||
@@ -1498,9 +1504,7 @@ Component* Component::removeChildComponent (int index, bool sendParentEvents, bo | |||
// thread, you'll need to use a MessageManagerLock object to make sure it's thread-safe. | |||
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED_OR_OFFSCREEN | |||
auto* child = childComponentList [index]; | |||
if (child != nullptr) | |||
if (auto* child = childComponentList [index]) | |||
{ | |||
sendParentEvents = sendParentEvents && child->isShowing(); | |||
@@ -1518,23 +1522,19 @@ Component* Component::removeChildComponent (int index, bool sendParentEvents, bo | |||
ComponentHelpers::releaseAllCachedImageResources (*child); | |||
// (NB: there are obscure situations where child->isShowing() = false, but it still has the focus) | |||
if (currentlyFocusedComponent == child || child->isParentOf (currentlyFocusedComponent)) | |||
if (child->hasKeyboardFocus (true)) | |||
{ | |||
if (sendParentEvents) | |||
{ | |||
const WeakReference<Component> thisPointer (this); | |||
const WeakReference<Component> safeThis (this); | |||
giveAwayFocus (sendChildEvents || currentlyFocusedComponent != child); | |||
child->giveAwayKeyboardFocusInternal (sendChildEvents || currentlyFocusedComponent != child); | |||
if (thisPointer == nullptr) | |||
if (sendParentEvents) | |||
{ | |||
if (safeThis == nullptr) | |||
return child; | |||
grabKeyboardFocus(); | |||
} | |||
else | |||
{ | |||
giveAwayFocus (sendChildEvents || currentlyFocusedComponent != child); | |||
} | |||
} | |||
if (sendChildEvents) | |||
@@ -1542,9 +1542,11 @@ Component* Component::removeChildComponent (int index, bool sendParentEvents, bo | |||
if (sendParentEvents) | |||
internalChildrenChanged(); | |||
return child; | |||
} | |||
return child; | |||
return nullptr; | |||
} | |||
//============================================================================== | |||
@@ -1656,6 +1658,10 @@ void Component::internalHierarchyChanged() | |||
i = jmin (i, childComponentList.size()); | |||
} | |||
if (flags.hasHeavyweightPeerFlag) | |||
if (auto* handler = getAccessibilityHandler()) | |||
handler->notifyAccessibilityEvent (AccessibilityEvent::structureChanged); | |||
} | |||
//============================================================================== | |||
@@ -2416,7 +2422,7 @@ void Component::internalMouseDown (MouseInputSource source, Point<float> relativ | |||
if (! flags.dontFocusOnMouseClickFlag) | |||
{ | |||
grabFocusInternal (focusChangedByMouseClick, true); | |||
grabKeyboardFocusInternal (focusChangedByMouseClick, true); | |||
if (checker.shouldBailOut()) | |||
return; | |||
@@ -2644,36 +2650,48 @@ void Component::focusGained (FocusChangeType) {} | |||
void Component::focusLost (FocusChangeType) {} | |||
void Component::focusOfChildComponentChanged (FocusChangeType) {} | |||
void Component::internalFocusGain (FocusChangeType cause) | |||
void Component::internalKeyboardFocusGain (FocusChangeType cause) | |||
{ | |||
internalFocusGain (cause, WeakReference<Component> (this)); | |||
internalKeyboardFocusGain (cause, WeakReference<Component> (this)); | |||
} | |||
void Component::internalFocusGain (FocusChangeType cause, const WeakReference<Component>& safePointer) | |||
void Component::internalKeyboardFocusGain (FocusChangeType cause, | |||
const WeakReference<Component>& safePointer) | |||
{ | |||
focusGained (cause); | |||
if (safePointer != nullptr) | |||
internalChildFocusChange (cause, safePointer); | |||
{ | |||
if (auto* handler = getAccessibilityHandler()) | |||
handler->grabFocus(); | |||
internalChildKeyboardFocusChange (cause, safePointer); | |||
} | |||
} | |||
void Component::internalFocusLoss (FocusChangeType cause) | |||
void Component::internalKeyboardFocusLoss (FocusChangeType cause) | |||
{ | |||
const WeakReference<Component> safePointer (this); | |||
focusLost (cause); | |||
if (safePointer != nullptr) | |||
internalChildFocusChange (cause, safePointer); | |||
{ | |||
if (auto* handler = getAccessibilityHandler()) | |||
handler->giveAwayFocus(); | |||
internalChildKeyboardFocusChange (cause, safePointer); | |||
} | |||
} | |||
void Component::internalChildFocusChange (FocusChangeType cause, const WeakReference<Component>& safePointer) | |||
void Component::internalChildKeyboardFocusChange (FocusChangeType cause, | |||
const WeakReference<Component>& safePointer) | |||
{ | |||
const bool childIsNowFocused = hasKeyboardFocus (true); | |||
const bool childIsNowKeyboardFocused = hasKeyboardFocus (true); | |||
if (flags.childCompFocusedFlag != childIsNowFocused) | |||
if (flags.childKeyboardFocusedFlag != childIsNowKeyboardFocused) | |||
{ | |||
flags.childCompFocusedFlag = childIsNowFocused; | |||
flags.childKeyboardFocusedFlag = childIsNowKeyboardFocused; | |||
focusOfChildComponentChanged (cause); | |||
@@ -2682,12 +2700,12 @@ void Component::internalChildFocusChange (FocusChangeType cause, const WeakRefer | |||
} | |||
if (parentComponent != nullptr) | |||
parentComponent->internalChildFocusChange (cause, WeakReference<Component> (parentComponent)); | |||
parentComponent->internalChildKeyboardFocusChange (cause, parentComponent); | |||
} | |||
void Component::setWantsKeyboardFocus (bool wantsFocus) noexcept | |||
{ | |||
flags.wantsFocusFlag = wantsFocus; | |||
flags.wantsKeyboardFocusFlag = wantsFocus; | |||
} | |||
void Component::setMouseClickGrabsKeyboardFocus (bool shouldGrabFocus) | |||
@@ -2702,12 +2720,15 @@ bool Component::getMouseClickGrabsKeyboardFocus() const noexcept | |||
bool Component::getWantsKeyboardFocus() const noexcept | |||
{ | |||
return flags.wantsFocusFlag && ! flags.isDisabledFlag; | |||
return flags.wantsKeyboardFocusFlag && ! flags.isDisabledFlag; | |||
} | |||
void Component::setFocusContainer (bool shouldBeFocusContainer) noexcept | |||
void Component::setFocusContainerType (FocusContainerType containerType) noexcept | |||
{ | |||
flags.isFocusContainerFlag = shouldBeFocusContainer; | |||
flags.isFocusContainerFlag = (containerType == FocusContainerType::focusContainer | |||
|| containerType == FocusContainerType::keyboardFocusContainer); | |||
flags.isKeyboardFocusContainerFlag = (containerType == FocusContainerType::keyboardFocusContainer); | |||
} | |||
bool Component::isFocusContainer() const noexcept | |||
@@ -2715,6 +2736,35 @@ bool Component::isFocusContainer() const noexcept | |||
return flags.isFocusContainerFlag; | |||
} | |||
bool Component::isKeyboardFocusContainer() const noexcept | |||
{ | |||
return flags.isKeyboardFocusContainerFlag; | |||
} | |||
template <typename FocusContainerFn> | |||
static Component* findContainer (const Component* child, FocusContainerFn isFocusContainer) | |||
{ | |||
if (auto* parent = child->getParentComponent()) | |||
{ | |||
if ((parent->*isFocusContainer)() || parent->getParentComponent() == nullptr) | |||
return parent; | |||
return findContainer (parent, isFocusContainer); | |||
} | |||
return nullptr; | |||
} | |||
Component* Component::findFocusContainer() const | |||
{ | |||
return findContainer (this, &Component::isFocusContainer); | |||
} | |||
Component* Component::findKeyboardFocusContainer() const | |||
{ | |||
return findContainer (this, &Component::isKeyboardFocusContainer); | |||
} | |||
static const Identifier juce_explicitFocusOrderId ("_jexfo"); | |||
int Component::getExplicitFocusOrder() const | |||
@@ -2727,85 +2777,78 @@ void Component::setExplicitFocusOrder (int newFocusOrderIndex) | |||
properties.set (juce_explicitFocusOrderId, newFocusOrderIndex); | |||
} | |||
KeyboardFocusTraverser* Component::createFocusTraverser() | |||
std::unique_ptr<ComponentTraverser> Component::createFocusTraverser() | |||
{ | |||
if (flags.isFocusContainerFlag || parentComponent == nullptr) | |||
return new KeyboardFocusTraverser(); | |||
return std::make_unique<FocusTraverser>(); | |||
return parentComponent->createFocusTraverser(); | |||
} | |||
std::unique_ptr<ComponentTraverser> Component::createKeyboardFocusTraverser() | |||
{ | |||
if (flags.isKeyboardFocusContainerFlag || parentComponent == nullptr) | |||
return std::make_unique<KeyboardFocusTraverser>(); | |||
return parentComponent->createKeyboardFocusTraverser(); | |||
} | |||
void Component::takeKeyboardFocus (FocusChangeType cause) | |||
{ | |||
// give the focus to this component | |||
if (currentlyFocusedComponent != this) | |||
if (currentlyFocusedComponent == this) | |||
return; | |||
if (auto* peer = getPeer()) | |||
{ | |||
// get the focus onto our desktop window | |||
if (auto* peer = getPeer()) | |||
{ | |||
const WeakReference<Component> safePointer (this); | |||
peer->grabFocus(); | |||
const WeakReference<Component> safePointer (this); | |||
peer->grabFocus(); | |||
if (peer->isFocused() && currentlyFocusedComponent != this) | |||
{ | |||
WeakReference<Component> componentLosingFocus (currentlyFocusedComponent); | |||
currentlyFocusedComponent = this; | |||
if (! peer->isFocused() || currentlyFocusedComponent == this) | |||
return; | |||
Desktop::getInstance().triggerFocusCallback(); | |||
WeakReference<Component> componentLosingFocus (currentlyFocusedComponent); | |||
currentlyFocusedComponent = this; | |||
// call this after setting currentlyFocusedComponent so that the one that's | |||
// losing it has a chance to see where focus is going | |||
if (componentLosingFocus != nullptr) | |||
componentLosingFocus->internalFocusLoss (cause); | |||
Desktop::getInstance().triggerFocusCallback(); | |||
if (currentlyFocusedComponent == this) | |||
internalFocusGain (cause, safePointer); | |||
} | |||
} | |||
// call this after setting currentlyFocusedComponent so that the one that's | |||
// losing it has a chance to see where focus is going | |||
if (componentLosingFocus != nullptr) | |||
componentLosingFocus->internalKeyboardFocusLoss (cause); | |||
if (currentlyFocusedComponent == this) | |||
internalKeyboardFocusGain (cause, safePointer); | |||
} | |||
} | |||
void Component::grabFocusInternal (FocusChangeType cause, bool canTryParent) | |||
void Component::grabKeyboardFocusInternal (FocusChangeType cause, bool canTryParent) | |||
{ | |||
if (isShowing()) | |||
{ | |||
if (flags.wantsFocusFlag && (isEnabled() || parentComponent == nullptr)) | |||
{ | |||
takeKeyboardFocus (cause); | |||
} | |||
else | |||
{ | |||
if (isParentOf (currentlyFocusedComponent) | |||
&& currentlyFocusedComponent->isShowing()) | |||
{ | |||
// do nothing if the focused component is actually a child of ours.. | |||
} | |||
else | |||
{ | |||
// find the default child component.. | |||
std::unique_ptr<KeyboardFocusTraverser> traverser (createFocusTraverser()); | |||
if (! isShowing()) | |||
return; | |||
if (traverser != nullptr) | |||
{ | |||
auto* defaultComp = traverser->getDefaultComponent (this); | |||
traverser.reset(); | |||
if (flags.wantsKeyboardFocusFlag | |||
&& (isEnabled() || parentComponent == nullptr)) | |||
{ | |||
takeKeyboardFocus (cause); | |||
return; | |||
} | |||
if (defaultComp != nullptr) | |||
{ | |||
defaultComp->grabFocusInternal (cause, false); | |||
return; | |||
} | |||
} | |||
if (isParentOf (currentlyFocusedComponent) && currentlyFocusedComponent->isShowing()) | |||
return; | |||
if (canTryParent && parentComponent != nullptr) | |||
{ | |||
// if no children want it and we're allowed to try our parent comp, | |||
// then pass up to parent, which will try our siblings. | |||
parentComponent->grabFocusInternal (cause, true); | |||
} | |||
} | |||
if (auto traverser = createKeyboardFocusTraverser()) | |||
{ | |||
if (auto* defaultComp = traverser->getDefaultComponent (this)) | |||
{ | |||
defaultComp->grabKeyboardFocusInternal (cause, false); | |||
return; | |||
} | |||
} | |||
// if no children want it and we're allowed to try our parent comp, | |||
// then pass up to parent, which will try our siblings. | |||
if (canTryParent && parentComponent != nullptr) | |||
parentComponent->grabKeyboardFocusInternal (cause, true); | |||
} | |||
void Component::grabKeyboardFocus() | |||
@@ -2814,7 +2857,7 @@ void Component::grabKeyboardFocus() | |||
// thread, you'll need to use a MessageManagerLock object to make sure it's thread-safe. | |||
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | |||
grabFocusInternal (focusChangedDirectly, true); | |||
grabKeyboardFocusInternal (focusChangedDirectly, true); | |||
// A component can only be focused when it's actually on the screen! | |||
// If this fails then you're probably trying to grab the focus before you've | |||
@@ -2823,6 +2866,31 @@ void Component::grabKeyboardFocus() | |||
jassert (isShowing() || isOnDesktop()); | |||
} | |||
void Component::giveAwayKeyboardFocusInternal (bool sendFocusLossEvent) | |||
{ | |||
if (hasKeyboardFocus (true)) | |||
{ | |||
if (auto* componentLosingFocus = currentlyFocusedComponent) | |||
{ | |||
currentlyFocusedComponent = nullptr; | |||
if (sendFocusLossEvent && componentLosingFocus != nullptr) | |||
componentLosingFocus->internalKeyboardFocusLoss (focusChangedDirectly); | |||
Desktop::getInstance().triggerFocusCallback(); | |||
} | |||
} | |||
} | |||
void Component::giveAwayKeyboardFocus() | |||
{ | |||
// if component methods are being called from threads other than the message | |||
// thread, you'll need to use a MessageManagerLock object to make sure it's thread-safe. | |||
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED | |||
giveAwayKeyboardFocusInternal (true); | |||
} | |||
void Component::moveKeyboardFocusToSibling (bool moveToNext) | |||
{ | |||
// if component methods are being called from threads other than the message | |||
@@ -2831,15 +2899,27 @@ void Component::moveKeyboardFocusToSibling (bool moveToNext) | |||
if (parentComponent != nullptr) | |||
{ | |||
std::unique_ptr<KeyboardFocusTraverser> traverser (createFocusTraverser()); | |||
if (traverser != nullptr) | |||
if (auto traverser = createKeyboardFocusTraverser()) | |||
{ | |||
auto* nextComp = moveToNext ? traverser->getNextComponent (this) | |||
: traverser->getPreviousComponent (this); | |||
traverser.reset(); | |||
auto findComponentToFocus = [&]() -> Component* | |||
{ | |||
if (auto* comp = (moveToNext ? traverser->getNextComponent (this) | |||
: traverser->getPreviousComponent (this))) | |||
return comp; | |||
if (auto* focusContainer = findKeyboardFocusContainer()) | |||
{ | |||
auto allFocusableComponents = traverser->getAllComponents (focusContainer); | |||
if (! allFocusableComponents.empty()) | |||
return moveToNext ? allFocusableComponents.front() | |||
: allFocusableComponents.back(); | |||
} | |||
return nullptr; | |||
}; | |||
if (nextComp != nullptr) | |||
if (auto* nextComp = findComponentToFocus()) | |||
{ | |||
if (nextComp->isCurrentlyBlockedByAnotherModalComponent()) | |||
{ | |||
@@ -2850,7 +2930,7 @@ void Component::moveKeyboardFocusToSibling (bool moveToNext) | |||
return; | |||
} | |||
nextComp->grabFocusInternal (focusChangedByTabKey, true); | |||
nextComp->grabKeyboardFocusInternal (focusChangedByTabKey, true); | |||
return; | |||
} | |||
} | |||
@@ -2872,19 +2952,8 @@ Component* JUCE_CALLTYPE Component::getCurrentlyFocusedComponent() noexcept | |||
void JUCE_CALLTYPE Component::unfocusAllComponents() | |||
{ | |||
if (auto* c = getCurrentlyFocusedComponent()) | |||
c->giveAwayFocus (true); | |||
} | |||
void Component::giveAwayFocus (bool sendFocusLossEvent) | |||
{ | |||
auto* componentLosingFocus = currentlyFocusedComponent; | |||
currentlyFocusedComponent = nullptr; | |||
if (sendFocusLossEvent && componentLosingFocus != nullptr) | |||
componentLosingFocus->internalFocusLoss (focusChangedDirectly); | |||
Desktop::getInstance().triggerFocusCallback(); | |||
if (currentlyFocusedComponent != nullptr) | |||
currentlyFocusedComponent->giveAwayKeyboardFocus(); | |||
} | |||
//============================================================================== | |||
@@ -3029,4 +3098,52 @@ bool Component::BailOutChecker::shouldBailOut() const noexcept | |||
return safePointer == nullptr; | |||
} | |||
//============================================================================== | |||
void Component::setTitle (const String& newTitle) | |||
{ | |||
componentTitle = newTitle; | |||
} | |||
void Component::setDescription (const String& newDescription) | |||
{ | |||
componentDescription = newDescription; | |||
} | |||
void Component::setHelpText (const String& newHelpText) | |||
{ | |||
componentHelpText = newHelpText; | |||
} | |||
void Component::setAccessible (bool shouldBeAccessible) | |||
{ | |||
flags.accessibilityIgnoredFlag = ! shouldBeAccessible; | |||
if (flags.accessibilityIgnoredFlag) | |||
invalidateAccessibilityHandler(); | |||
} | |||
std::unique_ptr<AccessibilityHandler> Component::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::unspecified); | |||
} | |||
void Component::invalidateAccessibilityHandler() | |||
{ | |||
accessibilityHandler = nullptr; | |||
} | |||
AccessibilityHandler* Component::getAccessibilityHandler() | |||
{ | |||
if (flags.accessibilityIgnoredFlag) | |||
return nullptr; | |||
if (accessibilityHandler == nullptr | |||
|| accessibilityHandler->getTypeIndex() != std::type_index (typeid (*this))) | |||
{ | |||
accessibilityHandler = createAccessibilityHandler(); | |||
} | |||
return accessibilityHandler.get(); | |||
} | |||
} // namespace juce |
@@ -73,7 +73,7 @@ public: | |||
/** Returns the name of this component. | |||
@see setName | |||
*/ | |||
const String& getName() const noexcept { return componentName; } | |||
String getName() const noexcept { return componentName; } | |||
/** Sets the name of this component. | |||
@@ -87,7 +87,7 @@ public: | |||
/** Returns the ID string that was set by setComponentID(). | |||
@see setComponentID, findChildWithID | |||
*/ | |||
const String& getComponentID() const noexcept { return componentID; } | |||
String getComponentID() const noexcept { return componentID; } | |||
/** Sets the component's ID string. | |||
You can retrieve the ID using getComponentID(). | |||
@@ -217,11 +217,12 @@ public: | |||
then they will still be kept in front of this one (unless of course this | |||
one is also 'always-on-top'). | |||
@param shouldAlsoGainFocus if true, this will also try to assign keyboard focus | |||
to the component (see grabKeyboardFocus() for more details) | |||
@param shouldAlsoGainKeyboardFocus if true, this will also try to assign | |||
keyboard focus to the component (see | |||
grabKeyboardFocus() for more details) | |||
@see toBack, toBehind, setAlwaysOnTop | |||
*/ | |||
void toFront (bool shouldAlsoGainFocus); | |||
void toFront (bool shouldAlsoGainKeyboardFocus); | |||
/** Changes this component's z-order to be at the back of all its siblings. | |||
@@ -1203,55 +1204,156 @@ public: | |||
bool isBroughtToFrontOnMouseClick() const noexcept; | |||
//============================================================================== | |||
// Keyboard focus methods | |||
// Focus methods | |||
/** Sets a flag to indicate whether this component needs keyboard focus or not. | |||
/** Sets the focus order of this component. | |||
By default components aren't actually interested in gaining the | |||
The focus order is used by the default traverser implementation returned by | |||
createFocusTraverser() as part of its algorithm for deciding the order in | |||
which components should be traversed. A value of 0 or less is taken to mean | |||
that no explicit order is wanted, and that traversal should use other | |||
factors, like the component's position. | |||
@see getExplicitFocusOrder, FocusTraverser, createFocusTraverser | |||
*/ | |||
void setExplicitFocusOrder (int newFocusOrderIndex); | |||
/** Returns the focus order of this component, if one has been specified. | |||
By default components don't have a focus order - in that case, this will | |||
return 0. | |||
@see setExplicitFocusOrder | |||
*/ | |||
int getExplicitFocusOrder() const; | |||
/** A focus container type that can be passed to setFocusContainer(). | |||
If a component is marked as a focus container or keyboard focus container then | |||
it will act as the top-level component within which focus or keyboard focus is | |||
passed around. By default components are considered "focusable" if they are visible | |||
and enabled and "keyboard focusable" if `getWantsKeyboardFocus() == true`. | |||
The order of traversal within a focus container is determined by the objects | |||
returned by createFocusTraverser() and createKeyboardFocusTraverser(), | |||
respectively - see the documentation of the default FocusContainer and | |||
KeyboardFocusContainer implementations for more information. | |||
*/ | |||
enum class FocusContainerType | |||
{ | |||
/** The component will not act as a focus container. | |||
This is the default setting for non top-level components and means that it and any | |||
sub-components are navigable within their containing focus container. | |||
*/ | |||
none, | |||
/** The component will act as a top-level component within which focus is passed around. | |||
The default traverser implementation returned by createFocusTraverser() will use this | |||
flag to find the first parent component (of the currently focused one) that wants to | |||
be a focus container. | |||
This is currently used when determining the hierarchy of accessible UI elements presented | |||
to screen reader clients on supported platforms. See the AccessibilityHandler class for | |||
more information. | |||
*/ | |||
focusContainer, | |||
/** The component will act as a top-level component within which keyboard focus is passed around. | |||
The default traverser implementation returned by createKeyboardFocusTraverser() will | |||
use this flag to find the first parent component (of the currently focused one) that | |||
wants to be a keyboard focus container. | |||
This is currently used when determining how keyboard focus is passed between components | |||
that have been marked as keyboard focusable with setWantsKeyboardFocus() when clicking | |||
on components and navigating with the tab key. | |||
*/ | |||
keyboardFocusContainer | |||
}; | |||
/** Sets whether this component is a container for components that can have | |||
their focus traversed, and the type of focus traversal that it supports. | |||
@see FocusContainerType, isFocusContainer, isKeyboardFocusContainer, | |||
FocusTraverser, createFocusTraverser, | |||
KeyboardFocusTraverser, createKeyboardFocusTraverser | |||
*/ | |||
void setFocusContainerType (FocusContainerType containerType) noexcept; | |||
/** Returns true if this component has been marked as a focus container. | |||
@see setFocusContainer | |||
*/ | |||
bool isFocusContainer() const noexcept; | |||
/** Returns true if this component has been marked as a keyboard focus container. | |||
@see setFocusContainer | |||
*/ | |||
bool isKeyboardFocusContainer() const noexcept; | |||
/** Returns the focus container for this component. | |||
@see isFocusContainer, setFocusContainer | |||
*/ | |||
Component* findFocusContainer() const; | |||
/** Returns the keyboard focus container for this component. | |||
@see isFocusContainer, setFocusContainer | |||
*/ | |||
Component* findKeyboardFocusContainer() const; | |||
//============================================================================== | |||
/** Sets a flag to indicate whether this component wants keyboard focus or not. | |||
By default components aren't actually interested in gaining the keyboard | |||
focus, but this method can be used to turn this on. | |||
See the grabKeyboardFocus() method for details about the way a component | |||
is chosen to receive the focus. | |||
@see grabKeyboardFocus, getWantsKeyboardFocus | |||
@see grabKeyboardFocus, giveAwayKeyboardFocus, getWantsKeyboardFocus | |||
*/ | |||
void setWantsKeyboardFocus (bool wantsFocus) noexcept; | |||
/** Returns true if the component is interested in getting keyboard focus. | |||
This returns the flag set by setWantsKeyboardFocus(). The default | |||
setting is false. | |||
This returns the flag set by setWantsKeyboardFocus(). The default setting | |||
is false. | |||
@see setWantsKeyboardFocus | |||
*/ | |||
bool getWantsKeyboardFocus() const noexcept; | |||
//============================================================================== | |||
/** Chooses whether a click on this component automatically grabs the focus. | |||
By default this is set to true, but you might want a component which can | |||
be focused, but where you don't want the user to be able to affect it directly | |||
by clicking. | |||
be focused, but where you don't want the user to be able to affect it | |||
directly by clicking. | |||
*/ | |||
void setMouseClickGrabsKeyboardFocus (bool shouldGrabFocus); | |||
/** Returns the last value set with setMouseClickGrabsKeyboardFocus(). | |||
See setMouseClickGrabsKeyboardFocus() for more info. | |||
@see setMouseClickGrabsKeyboardFocus | |||
*/ | |||
bool getMouseClickGrabsKeyboardFocus() const noexcept; | |||
//============================================================================== | |||
/** Tries to give keyboard focus to this component. | |||
When the user clicks on a component or its grabKeyboardFocus() | |||
method is called, the following procedure is used to work out which | |||
component should get it: | |||
When the user clicks on a component or its grabKeyboardFocus() method is | |||
called, the following procedure is used to work out which component should | |||
get it: | |||
- if the component that was clicked on actually wants focus (as indicated | |||
by calling getWantsKeyboardFocus), it gets it. | |||
- if the component itself doesn't want focus, it will try to pass it | |||
on to whichever of its children is the default component, as determined by | |||
KeyboardFocusTraverser::getDefaultComponent() | |||
the getDefaultComponent() implemetation of the ComponentTraverser returned | |||
by createKeyboardFocusTraverser(). | |||
- if none of its children want focus at all, it will pass it up to its | |||
parent instead, unless it's a top-level component without a parent, | |||
in which case it just takes the focus itself. | |||
@@ -1261,12 +1363,21 @@ public: | |||
visible. So there's no point trying to call this in the component's own | |||
constructor or before all of its parent hierarchy has been fully instantiated. | |||
@see setWantsKeyboardFocus, getWantsKeyboardFocus, hasKeyboardFocus, | |||
getCurrentlyFocusedComponent, focusGained, focusLost, | |||
@see giveAwayKeyboardFocus, setWantsKeyboardFocus, getWantsKeyboardFocus, | |||
hasKeyboardFocus, getCurrentlyFocusedComponent, focusGained, focusLost, | |||
keyPressed, keyStateChanged | |||
*/ | |||
void grabKeyboardFocus(); | |||
/** If this component or any of its children currently have the keyboard focus, | |||
this will defocus it, send a focus change notification, and try to pass the | |||
focus to the next component. | |||
@see grabKeyboardFocus, setWantsKeyboardFocus, getCurrentlyFocusedComponent, | |||
focusGained, focusLost | |||
*/ | |||
void giveAwayKeyboardFocus(); | |||
/** Returns true if this component currently has the keyboard focus. | |||
@param trueIfChildIsFocused if this is true, then the method returns true if | |||
@@ -1274,97 +1385,61 @@ public: | |||
have the focus. If false, the method only returns true if | |||
this component has the focus. | |||
@see grabKeyboardFocus, setWantsKeyboardFocus, getCurrentlyFocusedComponent, | |||
focusGained, focusLost | |||
@see grabKeyboardFocus, giveAwayKeyboardFocus, setWantsKeyboardFocus, | |||
getCurrentlyFocusedComponent, focusGained, focusLost | |||
*/ | |||
bool hasKeyboardFocus (bool trueIfChildIsFocused) const; | |||
/** Returns the component that currently has the keyboard focus. | |||
@returns the focused component, or null if nothing is focused. | |||
*/ | |||
static Component* JUCE_CALLTYPE getCurrentlyFocusedComponent() noexcept; | |||
/** If any component has keyboard focus, this will defocus it. */ | |||
static void JUCE_CALLTYPE unfocusAllComponents(); | |||
//============================================================================== | |||
/** Tries to move the keyboard focus to one of this component's siblings. | |||
This will try to move focus to either the next or previous component. (This | |||
is the method that is used when shifting focus by pressing the tab key). | |||
This will try to move focus to either the next or previous component, as | |||
determined by the getNextComponent() and getPreviousComponent() implemetations | |||
of the ComponentTraverser returned by createKeyboardFocusTraverser(). | |||
Components for which getWantsKeyboardFocus() returns false are not looked at. | |||
This is the method that is used when shifting focus by pressing the tab key. | |||
@param moveToNext if true, the focus will move forwards; if false, it will | |||
move backwards | |||
@see grabKeyboardFocus, setFocusContainer, setWantsKeyboardFocus | |||
@see grabKeyboardFocus, giveAwayKeyboardFocus, setFocusContainer, setWantsKeyboardFocus | |||
*/ | |||
void moveKeyboardFocusToSibling (bool moveToNext); | |||
/** Creates a KeyboardFocusTraverser object to use to determine the logic by | |||
which focus should be passed from this component. | |||
The default implementation of this method will return a default | |||
KeyboardFocusTraverser if this component is a focus container (as determined | |||
by the setFocusContainer() method). If the component isn't a focus | |||
container, then it will recursively ask its parents for a KeyboardFocusTraverser. | |||
If you override this to return a custom KeyboardFocusTraverser, then | |||
this component and all its sub-components will use the new object to | |||
make their focusing decisions. | |||
The method should return a new object, which the caller is required to | |||
delete when no longer needed. | |||
*/ | |||
virtual KeyboardFocusTraverser* createFocusTraverser(); | |||
/** Returns the focus order of this component, if one has been specified. | |||
By default components don't have a focus order - in that case, this | |||
will return 0. Lower numbers indicate that the component will be | |||
earlier in the focus traversal order. | |||
To change the order, call setExplicitFocusOrder(). | |||
The focus order may be used by the KeyboardFocusTraverser class as part of | |||
its algorithm for deciding the order in which components should be traversed. | |||
See the KeyboardFocusTraverser class for more details on this. | |||
@see moveKeyboardFocusToSibling, createFocusTraverser, KeyboardFocusTraverser | |||
*/ | |||
int getExplicitFocusOrder() const; | |||
/** Sets the index used in determining the order in which focusable components | |||
should be traversed. | |||
A value of 0 or less is taken to mean that no explicit order is wanted, and | |||
that traversal should use other factors, like the component's position. | |||
/** Returns the component that currently has the keyboard focus. | |||
@see getExplicitFocusOrder, moveKeyboardFocusToSibling | |||
@returns the focused component, or nullptr if nothing is focused. | |||
*/ | |||
void setExplicitFocusOrder (int newFocusOrderIndex); | |||
static Component* JUCE_CALLTYPE getCurrentlyFocusedComponent() noexcept; | |||
/** Indicates whether this component is a parent for components that can have | |||
their focus traversed. | |||
/** If any component has keyboard focus, this will defocus it. */ | |||
static void JUCE_CALLTYPE unfocusAllComponents(); | |||
This flag is used by the default implementation of the createFocusTraverser() | |||
method, which uses the flag to find the first parent component (of the currently | |||
focused one) which wants to be a focus container. | |||
//============================================================================== | |||
/** Creates a ComponentTraverser object to determine the logic by which focus should be | |||
passed from this component. | |||
So using this method to set the flag to 'true' causes this component to | |||
act as the top level within which focus is passed around. | |||
The default implementation of this method will return an instance of FocusTraverser | |||
if this component is a focus container (as determined by the setFocusContainer() method). | |||
If the component isn't a focus container, then it will recursively call | |||
createFocusTraverser() on its parents. | |||
@see isFocusContainer, createFocusTraverser, moveKeyboardFocusToSibling | |||
If you override this to return a custom traverser object, then this component and | |||
all its sub-components will use the new object to make their focusing decisions. | |||
*/ | |||
void setFocusContainer (bool shouldBeFocusContainer) noexcept; | |||
virtual std::unique_ptr<ComponentTraverser> createFocusTraverser(); | |||
/** Returns true if this component has been marked as a focus container. | |||
/** Creates a ComponentTraverser object to use to determine the logic by which keyboard | |||
focus should be passed from this component. | |||
See setFocusContainer() for more details. | |||
The default implementation of this method will return an instance of | |||
KeyboardFocusTraverser if this component is a keyboard focus container (as determined by | |||
the setFocusContainer() method). If the component isn't a keyboard focus container, then | |||
it will recursively call createKeyboardFocusTraverser() on its parents. | |||
@see setFocusContainer, moveKeyboardFocusToSibling, createFocusTraverser | |||
If you override this to return a custom traverser object, then this component and | |||
all its sub-components will use the new object to make their keyboard focusing | |||
decisions. | |||
*/ | |||
bool isFocusContainer() const noexcept; | |||
virtual std::unique_ptr<ComponentTraverser> createKeyboardFocusTraverser(); | |||
//============================================================================== | |||
/** Returns true if the component (and all its parents) are enabled. | |||
@@ -2284,7 +2359,109 @@ public: | |||
*/ | |||
bool getViewportIgnoreDragFlag() const noexcept { return flags.viewportIgnoreDragFlag; } | |||
//============================================================================== | |||
/** Returns the title text for this component. | |||
@see setTitle | |||
*/ | |||
String getTitle() const noexcept { return componentTitle; } | |||
/** Sets the title for this component. | |||
If this component supports accessibility using the default AccessibilityHandler | |||
implementation, this string will be passed to accessibility clients requesting a | |||
title and may be read out by a screen reader. | |||
@see getTitle, getAccessibilityHandler | |||
*/ | |||
void setTitle (const String& newTitle); | |||
/** Returns the description for this component. | |||
@see setDescription | |||
*/ | |||
String getDescription() const noexcept { return componentDescription; } | |||
/** Sets the description for this component. | |||
If this component supports accessibility using the default AccessibilityHandler | |||
implementation, this string will be passed to accessibility clients requesting a | |||
description and may be read out by a screen reader. | |||
@see getDescription, getAccessibilityHandler | |||
*/ | |||
void setDescription (const String& newDescription); | |||
/** Returns the help text for this component. | |||
@see setHelpText | |||
*/ | |||
String getHelpText() const noexcept { return componentHelpText; } | |||
/** Sets the help text for this component. | |||
If this component supports accessibility using the default AccessibilityHandler | |||
implementation, this string will be passed to accessibility clients requesting help text | |||
and may be read out by a screen reader. | |||
@see getHelpText, getAccessibilityHandler | |||
*/ | |||
void setHelpText (const String& newHelpText); | |||
/** Sets whether this component is visible to accessibility clients. | |||
If this flag is set to false then the getAccessibilityHandler() method will return nullptr | |||
and this component will not be visible to any accessibility clients. | |||
By default this is set to true. | |||
@see getAccessibilityHandler, createAccessibilityHandler | |||
*/ | |||
void setAccessible (bool shouldBeAccessible); | |||
/** Returns the accessibility handler for this component, or nullptr if this component is not | |||
accessible. | |||
@see createAccessibilityHandler, setAccessible | |||
*/ | |||
AccessibilityHandler* getAccessibilityHandler(); | |||
/** Invalidates the AccessibilityHandler that is currently being used for this component. | |||
Use this to indicate that something in the accessible component has changed | |||
and its handler needs to be updated. This will trigger a call to | |||
createAccessibilityHandler(). | |||
*/ | |||
void invalidateAccessibilityHandler(); | |||
//============================================================================== | |||
// This method has been deprecated in favour of the setFocusContainerType() method | |||
// that takes a more descriptive enum. | |||
JUCE_DEPRECATED_WITH_BODY (void setFocusContainer (bool shouldBeFocusContainer) noexcept, | |||
{ | |||
setFocusContainerType (shouldBeFocusContainer ? FocusContainerType::keyboardFocusContainer | |||
: FocusContainerType::none); | |||
}) | |||
private: | |||
//============================================================================== | |||
/** Override this method to return a custom AccessibilityHandler for this component. | |||
The default implementation creates and returns a AccessibilityHandler object with an | |||
unspecified role, meaning that it will be visible to accessibility clients but | |||
without a specific role, action callbacks or interfaces. To control how accessibility | |||
clients see and interact with your component subclass AccessibilityHandler, implement | |||
the desired behaviours, and return an instance of it from this method in your | |||
component subclass. | |||
The accessibility handler you return here is guaranteed to be destroyed before | |||
its Component, so it's safe to store and use a reference back to the Component | |||
inside the AccessibilityHandler if necessary. | |||
@see getAccessibilityHandler | |||
*/ | |||
virtual std::unique_ptr<AccessibilityHandler> createAccessibilityHandler(); | |||
//============================================================================== | |||
friend class ComponentPeer; | |||
friend class MouseInputSource; | |||
@@ -2294,7 +2471,7 @@ private: | |||
static Component* currentlyFocusedComponent; | |||
//============================================================================== | |||
String componentName, componentID; | |||
String componentName, componentID, componentTitle, componentDescription, componentHelpText; | |||
Component* parentComponent = nullptr; | |||
Rectangle<int> boundsRelativeToParent; | |||
std::unique_ptr<Positioner> positioner; | |||
@@ -2314,29 +2491,33 @@ private: | |||
friend class WeakReference<Component>; | |||
WeakReference<Component>::Master masterReference; | |||
std::unique_ptr<AccessibilityHandler> accessibilityHandler; | |||
struct ComponentFlags | |||
{ | |||
bool hasHeavyweightPeerFlag : 1; | |||
bool visibleFlag : 1; | |||
bool opaqueFlag : 1; | |||
bool ignoresMouseClicksFlag : 1; | |||
bool allowChildMouseClicksFlag : 1; | |||
bool wantsFocusFlag : 1; | |||
bool isFocusContainerFlag : 1; | |||
bool dontFocusOnMouseClickFlag : 1; | |||
bool alwaysOnTopFlag : 1; | |||
bool bufferToImageFlag : 1; | |||
bool bringToFrontOnClickFlag : 1; | |||
bool repaintOnMouseActivityFlag : 1; | |||
bool isDisabledFlag : 1; | |||
bool childCompFocusedFlag : 1; | |||
bool dontClipGraphicsFlag : 1; | |||
bool mouseDownWasBlocked : 1; | |||
bool isMoveCallbackPending : 1; | |||
bool isResizeCallbackPending : 1; | |||
bool viewportIgnoreDragFlag : 1; | |||
bool hasHeavyweightPeerFlag : 1; | |||
bool visibleFlag : 1; | |||
bool opaqueFlag : 1; | |||
bool ignoresMouseClicksFlag : 1; | |||
bool allowChildMouseClicksFlag : 1; | |||
bool wantsKeyboardFocusFlag : 1; | |||
bool isFocusContainerFlag : 1; | |||
bool isKeyboardFocusContainerFlag : 1; | |||
bool childKeyboardFocusedFlag : 1; | |||
bool dontFocusOnMouseClickFlag : 1; | |||
bool alwaysOnTopFlag : 1; | |||
bool bufferToImageFlag : 1; | |||
bool bringToFrontOnClickFlag : 1; | |||
bool repaintOnMouseActivityFlag : 1; | |||
bool isDisabledFlag : 1; | |||
bool dontClipGraphicsFlag : 1; | |||
bool mouseDownWasBlocked : 1; | |||
bool isMoveCallbackPending : 1; | |||
bool isResizeCallbackPending : 1; | |||
bool viewportIgnoreDragFlag : 1; | |||
bool accessibilityIgnoredFlag : 1; | |||
#if JUCE_DEBUG | |||
bool isInsidePaintCall : 1; | |||
bool isInsidePaintCall : 1; | |||
#endif | |||
}; | |||
@@ -2358,10 +2539,10 @@ private: | |||
void internalMouseWheel (MouseInputSource, Point<float>, Time, const MouseWheelDetails&); | |||
void internalMagnifyGesture (MouseInputSource, Point<float>, Time, float); | |||
void internalBroughtToFront(); | |||
void internalFocusGain (FocusChangeType, const WeakReference<Component>&); | |||
void internalFocusGain (FocusChangeType); | |||
void internalFocusLoss (FocusChangeType); | |||
void internalChildFocusChange (FocusChangeType, const WeakReference<Component>&); | |||
void internalKeyboardFocusGain (FocusChangeType, const WeakReference<Component>&); | |||
void internalKeyboardFocusGain (FocusChangeType); | |||
void internalKeyboardFocusLoss (FocusChangeType); | |||
void internalChildKeyboardFocusChange (FocusChangeType, const WeakReference<Component>&); | |||
void internalModalInputAttempt(); | |||
void internalModifierKeysChanged(); | |||
void internalChildrenChanged(); | |||
@@ -2377,8 +2558,8 @@ private: | |||
void repaintParent(); | |||
void sendFakeMouseMove() const; | |||
void takeKeyboardFocus (FocusChangeType); | |||
void grabFocusInternal (FocusChangeType, bool canTryParent); | |||
static void giveAwayFocus (bool sendFocusLossEvent); | |||
void grabKeyboardFocusInternal (FocusChangeType, bool canTryParent); | |||
void giveAwayKeyboardFocusInternal (bool sendFocusLossEvent); | |||
void sendEnablementChangeMessage(); | |||
void sendVisibilityChangeMessage(); | |||
@@ -0,0 +1,72 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** | |||
Base class for traversing components. | |||
If you need custom focus or keyboard focus traversal for a component you can | |||
create a subclass of ComponentTraverser and return it from | |||
Component::createFocusTraverser() or Component::createKeyboardFocusTraverser(). | |||
@see Component::createFocusTraverser, Component::createKeyboardFocusTraverser | |||
@tags{GUI} | |||
*/ | |||
class JUCE_API ComponentTraverser | |||
{ | |||
public: | |||
/** Destructor. */ | |||
virtual ~ComponentTraverser() = default; | |||
/** Returns the component that should be used as the traversal entry point | |||
within the given parent component. | |||
This must return nullptr if there is no default component. | |||
*/ | |||
virtual Component* getDefaultComponent (Component* parentComponent) = 0; | |||
/** Returns the component that comes after the specified one when moving "forwards". | |||
This must return nullptr if there is no next component. | |||
*/ | |||
virtual Component* getNextComponent (Component* current) = 0; | |||
/** Returns the component that comes after the specified one when moving "backwards". | |||
This must return nullptr if there is no previous component. | |||
*/ | |||
virtual Component* getPreviousComponent (Component* current) = 0; | |||
/** Returns all of the traversable components within the given parent component in | |||
traversal order. | |||
*/ | |||
virtual std::vector<Component*> getAllComponents (Component* parentComponent) = 0; | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,359 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
namespace FocusHelpers | |||
{ | |||
static int getOrder (const Component* c) | |||
{ | |||
auto order = c->getExplicitFocusOrder(); | |||
return order > 0 ? order : std::numeric_limits<int>::max(); | |||
} | |||
template <typename FocusContainerFn> | |||
static void findAllComponents (Component* parent, | |||
std::vector<Component*>& components, | |||
FocusContainerFn isFocusContainer) | |||
{ | |||
if (parent == nullptr || parent->getNumChildComponents() == 0) | |||
return; | |||
std::vector<Component*> localComponents; | |||
for (auto* c : parent->getChildren()) | |||
if (c->isVisible() && c->isEnabled()) | |||
localComponents.push_back (c); | |||
const auto compareComponents = [&] (const Component* a, const Component* b) | |||
{ | |||
const auto getComponentOrderAttributes = [] (const Component* c) | |||
{ | |||
return std::make_tuple (getOrder (c), | |||
c->isAlwaysOnTop() ? 0 : 1, | |||
c->getY(), | |||
c->getX()); | |||
}; | |||
return getComponentOrderAttributes (a) < getComponentOrderAttributes (b); | |||
}; | |||
// This will sort so that they are ordered in terms of explicit focus, | |||
// always on top, left-to-right, and then top-to-bottom. | |||
std::stable_sort (localComponents.begin(), localComponents.end(), compareComponents); | |||
for (auto* c : localComponents) | |||
{ | |||
components.push_back (c); | |||
if (! (c->*isFocusContainer)()) | |||
findAllComponents (c, components, isFocusContainer); | |||
} | |||
} | |||
enum class NavigationDirection { forwards, backwards }; | |||
template <typename FocusContainerFn> | |||
static Component* navigateFocus (Component* current, | |||
Component* focusContainer, | |||
NavigationDirection direction, | |||
FocusContainerFn isFocusContainer) | |||
{ | |||
if (focusContainer != nullptr) | |||
{ | |||
std::vector<Component*> components; | |||
findAllComponents (focusContainer, components, isFocusContainer); | |||
const auto iter = std::find (components.cbegin(), components.cend(), current); | |||
if (iter == components.cend()) | |||
return nullptr; | |||
switch (direction) | |||
{ | |||
case NavigationDirection::forwards: | |||
if (iter != std::prev (components.cend())) | |||
return *std::next (iter); | |||
break; | |||
case NavigationDirection::backwards: | |||
if (iter != components.cbegin()) | |||
return *std::prev (iter); | |||
break; | |||
} | |||
} | |||
return nullptr; | |||
} | |||
} | |||
//============================================================================== | |||
Component* FocusTraverser::getNextComponent (Component* current) | |||
{ | |||
jassert (current != nullptr); | |||
return FocusHelpers::navigateFocus (current, | |||
current->findFocusContainer(), | |||
FocusHelpers::NavigationDirection::forwards, | |||
&Component::isFocusContainer); | |||
} | |||
Component* FocusTraverser::getPreviousComponent (Component* current) | |||
{ | |||
jassert (current != nullptr); | |||
return FocusHelpers::navigateFocus (current, | |||
current->findFocusContainer(), | |||
FocusHelpers::NavigationDirection::backwards, | |||
&Component::isFocusContainer); | |||
} | |||
Component* FocusTraverser::getDefaultComponent (Component* parentComponent) | |||
{ | |||
if (parentComponent != nullptr) | |||
{ | |||
std::vector<Component*> components; | |||
FocusHelpers::findAllComponents (parentComponent, | |||
components, | |||
&Component::isFocusContainer); | |||
if (! components.empty()) | |||
return components.front(); | |||
} | |||
return nullptr; | |||
} | |||
std::vector<Component*> FocusTraverser::getAllComponents (Component* parentComponent) | |||
{ | |||
std::vector<Component*> components; | |||
FocusHelpers::findAllComponents (parentComponent, | |||
components, | |||
&Component::isFocusContainer); | |||
return components; | |||
} | |||
//============================================================================== | |||
//============================================================================== | |||
#if JUCE_UNIT_TESTS | |||
struct FocusTraverserTests : public UnitTest | |||
{ | |||
FocusTraverserTests() | |||
: UnitTest ("FocusTraverser", UnitTestCategories::gui) | |||
{} | |||
void runTest() override | |||
{ | |||
ScopedJuceInitialiser_GUI libraryInitialiser; | |||
beginTest ("Basic traversal"); | |||
{ | |||
TestComponent parent; | |||
expect (traverser.getDefaultComponent (&parent) == &parent.children.front()); | |||
for (auto iter = parent.children.begin(); iter != parent.children.end(); ++iter) | |||
expect (traverser.getNextComponent (&(*iter)) == (iter == std::prev (parent.children.cend()) ? nullptr | |||
: &(*std::next (iter)))); | |||
for (auto iter = parent.children.rbegin(); iter != parent.children.rend(); ++iter) | |||
expect (traverser.getPreviousComponent (&(*iter)) == (iter == std::prev (parent.children.rend()) ? nullptr | |||
: &(*std::next (iter)))); | |||
auto allComponents = traverser.getAllComponents (&parent); | |||
expect (std::equal (allComponents.cbegin(), allComponents.cend(), parent.children.cbegin(), | |||
[] (const Component* c1, const Component& c2) { return c1 == &c2; })); | |||
} | |||
beginTest ("Disabled components are ignored"); | |||
{ | |||
checkIgnored ([] (Component& c) { c.setEnabled (false); }); | |||
} | |||
beginTest ("Invisible components are ignored"); | |||
{ | |||
checkIgnored ([] (Component& c) { c.setVisible (false); }); | |||
} | |||
beginTest ("Explicit focus order comes before unspecified"); | |||
{ | |||
TestComponent parent; | |||
auto& explicitFocusComponent = parent.children[2]; | |||
explicitFocusComponent.setExplicitFocusOrder (1); | |||
expect (traverser.getDefaultComponent (&parent) == &explicitFocusComponent); | |||
expect (traverser.getAllComponents (&parent).front() == &explicitFocusComponent); | |||
} | |||
beginTest ("Explicit focus order comparison"); | |||
{ | |||
checkComponentProperties ([this] (Component& child) { child.setExplicitFocusOrder (getRandom().nextInt ({ 1, 100 })); }, | |||
[] (const Component& c1, const Component& c2) { return c1.getExplicitFocusOrder() | |||
<= c2.getExplicitFocusOrder(); }); | |||
} | |||
beginTest ("Left to right"); | |||
{ | |||
checkComponentProperties ([this] (Component& child) { child.setTopLeftPosition (getRandom().nextInt ({ 0, 100 }), 0); }, | |||
[] (const Component& c1, const Component& c2) { return c1.getX() <= c2.getX(); }); | |||
} | |||
beginTest ("Top to bottom"); | |||
{ | |||
checkComponentProperties ([this] (Component& child) { child.setTopLeftPosition (0, getRandom().nextInt ({ 0, 100 })); }, | |||
[] (const Component& c1, const Component& c2) { return c1.getY() <= c2.getY(); }); | |||
} | |||
beginTest ("Focus containers have their own focus"); | |||
{ | |||
Component root; | |||
TestComponent container; | |||
container.setFocusContainerType (Component::FocusContainerType::focusContainer); | |||
root.addAndMakeVisible (container); | |||
expect (traverser.getDefaultComponent (&root) == &container); | |||
expect (traverser.getNextComponent (&container) == nullptr); | |||
expect (traverser.getPreviousComponent (&container) == nullptr); | |||
expect (traverser.getDefaultComponent (&container) == &container.children.front()); | |||
for (auto iter = container.children.begin(); iter != container.children.end(); ++iter) | |||
expect (traverser.getNextComponent (&(*iter)) == (iter == std::prev (container.children.cend()) ? nullptr | |||
: &(*std::next (iter)))); | |||
for (auto iter = container.children.rbegin(); iter != container.children.rend(); ++iter) | |||
expect (traverser.getPreviousComponent (&(*iter)) == (iter == std::prev (container.children.rend()) ? nullptr | |||
: &(*std::next (iter)))); | |||
expect (traverser.getAllComponents (&root).size() == 1); | |||
auto allContainerComponents = traverser.getAllComponents (&container); | |||
expect (std::equal (allContainerComponents.cbegin(), allContainerComponents.cend(), container.children.cbegin(), | |||
[] (const Component* c1, const Component& c2) { return c1 == &c2; })); | |||
} | |||
beginTest ("Non-focus containers pass-through focus"); | |||
{ | |||
Component root; | |||
TestComponent container; | |||
container.setFocusContainerType (Component::FocusContainerType::none); | |||
root.addAndMakeVisible (container); | |||
expect (traverser.getDefaultComponent (&root) == &container); | |||
expect (traverser.getNextComponent (&container) == &container.children.front()); | |||
expect (traverser.getPreviousComponent (&container) == nullptr); | |||
expect (traverser.getDefaultComponent (&container) == &container.children.front()); | |||
for (auto iter = container.children.begin(); iter != container.children.end(); ++iter) | |||
expect (traverser.getNextComponent (&(*iter)) == (iter == std::prev (container.children.cend()) ? nullptr | |||
: &(*std::next (iter)))); | |||
for (auto iter = container.children.rbegin(); iter != container.children.rend(); ++iter) | |||
expect (traverser.getPreviousComponent (&(*iter)) == (iter == std::prev (container.children.rend()) ? &container | |||
: &(*std::next (iter)))); | |||
expect (traverser.getAllComponents (&root).size() == container.children.size() + 1); | |||
} | |||
} | |||
private: | |||
struct TestComponent : public Component | |||
{ | |||
TestComponent() | |||
{ | |||
for (auto& child : children) | |||
addAndMakeVisible (child); | |||
} | |||
std::array<Component, 10> children; | |||
}; | |||
void checkComponentProperties (std::function<void (Component&)>&& childFn, | |||
std::function<bool (const Component&, const Component&)>&& testProperty) | |||
{ | |||
TestComponent parent; | |||
for (auto& child : parent.children) | |||
childFn (child); | |||
auto* comp = traverser.getDefaultComponent (&parent); | |||
for (const auto& child : parent.children) | |||
if (&child != comp) | |||
expect (testProperty (*comp, child)); | |||
for (;;) | |||
{ | |||
auto* next = traverser.getNextComponent (comp); | |||
if (next == nullptr) | |||
break; | |||
expect (testProperty (*comp, *next)); | |||
comp = next; | |||
} | |||
} | |||
void checkIgnored (const std::function<void(Component&)>& makeIgnored) | |||
{ | |||
TestComponent parent; | |||
auto iter = parent.children.begin(); | |||
makeIgnored (*iter); | |||
expect (traverser.getDefaultComponent (&parent) == std::addressof (*std::next (iter))); | |||
iter += 5; | |||
makeIgnored (*iter); | |||
expect (traverser.getNextComponent (std::addressof (*std::prev (iter))) == std::addressof (*std::next (iter))); | |||
expect (traverser.getPreviousComponent (std::addressof (*std::next (iter))) == std::addressof (*std::prev (iter))); | |||
auto allComponents = traverser.getAllComponents (&parent); | |||
expect (std::find (allComponents.cbegin(), allComponents.cend(), &parent.children.front()) == allComponents.cend()); | |||
expect (std::find (allComponents.cbegin(), allComponents.cend(), std::addressof (*iter)) == allComponents.cend()); | |||
} | |||
FocusTraverser traverser; | |||
}; | |||
static FocusTraverserTests focusTraverserTests; | |||
#endif | |||
} // namespace juce |
@@ -0,0 +1,93 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
/** | |||
Controls the order in which focus moves between components. | |||
The algorithm used by this class to work out the order of traversal is as | |||
follows: | |||
- Only visible and enabled components are considered focusable. | |||
- If two components both have an explicit focus order specified then the | |||
one with the lowest number comes first (see the | |||
Component::setExplicitFocusOrder() method). | |||
- Any component with an explicit focus order greater than 0 comes before ones | |||
that don't have an order specified. | |||
- Components with their 'always on top' flag set come before those without. | |||
- Any unspecified components are traversed in a left-to-right, then | |||
top-to-bottom order. | |||
If you need focus traversal in a more customised way you can create a | |||
ComponentTraverser subclass that uses your own algorithm and return it | |||
from Component::createFocusTraverser(). | |||
@see ComponentTraverser, Component::createFocusTraverser | |||
@tags{GUI} | |||
*/ | |||
class JUCE_API FocusTraverser : public ComponentTraverser | |||
{ | |||
public: | |||
/** Destructor. */ | |||
~FocusTraverser() override = default; | |||
/** Returns the component that should receive focus by default within the given | |||
parent component. | |||
The default implementation will just return the foremost visible and enabled | |||
child component, and will return nullptr if there is no suitable component. | |||
*/ | |||
Component* getDefaultComponent (Component* parentComponent) override; | |||
/** Returns the component that should be given focus after the specified one when | |||
moving "forwards". | |||
The default implementation will return the next visible and enabled component | |||
which is to the right of or below this one, and will return nullptr if there | |||
is no suitable component. | |||
*/ | |||
Component* getNextComponent (Component* current) override; | |||
/** Returns the component that should be given focus after the specified one when | |||
moving "backwards". | |||
The default implementation will return the previous visible and enabled component | |||
which is to the left of or above this one, and will return nullptr if there | |||
is no suitable component. | |||
*/ | |||
Component* getPreviousComponent (Component* current) override; | |||
/** Returns all of the components that can receive focus within the given parent | |||
component in traversal order. | |||
The default implementation will return all visible and enabled child components. | |||
*/ | |||
std::vector<Component*> getAllComponents (Component* parentComponent) override; | |||
}; | |||
} // namespace juce |
@@ -204,4 +204,10 @@ std::unique_ptr<Drawable> Drawable::createFromImageFile (const File& file) | |||
return {}; | |||
} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> Drawable::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::ignored); | |||
} | |||
} // namespace juce |
@@ -199,6 +199,8 @@ protected: | |||
void setBoundsToEnclose (Rectangle<float>); | |||
/** @internal */ | |||
void applyDrawableClipPath (Graphics&); | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
Point<int> originRelativeToComponent; | |||
std::unique_ptr<Drawable> drawableClipPath; | |||
@@ -133,4 +133,10 @@ Path DrawableImage::getOutlineAsPath() const | |||
return {}; // not applicable for images | |||
} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> DrawableImage::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::image); | |||
} | |||
} // namespace juce |
@@ -94,6 +94,8 @@ public: | |||
Rectangle<float> getDrawableBounds() const override; | |||
/** @internal */ | |||
Path getOutlineAsPath() const override; | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
private: | |||
//============================================================================== | |||
@@ -208,4 +208,25 @@ bool DrawableText::replaceColour (Colour originalColour, Colour replacementColou | |||
return true; | |||
} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> DrawableText::createAccessibilityHandler() | |||
{ | |||
class DrawableTextAccessibilityHandler : public AccessibilityHandler | |||
{ | |||
public: | |||
DrawableTextAccessibilityHandler (DrawableText& drawableTextToWrap) | |||
: AccessibilityHandler (drawableTextToWrap, AccessibilityRole::staticText), | |||
drawableText (drawableTextToWrap) | |||
{ | |||
} | |||
String getTitle() const override { return drawableText.getText(); } | |||
private: | |||
DrawableText& drawableText; | |||
}; | |||
return std::make_unique<DrawableTextAccessibilityHandler> (*this); | |||
} | |||
} // namespace juce |
@@ -98,6 +98,8 @@ public: | |||
Path getOutlineAsPath() const override; | |||
/** @internal */ | |||
bool replaceColour (Colour originalColour, Colour replacementColour) override; | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
private: | |||
//============================================================================== | |||
@@ -616,4 +616,10 @@ void FileBrowserComponent::timerCallback() | |||
} | |||
} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> FileBrowserComponent::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||
} | |||
} // namespace juce |
@@ -252,6 +252,8 @@ public: | |||
FilePreviewComponent* getPreviewComponent() const noexcept; | |||
/** @internal */ | |||
DirectoryContentsDisplayComponent* getDisplayComponent() const noexcept; | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
protected: | |||
/** Returns a list of names and paths for the default places the user might want to look. | |||
@@ -35,6 +35,7 @@ FileListComponent::FileListComponent (DirectoryContentsList& listToShow) | |||
DirectoryContentsDisplayComponent (listToShow), | |||
lastDirectory (listToShow.getDirectory()) | |||
{ | |||
setTitle ("Files"); | |||
setModel (this); | |||
directoryContentsList.addChangeListener (this); | |||
} | |||
@@ -68,7 +69,7 @@ void FileListComponent::setSelectedFile (const File& f) | |||
{ | |||
for (int i = directoryContentsList.getNumFiles(); --i >= 0;) | |||
{ | |||
if (directoryContentsList.getFile(i) == f) | |||
if (directoryContentsList.getFile (i) == f) | |||
{ | |||
fileWaitingToBeSelected = File(); | |||
@@ -189,6 +190,11 @@ public: | |||
repaint(); | |||
} | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override | |||
{ | |||
return nullptr; | |||
} | |||
private: | |||
//============================================================================== | |||
FileListComponent& owner; | |||
@@ -231,6 +237,11 @@ int FileListComponent::getNumRows() | |||
return directoryContentsList.getNumFiles(); | |||
} | |||
String FileListComponent::getNameForRow (int rowNumber) | |||
{ | |||
return directoryContentsList.getFile (rowNumber).getFileName(); | |||
} | |||
void FileListComponent::paintListBoxItem (int, Graphics&, int, int, bool) | |||
{ | |||
} | |||
@@ -82,6 +82,7 @@ private: | |||
void changeListenerCallback (ChangeBroadcaster*) override; | |||
int getNumRows() override; | |||
String getNameForRow (int rowNumber) override; | |||
void paintListBoxItem (int, Graphics&, int, int, bool) override; | |||
Component* refreshComponentForRow (int rowNumber, bool isRowSelected, Component*) override; | |||
void selectedRowsChanged (int row) override; | |||
@@ -190,6 +190,11 @@ public: | |||
indexInContentsList, owner); | |||
} | |||
String getAccessibilityName() override | |||
{ | |||
return file.getFileName(); | |||
} | |||
void itemClicked (const MouseEvent& e) override | |||
{ | |||
owner.sendMouseClickMessage (file, e); | |||
@@ -70,11 +70,11 @@ void FilenameComponent::resized() | |||
getLookAndFeel().layoutFilenameComponent (*this, &filenameBox, browseButton.get()); | |||
} | |||
KeyboardFocusTraverser* FilenameComponent::createFocusTraverser() | |||
std::unique_ptr<ComponentTraverser> FilenameComponent::createKeyboardFocusTraverser() | |||
{ | |||
// This prevents the sub-components from grabbing focus if the | |||
// FilenameComponent has been set to refuse focus. | |||
return getWantsKeyboardFocus() ? Component::createFocusTraverser() : nullptr; | |||
return getWantsKeyboardFocus() ? Component::createKeyboardFocusTraverser() : nullptr; | |||
} | |||
void FilenameComponent::setBrowseButtonText (const String& newBrowseButtonText) | |||
@@ -212,7 +212,7 @@ public: | |||
/** @internal */ | |||
void fileDragExit (const StringArray&) override; | |||
/** @internal */ | |||
KeyboardFocusTraverser* createFocusTraverser() override; | |||
std::unique_ptr<ComponentTraverser> createKeyboardFocusTraverser() override; | |||
private: | |||
//============================================================================== | |||
@@ -117,4 +117,10 @@ void ImagePreviewComponent::paint (Graphics& g) | |||
} | |||
} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> ImagePreviewComponent::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::image); | |||
} | |||
} // namespace juce |
@@ -53,6 +53,8 @@ public: | |||
void paint (Graphics&) override; | |||
/** @internal */ | |||
void timerCallback() override; | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
private: | |||
File fileToLoad; | |||
@@ -66,6 +66,8 @@ | |||
#include <windowsx.h> | |||
#include <vfw.h> | |||
#include <commdlg.h> | |||
#include <UIAutomation.h> | |||
#include <sapi.h> | |||
#if JUCE_WEB_BROWSER | |||
#include <exdisp.h> | |||
@@ -103,8 +105,10 @@ namespace juce | |||
extern bool juce_areThereAnyAlwaysOnTopWindows(); | |||
} | |||
#include "accessibility/juce_AccessibilityHandler.cpp" | |||
#include "components/juce_Component.cpp" | |||
#include "components/juce_ComponentListener.cpp" | |||
#include "components/juce_FocusTraverser.cpp" | |||
#include "mouse/juce_MouseInputSource.cpp" | |||
#include "desktop/juce_Displays.cpp" | |||
#include "desktop/juce_Desktop.cpp" | |||
@@ -240,6 +244,7 @@ namespace juce | |||
#endif | |||
#else | |||
#include "native/accessibility/juce_mac_Accessibility.mm" | |||
#include "native/juce_mac_NSViewComponentPeer.mm" | |||
#include "native/juce_mac_Windowing.mm" | |||
#include "native/juce_mac_MainMenu.mm" | |||
@@ -249,6 +254,12 @@ namespace juce | |||
#include "native/juce_mac_MouseCursor.mm" | |||
#elif JUCE_WINDOWS | |||
#include "native/accessibility/juce_win32_WindowsUIAWrapper.h" | |||
#include "native/accessibility/juce_win32_AccessibilityElement.h" | |||
#include "native/accessibility/juce_win32_UIAHelpers.h" | |||
#include "native/accessibility/juce_win32_UIAProviders.h" | |||
#include "native/accessibility/juce_win32_AccessibilityElement.cpp" | |||
#include "native/accessibility/juce_win32_Accessibility.cpp" | |||
#include "native/juce_win32_Windowing.cpp" | |||
#include "native/juce_win32_DragAndDrop.cpp" | |||
#include "native/juce_win32_FileChooser.cpp" | |||
@@ -155,6 +155,8 @@ namespace juce | |||
class ApplicationCommandManagerListener; | |||
class DrawableButton; | |||
class Displays; | |||
class AccessibilityHandler; | |||
class KeyboardFocusTraverser; | |||
class FlexBox; | |||
class Grid; | |||
@@ -167,7 +169,8 @@ namespace juce | |||
#include "mouse/juce_MouseEvent.h" | |||
#include "keyboard/juce_KeyPress.h" | |||
#include "keyboard/juce_KeyListener.h" | |||
#include "keyboard/juce_KeyboardFocusTraverser.h" | |||
#include "components/juce_ComponentTraverser.h" | |||
#include "components/juce_FocusTraverser.h" | |||
#include "components/juce_ModalComponentManager.h" | |||
#include "components/juce_ComponentListener.h" | |||
#include "components/juce_CachedComponentImage.h" | |||
@@ -185,6 +188,7 @@ namespace juce | |||
#include "mouse/juce_TextDragAndDropTarget.h" | |||
#include "mouse/juce_TooltipClient.h" | |||
#include "keyboard/juce_CaretComponent.h" | |||
#include "keyboard/juce_KeyboardFocusTraverser.h" | |||
#include "keyboard/juce_SystemClipboard.h" | |||
#include "keyboard/juce_TextEditorKeyMapper.h" | |||
#include "keyboard/juce_TextInputTarget.h" | |||
@@ -293,6 +297,22 @@ namespace juce | |||
#include "lookandfeel/juce_LookAndFeel_V3.h" | |||
#include "lookandfeel/juce_LookAndFeel_V4.h" | |||
#include "mouse/juce_LassoComponent.h" | |||
#include "accessibility/interfaces/juce_AccessibilityCellInterface.h" | |||
#include "accessibility/interfaces/juce_AccessibilityTableInterface.h" | |||
#include "accessibility/interfaces/juce_AccessibilityTextInterface.h" | |||
#include "accessibility/interfaces/juce_AccessibilityValueInterface.h" | |||
#include "accessibility/enums/juce_AccessibilityActions.h" | |||
#include "accessibility/enums/juce_AccessibilityEvent.h" | |||
#include "accessibility/enums/juce_AccessibilityRole.h" | |||
#include "accessibility/juce_AccessibilityState.h" | |||
#include "accessibility/juce_AccessibilityHandler.h" | |||
#include "accessibility/widget_handlers/juce_ButtonAccessibilityHandler.h" | |||
#include "accessibility/widget_handlers/juce_ComboBoxAccessibilityHandler.h" | |||
#include "accessibility/widget_handlers/juce_LabelAccessibilityHandler.h" | |||
#include "accessibility/widget_handlers/juce_SliderAccessibilityHandler.h" | |||
#include "accessibility/widget_handlers/juce_TableListBoxAccessibilityHandler.h" | |||
#include "accessibility/widget_handlers/juce_TextEditorAccessibilityHandler.h" | |||
#include "accessibility/widget_handlers/juce_TreeViewAccessibilityHandler.h" | |||
#if JUCE_LINUX || JUCE_BSD | |||
#if JUCE_GUI_BASICS_INCLUDE_XHEADERS | |||
@@ -26,105 +26,248 @@ | |||
namespace juce | |||
{ | |||
namespace KeyboardFocusHelpers | |||
//============================================================================== | |||
namespace KeyboardFocusTraverserHelpers | |||
{ | |||
static int getOrder (const Component* c) | |||
static bool isKeyboardFocusable (const Component* comp, const Component* container) | |||
{ | |||
return comp->getWantsKeyboardFocus() && container->isParentOf (comp); | |||
} | |||
static Component* traverse (Component* current, Component* container, | |||
FocusHelpers::NavigationDirection direction) | |||
{ | |||
auto order = c->getExplicitFocusOrder(); | |||
return order > 0 ? order : (std::numeric_limits<int>::max() / 2); | |||
if (auto* comp = FocusHelpers::navigateFocus (current, container, direction, | |||
&Component::isKeyboardFocusContainer)) | |||
{ | |||
if (isKeyboardFocusable (comp, container)) | |||
return comp; | |||
return traverse (comp, container, direction); | |||
} | |||
return nullptr; | |||
} | |||
} | |||
Component* KeyboardFocusTraverser::getNextComponent (Component* current) | |||
{ | |||
return KeyboardFocusTraverserHelpers::traverse (current, current->findKeyboardFocusContainer(), | |||
FocusHelpers::NavigationDirection::forwards); | |||
} | |||
Component* KeyboardFocusTraverser::getPreviousComponent (Component* current) | |||
{ | |||
return KeyboardFocusTraverserHelpers::traverse (current, current->findKeyboardFocusContainer(), | |||
FocusHelpers::NavigationDirection::backwards); | |||
} | |||
Component* KeyboardFocusTraverser::getDefaultComponent (Component* parentComponent) | |||
{ | |||
for (auto* comp : getAllComponents (parentComponent)) | |||
if (KeyboardFocusTraverserHelpers::isKeyboardFocusable (comp, parentComponent)) | |||
return comp; | |||
return nullptr; | |||
} | |||
static void findAllFocusableComponents (Component* parent, Array<Component*>& comps) | |||
std::vector<Component*> KeyboardFocusTraverser::getAllComponents (Component* parentComponent) | |||
{ | |||
std::vector<Component*> components; | |||
FocusHelpers::findAllComponents (parentComponent, | |||
components, | |||
&Component::isKeyboardFocusContainer); | |||
auto removePredicate = [parentComponent] (const Component* comp) | |||
{ | |||
if (parent->getNumChildComponents() != 0) | |||
return ! KeyboardFocusTraverserHelpers::isKeyboardFocusable (comp, parentComponent); | |||
}; | |||
components.erase (std::remove_if (std::begin (components), std::end (components), std::move (removePredicate)), | |||
std::end (components)); | |||
return components; | |||
} | |||
//============================================================================== | |||
//============================================================================== | |||
#if JUCE_UNIT_TESTS | |||
struct KeyboardFocusTraverserTests : public UnitTest | |||
{ | |||
KeyboardFocusTraverserTests() | |||
: UnitTest ("KeyboardFocusTraverser", UnitTestCategories::gui) | |||
{} | |||
void runTest() override | |||
{ | |||
ScopedJuceInitialiser_GUI libraryInitialiser; | |||
beginTest ("No child wants keyboard focus"); | |||
{ | |||
TestComponent parent; | |||
expect (traverser.getDefaultComponent (&parent) == nullptr); | |||
expect (traverser.getAllComponents (&parent).empty()); | |||
} | |||
beginTest ("Single child wants keyboard focus"); | |||
{ | |||
TestComponent parent; | |||
parent.children[5].setWantsKeyboardFocus (true); | |||
auto* defaultComponent = traverser.getDefaultComponent (&parent); | |||
expect (defaultComponent == &parent.children[5]); | |||
expect (defaultComponent->getWantsKeyboardFocus()); | |||
expect (traverser.getNextComponent (defaultComponent) == nullptr); | |||
expect (traverser.getPreviousComponent (defaultComponent) == nullptr); | |||
expect (traverser.getAllComponents (&parent).size() == 1); | |||
} | |||
beginTest ("Multiple children want keyboard focus"); | |||
{ | |||
Array<Component*> localComps; | |||
TestComponent parent; | |||
Component* focusChildren[] | |||
{ | |||
&parent.children[1], | |||
&parent.children[9], | |||
&parent.children[3], | |||
&parent.children[5], | |||
&parent.children[8], | |||
&parent.children[0] | |||
}; | |||
for (auto* focusChild : focusChildren) | |||
focusChild->setWantsKeyboardFocus (true); | |||
auto allComponents = traverser.getAllComponents (&parent); | |||
for (auto* c : parent->getChildren()) | |||
if (c->isVisible() && c->isEnabled()) | |||
localComps.add (c); | |||
for (auto* focusChild : focusChildren) | |||
expect (std::find (allComponents.cbegin(), allComponents.cend(), focusChild) != allComponents.cend()); | |||
// This will sort so that they are ordered in terms of left-to-right | |||
// and then top-to-bottom. | |||
std::stable_sort (localComps.begin(), localComps.end(), | |||
[] (const Component* a, const Component* b) | |||
auto* componentToTest = traverser.getDefaultComponent (&parent); | |||
for (;;) | |||
{ | |||
auto explicitOrder1 = getOrder (a); | |||
auto explicitOrder2 = getOrder (b); | |||
expect (componentToTest->getWantsKeyboardFocus()); | |||
expect (std::find (std::begin (focusChildren), std::end (focusChildren), componentToTest) != std::end (focusChildren)); | |||
componentToTest = traverser.getNextComponent (componentToTest); | |||
if (explicitOrder1 != explicitOrder2) | |||
return explicitOrder1 < explicitOrder2; | |||
if (componentToTest == nullptr) | |||
break; | |||
} | |||
if (a->getY() != b->getY()) | |||
return a->getY() < b->getY(); | |||
int focusOrder = 1; | |||
for (auto* focusChild : focusChildren) | |||
focusChild->setExplicitFocusOrder (focusOrder++); | |||
return a->getX() < b->getX(); | |||
}); | |||
componentToTest = traverser.getDefaultComponent (&parent); | |||
for (auto* c : localComps) | |||
for (auto* focusChild : focusChildren) | |||
{ | |||
if (c->getWantsKeyboardFocus()) | |||
comps.add (c); | |||
expect (componentToTest == focusChild); | |||
expect (componentToTest->getWantsKeyboardFocus()); | |||
if (! c->isFocusContainer()) | |||
findAllFocusableComponents (c, comps); | |||
componentToTest = traverser.getNextComponent (componentToTest); | |||
} | |||
} | |||
} | |||
static Component* findFocusContainer (Component* c) | |||
{ | |||
c = c->getParentComponent(); | |||
beginTest ("Single nested child wants keyboard focus"); | |||
{ | |||
TestComponent parent; | |||
Component grandparent; | |||
if (c != nullptr) | |||
while (c->getParentComponent() != nullptr && ! c->isFocusContainer()) | |||
c = c->getParentComponent(); | |||
grandparent.addAndMakeVisible (parent); | |||
return c; | |||
} | |||
auto& focusChild = parent.children[5]; | |||
static Component* getIncrementedComponent (Component* current, int delta) | |||
{ | |||
if (auto* focusContainer = findFocusContainer (current)) | |||
focusChild.setWantsKeyboardFocus (true); | |||
expect (traverser.getDefaultComponent (&grandparent) == &focusChild); | |||
expect (traverser.getDefaultComponent (&parent) == &focusChild); | |||
expect (traverser.getNextComponent (&focusChild) == nullptr); | |||
expect (traverser.getPreviousComponent (&focusChild) == nullptr); | |||
expect (traverser.getAllComponents (&parent).size() == 1); | |||
} | |||
beginTest ("Multiple nested children want keyboard focus"); | |||
{ | |||
Array<Component*> comps; | |||
KeyboardFocusHelpers::findAllFocusableComponents (focusContainer, comps); | |||
TestComponent parent; | |||
Component grandparent; | |||
grandparent.addAndMakeVisible (parent); | |||
Component* focusChildren[] | |||
{ | |||
&parent.children[1], | |||
&parent.children[4], | |||
&parent.children[5] | |||
}; | |||
for (auto* focusChild : focusChildren) | |||
focusChild->setWantsKeyboardFocus (true); | |||
auto allComponents = traverser.getAllComponents (&parent); | |||
expect (std::equal (allComponents.cbegin(), allComponents.cend(), focusChildren, | |||
[] (const Component* c1, const Component* c2) { return c1 == c2; })); | |||
if (! comps.isEmpty()) | |||
const auto front = *focusChildren; | |||
const auto back = *std::prev (std::end (focusChildren)); | |||
expect (traverser.getDefaultComponent (&grandparent) == front); | |||
expect (traverser.getDefaultComponent (&parent) == front); | |||
expect (traverser.getNextComponent (front) == *std::next (std::begin (focusChildren))); | |||
expect (traverser.getPreviousComponent (back) == *std::prev (std::end (focusChildren), 2)); | |||
std::array<Component, 3> otherParents; | |||
for (auto& p : otherParents) | |||
{ | |||
auto index = comps.indexOf (current); | |||
return comps [(index + comps.size() + delta) % comps.size()]; | |||
grandparent.addAndMakeVisible (p); | |||
p.setWantsKeyboardFocus (true); | |||
} | |||
} | |||
return nullptr; | |||
} | |||
} | |||
expect (traverser.getDefaultComponent (&grandparent) == front); | |||
expect (traverser.getDefaultComponent (&parent) == front); | |||
expect (traverser.getNextComponent (back) == &otherParents.front()); | |||
expect (traverser.getNextComponent (&otherParents.back()) == nullptr); | |||
expect (traverser.getAllComponents (&grandparent).size() == numElementsInArray (focusChildren) + otherParents.size()); | |||
expect (traverser.getAllComponents (&parent).size() == (size_t) numElementsInArray (focusChildren)); | |||
//============================================================================== | |||
KeyboardFocusTraverser::KeyboardFocusTraverser() {} | |||
KeyboardFocusTraverser::~KeyboardFocusTraverser() {} | |||
for (auto* focusChild : focusChildren) | |||
focusChild->setWantsKeyboardFocus (false); | |||
Component* KeyboardFocusTraverser::getNextComponent (Component* current) | |||
{ | |||
jassert (current != nullptr); | |||
return KeyboardFocusHelpers::getIncrementedComponent (current, 1); | |||
} | |||
expect (traverser.getDefaultComponent (&grandparent) == &otherParents.front()); | |||
expect (traverser.getDefaultComponent (&parent) == nullptr); | |||
expect (traverser.getAllComponents (&grandparent).size() == otherParents.size()); | |||
expect (traverser.getAllComponents (&parent).empty()); | |||
} | |||
} | |||
Component* KeyboardFocusTraverser::getPreviousComponent (Component* current) | |||
{ | |||
jassert (current != nullptr); | |||
return KeyboardFocusHelpers::getIncrementedComponent (current, -1); | |||
} | |||
private: | |||
struct TestComponent : public Component | |||
{ | |||
TestComponent() | |||
{ | |||
for (auto& child : children) | |||
addAndMakeVisible (child); | |||
} | |||
Component* KeyboardFocusTraverser::getDefaultComponent (Component* parentComponent) | |||
{ | |||
Array<Component*> comps; | |||
std::array<Component, 10> children; | |||
}; | |||
if (parentComponent != nullptr) | |||
KeyboardFocusHelpers::findAllFocusableComponents (parentComponent, comps); | |||
KeyboardFocusTraverser traverser; | |||
}; | |||
return comps.getFirst(); | |||
} | |||
static KeyboardFocusTraverserTests keyboardFocusTraverserTests; | |||
#endif | |||
} // namespace juce |
@@ -28,63 +28,60 @@ namespace juce | |||
//============================================================================== | |||
/** | |||
Controls the order in which focus moves between components. | |||
Controls the order in which keyboard focus moves between components. | |||
The default algorithm used by this class to work out the order of traversal | |||
is as follows: | |||
- if two components both have an explicit focus order specified, then the | |||
one with the lowest number comes first (see the Component::setExplicitFocusOrder() | |||
method). | |||
- any component with an explicit focus order greater than 0 comes before ones | |||
that don't have an order specified. | |||
- any unspecified components are traversed in a left-to-right, then top-to-bottom | |||
order. | |||
The default behaviour of this class uses a FocusTraverser object internally to | |||
determine the default/next/previous component until it finds one which wants | |||
keyboard focus, as set by the Component::setWantsKeyboardFocus() method. | |||
If you need traversal in a more customised way, you can create a subclass | |||
of KeyboardFocusTraverser that uses your own algorithm, and use | |||
Component::createFocusTraverser() to create it. | |||
If you need keyboard focus traversal in a more customised way, you can create | |||
a subclass of ComponentTraverser that uses your own algorithm, and use | |||
Component::createKeyboardFocusTraverser() to create it. | |||
@see Component::setExplicitFocusOrder, Component::createFocusTraverser | |||
@see FocusTraverser, ComponentTraverser, Component::createKeyboardFocusTraverser | |||
@tags{GUI} | |||
*/ | |||
class JUCE_API KeyboardFocusTraverser | |||
class JUCE_API KeyboardFocusTraverser : public ComponentTraverser | |||
{ | |||
public: | |||
KeyboardFocusTraverser(); | |||
/** Destructor. */ | |||
virtual ~KeyboardFocusTraverser(); | |||
/** Returns the component that should be given focus after the specified one | |||
when moving "forwards". | |||
~KeyboardFocusTraverser() override = default; | |||
The default implementation will return the next component which is to the | |||
right of or below this one. | |||
/** Returns the component that should receive keyboard focus by default within the | |||
given parent component. | |||
This may return nullptr if there's no suitable candidate. | |||
The default implementation will return the foremost focusable component (as | |||
determined by FocusTraverser) that also wants keyboard focus, or nullptr if | |||
there is no suitable component. | |||
*/ | |||
virtual Component* getNextComponent (Component* current); | |||
/** Returns the component that should be given focus after the specified one | |||
when moving "backwards". | |||
Component* getDefaultComponent (Component* parentComponent) override; | |||
The default implementation will return the next component which is to the | |||
left of or above this one. | |||
/** Returns the component that should be given keyboard focus after the specified | |||
one when moving "forwards". | |||
This may return nullptr if there's no suitable candidate. | |||
The default implementation will return the next focusable component (as | |||
determined by FocusTraverser) that also wants keyboard focus, or nullptr if | |||
there is no suitable component. | |||
*/ | |||
virtual Component* getPreviousComponent (Component* current); | |||
Component* getNextComponent (Component* current) override; | |||
/** Returns the component that should receive focus be default within the given | |||
parent component. | |||
/** Returns the component that should be given keyboard focus after the specified | |||
one when moving "backwards". | |||
The default implementation will return the previous focusable component (as | |||
determined by FocusTraverser) that also wants keyboard focus, or nullptr if | |||
there is no suitable component. | |||
*/ | |||
Component* getPreviousComponent (Component* current) override; | |||
The default implementation will just return the foremost child component that | |||
wants focus. | |||
/** Returns all of the components that can receive keyboard focus within the given | |||
parent component in traversal order. | |||
This may return nullptr if there's no suitable candidate. | |||
The default implementation will return all focusable child components (as | |||
determined by FocusTraverser) that also wants keyboard focus. | |||
*/ | |||
virtual Component* getDefaultComponent (Component* parentComponent); | |||
std::vector<Component*> getAllComponents (Component* parentComponent) override; | |||
}; | |||
} // namespace juce |
@@ -150,6 +150,7 @@ public: | |||
{ | |||
ProxyComponent (Component& c) | |||
{ | |||
setAccessible (false); | |||
setWantsKeyboardFocus (false); | |||
setBounds (c.getBounds()); | |||
setTransform (c.getTransform()); | |||
@@ -459,4 +459,10 @@ void ConcertinaPanel::panelHeaderDoubleClicked (Component* component) | |||
setPanelSize (component, 0, true); | |||
} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> ConcertinaPanel::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||
} | |||
} // namespace juce |
@@ -119,6 +119,10 @@ public: | |||
ConcertinaPanel&, Component&) = 0; | |||
}; | |||
//============================================================================== | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
private: | |||
void resized() override; | |||
@@ -69,4 +69,10 @@ void GroupComponent::paint (Graphics& g) | |||
void GroupComponent::enablementChanged() { repaint(); } | |||
void GroupComponent::colourChanged() { repaint(); } | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> GroupComponent::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||
} | |||
} // namespace juce |
@@ -98,6 +98,8 @@ public: | |||
void enablementChanged() override; | |||
/** @internal */ | |||
void colourChanged() override; | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
private: | |||
String text; | |||
@@ -61,7 +61,7 @@ private: | |||
ScrollBar::ScrollBar (bool shouldBeVertical) : vertical (shouldBeVertical) | |||
{ | |||
setRepaintsOnMouseActivity (true); | |||
setFocusContainer (true); | |||
setFocusContainerType (FocusContainerType::keyboardFocusContainer); | |||
} | |||
ScrollBar::~ScrollBar() | |||
@@ -440,4 +440,46 @@ bool ScrollBar::getVisibility() const noexcept | |||
&& visibleRange.getLength() > 0.0); | |||
} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> ScrollBar::createAccessibilityHandler() | |||
{ | |||
class ScrollBarValueInterface : public AccessibilityRangedNumericValueInterface | |||
{ | |||
public: | |||
explicit ScrollBarValueInterface (ScrollBar& scrollBarToWrap) | |||
: scrollBar (scrollBarToWrap) | |||
{ | |||
} | |||
bool isReadOnly() const override { return false; } | |||
double getCurrentValue() const override | |||
{ | |||
return scrollBar.getCurrentRangeStart(); | |||
} | |||
void setValue (double newValue) override | |||
{ | |||
scrollBar.setCurrentRangeStart (newValue); | |||
} | |||
AccessibleValueRange getRange() const override | |||
{ | |||
if (scrollBar.getRangeLimit().isEmpty()) | |||
return {}; | |||
return { { scrollBar.getMinimumRangeLimit(), scrollBar.getMaximumRangeLimit() }, | |||
scrollBar.getSingleStepSize() }; | |||
} | |||
private: | |||
ScrollBar& scrollBar; | |||
}; | |||
return std::make_unique<AccessibilityHandler> (*this, | |||
AccessibilityRole::scrollBar, | |||
AccessibilityActions{}, | |||
AccessibilityHandler::Interfaces { std::make_unique<ScrollBarValueInterface> (*this) }); | |||
} | |||
} // namespace juce |
@@ -211,6 +211,11 @@ public: | |||
*/ | |||
void setSingleStepSize (double newSingleStepSize) noexcept; | |||
/** Returns the current step size. | |||
@see setSingleStepSize | |||
*/ | |||
double getSingleStepSize() const noexcept { return singleStepSize; } | |||
/** Moves the scrollbar by a number of single-steps. | |||
This will move the bar by a multiple of its single-step interval (as | |||
@@ -409,6 +414,8 @@ public: | |||
void parentHierarchyChanged() override; | |||
/** @internal */ | |||
void setVisible (bool) override; | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
private: | |||
//============================================================================== | |||
@@ -294,4 +294,10 @@ bool SidePanel::isMouseEventInThisOrChildren (Component* eventComponent) | |||
return false; | |||
} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> SidePanel::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||
} | |||
} // namespace juce |
@@ -195,6 +195,8 @@ public: | |||
void mouseDrag (const MouseEvent&) override; | |||
/** @internal */ | |||
void mouseUp (const MouseEvent&) override; | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
private: | |||
//============================================================================== | |||
@@ -202,7 +202,7 @@ TabbedButtonBar::TabbedButtonBar (Orientation orientationToUse) | |||
setInterceptsMouseClicks (false, true); | |||
behindFrontTab.reset (new BehindFrontTabComp (*this)); | |||
addAndMakeVisible (behindFrontTab.get()); | |||
setFocusContainer (true); | |||
setFocusContainerType (FocusContainerType::keyboardFocusContainer); | |||
} | |||
TabbedButtonBar::~TabbedButtonBar() | |||
@@ -574,4 +574,10 @@ void TabbedButtonBar::showExtraItemsMenu() | |||
void TabbedButtonBar::currentTabChanged (int, const String&) {} | |||
void TabbedButtonBar::popupMenuClickOnTab (int, const String&) {} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> TabbedButtonBar::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||
} | |||
} // namespace juce |
@@ -334,6 +334,8 @@ public: | |||
void resized() override; | |||
/** @internal */ | |||
void lookAndFeelChanged() override; | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
protected: | |||
//============================================================================== | |||
@@ -310,4 +310,10 @@ void TabbedComponent::changeCallback (int newCurrentTabIndex, const String& newT | |||
void TabbedComponent::currentTabChanged (int, const String&) {} | |||
void TabbedComponent::popupMenuClickOnTab (int, const String&) {} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> TabbedComponent::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group); | |||
} | |||
} // namespace juce |
@@ -197,6 +197,8 @@ public: | |||
void resized() override; | |||
/** @internal */ | |||
void lookAndFeelChanged() override; | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
protected: | |||
//============================================================================== | |||
@@ -293,4 +293,10 @@ void BurgerMenuComponent::lookAndFeelChanged() | |||
listBox.setRowHeight (roundToInt (getLookAndFeel().getPopupMenuFont().getHeight() * 2.0f)); | |||
} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> BurgerMenuComponent::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::menuBar); | |||
} | |||
} // namespace juce |
@@ -71,6 +71,8 @@ public: | |||
/** @internal */ | |||
void lookAndFeelChanged() override; | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
private: | |||
//============================================================================== | |||
@@ -26,11 +26,63 @@ | |||
namespace juce | |||
{ | |||
class MenuBarComponent::AccessibleItemComponent : public Component | |||
{ | |||
public: | |||
AccessibleItemComponent (MenuBarComponent& comp, const String& menuItemName) | |||
: owner (comp), | |||
name (menuItemName) | |||
{ | |||
setInterceptsMouseClicks (false, false); | |||
} | |||
const String& getName() const noexcept { return name; } | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override | |||
{ | |||
class ComponentHandler : public AccessibilityHandler | |||
{ | |||
public: | |||
explicit ComponentHandler (AccessibleItemComponent& item) | |||
: AccessibilityHandler (item, | |||
AccessibilityRole::menuItem, | |||
getAccessibilityActions (item)), | |||
itemComponent (item) | |||
{ | |||
} | |||
AccessibleState getCurrentState() const override | |||
{ | |||
auto state = AccessibilityHandler::getCurrentState().withSelectable(); | |||
return state.isFocused() ? state.withSelected() : state; | |||
} | |||
String getTitle() const override { return itemComponent.name; } | |||
private: | |||
static AccessibilityActions getAccessibilityActions (AccessibleItemComponent& item) | |||
{ | |||
auto showMenu = [&item] { item.owner.showMenu (item.owner.indexOfItemComponent (&item)); }; | |||
return AccessibilityActions().addAction (AccessibilityActionType::focus, | |||
[&item] { item.owner.setItemUnderMouse (item.owner.indexOfItemComponent (&item)); }) | |||
.addAction (AccessibilityActionType::press, showMenu) | |||
.addAction (AccessibilityActionType::showMenu, showMenu); | |||
} | |||
AccessibleItemComponent& itemComponent; | |||
}; | |||
return std::make_unique<ComponentHandler> (*this); | |||
} | |||
private: | |||
MenuBarComponent& owner; | |||
const String name; | |||
}; | |||
MenuBarComponent::MenuBarComponent (MenuBarModel* m) | |||
: model (nullptr), | |||
itemUnderMouse (-1), | |||
currentPopupIndex (-1), | |||
topLevelIndexClicked (0) | |||
{ | |||
setRepaintsOnMouseActivity (true); | |||
setWantsKeyboardFocus (false); | |||
@@ -70,77 +122,83 @@ void MenuBarComponent::setModel (MenuBarModel* const newModel) | |||
//============================================================================== | |||
void MenuBarComponent::paint (Graphics& g) | |||
{ | |||
const bool isMouseOverBar = currentPopupIndex >= 0 || itemUnderMouse >= 0 || isMouseOver(); | |||
const auto isMouseOverBar = (currentPopupIndex >= 0 || itemUnderMouse >= 0 || isMouseOver()); | |||
getLookAndFeel().drawMenuBarBackground (g, | |||
getWidth(), | |||
getHeight(), | |||
isMouseOverBar, | |||
*this); | |||
getLookAndFeel().drawMenuBarBackground (g, getWidth(), getHeight(), isMouseOverBar, *this); | |||
if (model != nullptr) | |||
if (model == nullptr) | |||
return; | |||
for (size_t i = 0; i < itemComponents.size(); ++i) | |||
{ | |||
for (int i = 0; i < menuNames.size(); ++i) | |||
{ | |||
Graphics::ScopedSaveState ss (g); | |||
g.setOrigin (xPositions [i], 0); | |||
g.reduceClipRegion (0, 0, xPositions[i + 1] - xPositions[i], getHeight()); | |||
getLookAndFeel().drawMenuBarItem (g, | |||
xPositions[i + 1] - xPositions[i], | |||
getHeight(), | |||
i, | |||
menuNames[i], | |||
i == itemUnderMouse, | |||
i == currentPopupIndex, | |||
isMouseOverBar, | |||
*this); | |||
} | |||
const auto& itemComponent = itemComponents[i]; | |||
const auto itemBounds = itemComponent->getBounds(); | |||
Graphics::ScopedSaveState ss (g); | |||
g.setOrigin (itemBounds.getX(), 0); | |||
g.reduceClipRegion (0, 0, itemBounds.getWidth(), itemBounds.getHeight()); | |||
getLookAndFeel().drawMenuBarItem (g, | |||
itemBounds.getWidth(), | |||
itemBounds.getHeight(), | |||
(int) i, | |||
itemComponent->getName(), | |||
(int) i == itemUnderMouse, | |||
(int) i == currentPopupIndex, | |||
isMouseOverBar, | |||
*this); | |||
} | |||
} | |||
void MenuBarComponent::resized() | |||
{ | |||
xPositions.clear(); | |||
int x = 0; | |||
xPositions.add (x); | |||
for (int i = 0; i < menuNames.size(); ++i) | |||
for (size_t i = 0; i < itemComponents.size(); ++i) | |||
{ | |||
x += getLookAndFeel().getMenuBarItemWidth (*this, i, menuNames[i]); | |||
xPositions.add (x); | |||
auto& itemComponent = itemComponents[i]; | |||
auto w = getLookAndFeel().getMenuBarItemWidth (*this, (int) i, itemComponent->getName()); | |||
itemComponent->setBounds (x, 0, w, getHeight()); | |||
x += w; | |||
} | |||
} | |||
int MenuBarComponent::getItemAt (Point<int> p) | |||
{ | |||
for (int i = 0; i < xPositions.size(); ++i) | |||
if (p.x >= xPositions[i] && p.x < xPositions[i + 1]) | |||
return reallyContains (p, true) ? i : -1; | |||
for (size_t i = 0; i < itemComponents.size(); ++i) | |||
if (itemComponents[i]->getBounds().contains (p) && reallyContains (p, true)) | |||
return (int) i; | |||
return -1; | |||
} | |||
void MenuBarComponent::repaintMenuItem (int index) | |||
{ | |||
if (isPositiveAndBelow (index, xPositions.size())) | |||
if (isPositiveAndBelow (index, (int) itemComponents.size())) | |||
{ | |||
const int x1 = xPositions [index]; | |||
const int x2 = xPositions [index + 1]; | |||
auto itemBounds = itemComponents[(size_t) index]->getBounds(); | |||
repaint (x1 - 2, 0, x2 - x1 + 4, getHeight()); | |||
repaint (itemBounds.getX() - 2, | |||
0, | |||
itemBounds.getWidth() + 4, | |||
itemBounds.getHeight()); | |||
} | |||
} | |||
void MenuBarComponent::setItemUnderMouse (const int index) | |||
void MenuBarComponent::setItemUnderMouse (int index) | |||
{ | |||
if (itemUnderMouse != index) | |||
{ | |||
repaintMenuItem (itemUnderMouse); | |||
itemUnderMouse = index; | |||
repaintMenuItem (itemUnderMouse); | |||
} | |||
if (itemUnderMouse == index) | |||
return; | |||
repaintMenuItem (itemUnderMouse); | |||
itemUnderMouse = index; | |||
repaintMenuItem (itemUnderMouse); | |||
if (isPositiveAndBelow (itemUnderMouse, (int) itemComponents.size())) | |||
if (auto* handler = itemComponents[(size_t) itemUnderMouse]->getAccessibilityHandler()) | |||
handler->grabFocus(); | |||
} | |||
void MenuBarComponent::setOpenItem (int index) | |||
@@ -156,7 +214,7 @@ void MenuBarComponent::setOpenItem (int index) | |||
currentPopupIndex = index; | |||
repaintMenuItem (currentPopupIndex); | |||
Desktop& desktop = Desktop::getInstance(); | |||
auto& desktop = Desktop::getInstance(); | |||
if (index >= 0) | |||
desktop.addGlobalMouseListener (this); | |||
@@ -180,30 +238,24 @@ void MenuBarComponent::showMenu (int index) | |||
setOpenItem (index); | |||
setItemUnderMouse (index); | |||
if (index >= 0) | |||
if (isPositiveAndBelow (index, (int) itemComponents.size())) | |||
{ | |||
PopupMenu m (model->getMenuForIndex (itemUnderMouse, | |||
menuNames [itemUnderMouse])); | |||
const auto& itemComponent = itemComponents[(size_t) index]; | |||
auto m = model->getMenuForIndex (itemUnderMouse, itemComponent->getName()); | |||
if (m.lookAndFeel == nullptr) | |||
m.setLookAndFeel (&getLookAndFeel()); | |||
const Rectangle<int> itemPos (xPositions [index], 0, xPositions [index + 1] - xPositions [index], getHeight()); | |||
auto itemBounds = itemComponent->getBounds(); | |||
m.showMenuAsync (PopupMenu::Options().withTargetComponent (this) | |||
.withTargetScreenArea (localAreaToGlobal (itemPos)) | |||
.withMinimumWidth (itemPos.getWidth()), | |||
ModalCallbackFunction::forComponent (menuBarMenuDismissedCallback, this, index)); | |||
.withTargetScreenArea (localAreaToGlobal (itemBounds)) | |||
.withMinimumWidth (itemBounds.getWidth()), | |||
[this, index] (int result) { menuDismissed (index, result); }); | |||
} | |||
} | |||
} | |||
void MenuBarComponent::menuBarMenuDismissedCallback (int result, MenuBarComponent* bar, int topLevelIndex) | |||
{ | |||
if (bar != nullptr) | |||
bar->menuDismissed (topLevelIndex, result); | |||
} | |||
void MenuBarComponent::menuDismissed (int topLevelIndex, int itemId) | |||
{ | |||
topLevelIndexClicked = topLevelIndex; | |||
@@ -212,8 +264,7 @@ void MenuBarComponent::menuDismissed (int topLevelIndex, int itemId) | |||
void MenuBarComponent::handleCommandMessage (int commandId) | |||
{ | |||
const Point<int> mousePos (getMouseXYRelative()); | |||
updateItemUnderMouse (mousePos); | |||
updateItemUnderMouse (getMouseXYRelative()); | |||
if (currentPopupIndex == topLevelIndexClicked) | |||
setOpenItem (-1); | |||
@@ -239,8 +290,7 @@ void MenuBarComponent::mouseDown (const MouseEvent& e) | |||
{ | |||
if (currentPopupIndex < 0) | |||
{ | |||
const MouseEvent e2 (e.getEventRelativeTo (this)); | |||
updateItemUnderMouse (e2.getPosition()); | |||
updateItemUnderMouse (e.getEventRelativeTo (this).getPosition()); | |||
currentPopupIndex = -2; | |||
showMenu (itemUnderMouse); | |||
@@ -249,8 +299,7 @@ void MenuBarComponent::mouseDown (const MouseEvent& e) | |||
void MenuBarComponent::mouseDrag (const MouseEvent& e) | |||
{ | |||
const MouseEvent e2 (e.getEventRelativeTo (this)); | |||
const int item = getItemAt (e2.getPosition()); | |||
const auto item = getItemAt (e.getEventRelativeTo (this).getPosition()); | |||
if (item >= 0) | |||
showMenu (item); | |||
@@ -258,7 +307,7 @@ void MenuBarComponent::mouseDrag (const MouseEvent& e) | |||
void MenuBarComponent::mouseUp (const MouseEvent& e) | |||
{ | |||
const MouseEvent e2 (e.getEventRelativeTo (this)); | |||
const auto e2 = e.getEventRelativeTo (this); | |||
updateItemUnderMouse (e2.getPosition()); | |||
@@ -271,13 +320,13 @@ void MenuBarComponent::mouseUp (const MouseEvent& e) | |||
void MenuBarComponent::mouseMove (const MouseEvent& e) | |||
{ | |||
const MouseEvent e2 (e.getEventRelativeTo (this)); | |||
const auto e2 = e.getEventRelativeTo (this); | |||
if (lastMousePos != e2.getPosition()) | |||
{ | |||
if (currentPopupIndex >= 0) | |||
{ | |||
const int item = getItemAt (e2.getPosition()); | |||
const auto item = getItemAt (e2.getPosition()); | |||
if (item >= 0) | |||
showMenu (item); | |||
@@ -293,11 +342,11 @@ void MenuBarComponent::mouseMove (const MouseEvent& e) | |||
bool MenuBarComponent::keyPressed (const KeyPress& key) | |||
{ | |||
const int numMenus = menuNames.size(); | |||
const auto numMenus = (int) itemComponents.size(); | |||
if (numMenus > 0) | |||
{ | |||
const int currentIndex = jlimit (0, numMenus - 1, currentPopupIndex); | |||
const auto currentIndex = jlimit (0, numMenus - 1, currentPopupIndex); | |||
if (key.isKeyCode (KeyPress::leftKey)) | |||
{ | |||
@@ -315,34 +364,69 @@ bool MenuBarComponent::keyPressed (const KeyPress& key) | |||
return false; | |||
} | |||
void MenuBarComponent::menuBarItemsChanged (MenuBarModel* /*menuBarModel*/) | |||
void MenuBarComponent::menuBarItemsChanged (MenuBarModel*) | |||
{ | |||
StringArray newNames; | |||
if (model != nullptr) | |||
newNames = model->getMenuBarNames(); | |||
if (newNames != menuNames) | |||
auto itemsHaveChanged = [this, &newNames] | |||
{ | |||
menuNames = newNames; | |||
if ((int) itemComponents.size() != newNames.size()) | |||
return true; | |||
for (size_t i = 0; i < itemComponents.size(); ++i) | |||
if (itemComponents[i]->getName() != newNames[(int) i]) | |||
return true; | |||
return false; | |||
}(); | |||
if (itemsHaveChanged) | |||
{ | |||
updateItemComponents (newNames); | |||
repaint(); | |||
resized(); | |||
} | |||
} | |||
void MenuBarComponent::menuCommandInvoked (MenuBarModel* /*menuBarModel*/, | |||
const ApplicationCommandTarget::InvocationInfo& info) | |||
void MenuBarComponent::updateItemComponents (const StringArray& menuNames) | |||
{ | |||
itemComponents.clear(); | |||
for (const auto& name : menuNames) | |||
{ | |||
itemComponents.push_back (std::make_unique<AccessibleItemComponent> (*this, name)); | |||
addAndMakeVisible (*itemComponents.back()); | |||
} | |||
} | |||
int MenuBarComponent::indexOfItemComponent (AccessibleItemComponent* itemComponent) const | |||
{ | |||
const auto iter = std::find_if (itemComponents.cbegin(), itemComponents.cend(), | |||
[itemComponent] (const std::unique_ptr<AccessibleItemComponent>& c) { return c.get() == itemComponent; }); | |||
if (iter != itemComponents.cend()) | |||
return (int) std::distance (itemComponents.cbegin(), iter); | |||
jassertfalse; | |||
return -1; | |||
} | |||
void MenuBarComponent::menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo& info) | |||
{ | |||
if (model == nullptr || (info.commandFlags & ApplicationCommandInfo::dontTriggerVisualFeedback) != 0) | |||
return; | |||
for (int i = 0; i < menuNames.size(); ++i) | |||
for (size_t i = 0; i < itemComponents.size(); ++i) | |||
{ | |||
const PopupMenu menu (model->getMenuForIndex (i, menuNames [i])); | |||
const auto menu = model->getMenuForIndex ((int) i, itemComponents[i]->getName()); | |||
if (menu.containsCommandItem (info.commandID)) | |||
{ | |||
setItemUnderMouse (i); | |||
setItemUnderMouse ((int) i); | |||
startTimer (200); | |||
break; | |||
} | |||
@@ -355,4 +439,20 @@ void MenuBarComponent::timerCallback() | |||
updateItemUnderMouse (getMouseXYRelative()); | |||
} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> MenuBarComponent::createAccessibilityHandler() | |||
{ | |||
struct MenuBarComponentAccessibilityHandler : public AccessibilityHandler | |||
{ | |||
explicit MenuBarComponentAccessibilityHandler (MenuBarComponent& menuBarComponent) | |||
: AccessibilityHandler (menuBarComponent, AccessibilityRole::menuBar) | |||
{ | |||
} | |||
AccessibleState getCurrentState() const override { return AccessibleState().withIgnored(); } | |||
}; | |||
return std::make_unique<MenuBarComponentAccessibilityHandler> (*this); | |||
} | |||
} // namespace juce |
@@ -95,24 +95,32 @@ public: | |||
void menuBarItemsChanged (MenuBarModel*) override; | |||
/** @internal */ | |||
void menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo&) override; | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
private: | |||
//============================================================================== | |||
MenuBarModel* model; | |||
class AccessibleItemComponent; | |||
StringArray menuNames; | |||
Array<int> xPositions; | |||
Point<int> lastMousePos; | |||
int itemUnderMouse, currentPopupIndex, topLevelIndexClicked; | |||
//============================================================================== | |||
void timerCallback() override; | |||
int getItemAt (Point<int>); | |||
void setItemUnderMouse (int index); | |||
void setOpenItem (int index); | |||
void setItemUnderMouse (int); | |||
void setOpenItem (int); | |||
void updateItemUnderMouse (Point<int>); | |||
void timerCallback() override; | |||
void repaintMenuItem (int index); | |||
void menuDismissed (int topLevelIndex, int itemId); | |||
static void menuBarMenuDismissedCallback (int, MenuBarComponent*, int); | |||
void repaintMenuItem (int); | |||
void menuDismissed (int, int); | |||
void updateItemComponents (const StringArray&); | |||
int indexOfItemComponent (AccessibleItemComponent*) const; | |||
//============================================================================== | |||
MenuBarModel* model = nullptr; | |||
std::vector<std::unique_ptr<AccessibleItemComponent>> itemComponents; | |||
Point<int> lastMousePos; | |||
int itemUnderMouse = -1, currentPopupIndex = -1, topLevelIndexClicked = 0; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuBarComponent) | |||
}; | |||
@@ -84,7 +84,7 @@ struct ItemComponent : public Component | |||
ItemComponent (const PopupMenu::Item& i, | |||
const PopupMenu::Options& o, | |||
MenuWindow& parent) | |||
: item (i), options (o), customComp (i.customComponent) | |||
: item (i), parentWindow (parent), options (o), customComp (i.customComponent) | |||
{ | |||
if (item.isSectionHeader) | |||
customComp = *new HeaderItemComponent (item.text, options); | |||
@@ -156,13 +156,99 @@ struct ItemComponent : public Component | |||
if (customComp != nullptr) | |||
customComp->setHighlighted (shouldBeHighlighted); | |||
if (isHighlighted) | |||
if (auto* handler = getAccessibilityHandler()) | |||
handler->grabFocus(); | |||
repaint(); | |||
} | |||
} | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override | |||
{ | |||
return item.isSeparator ? nullptr : std::make_unique<ItemAccessibilityHandler> (*this); | |||
} | |||
PopupMenu::Item item; | |||
private: | |||
//============================================================================== | |||
class ItemAccessibilityHandler : public AccessibilityHandler | |||
{ | |||
public: | |||
explicit ItemAccessibilityHandler (ItemComponent& itemComponentToWrap) | |||
: AccessibilityHandler (itemComponentToWrap, | |||
AccessibilityRole::menuItem, | |||
getAccessibilityActions (*this, itemComponentToWrap)), | |||
itemComponent (itemComponentToWrap) | |||
{ | |||
} | |||
String getTitle() const override | |||
{ | |||
return itemComponent.item.text; | |||
} | |||
AccessibleState getCurrentState() const override | |||
{ | |||
auto state = AccessibilityHandler::getCurrentState().withSelectable() | |||
.withAccessibleOffscreen(); | |||
if (hasActiveSubMenu (itemComponent.item)) | |||
{ | |||
state = itemComponent.parentWindow.isSubMenuVisible() ? state.withExpandable().withExpanded() | |||
: state.withExpandable().withCollapsed(); | |||
} | |||
return state.isFocused() ? state.withSelected() : state; | |||
} | |||
private: | |||
static AccessibilityActions getAccessibilityActions (ItemAccessibilityHandler& handler, | |||
ItemComponent& item) | |||
{ | |||
auto onFocus = [&item] | |||
{ | |||
item.parentWindow.disableTimerUntilMouseMoves(); | |||
item.parentWindow.ensureItemComponentIsVisible (item, -1); | |||
item.parentWindow.setCurrentlyHighlightedChild (&item); | |||
}; | |||
auto onPress = [&item] | |||
{ | |||
item.parentWindow.setCurrentlyHighlightedChild (&item); | |||
item.parentWindow.triggerCurrentlyHighlightedItem(); | |||
}; | |||
auto onToggle = [&handler, &item, onFocus] | |||
{ | |||
if (handler.getCurrentState().isSelected()) | |||
item.parentWindow.setCurrentlyHighlightedChild (nullptr); | |||
else | |||
onFocus(); | |||
}; | |||
auto actions = AccessibilityActions().addAction (AccessibilityActionType::focus, std::move (onFocus)) | |||
.addAction (AccessibilityActionType::press, std::move (onPress)) | |||
.addAction (AccessibilityActionType::toggle, std::move (onToggle)); | |||
if (hasActiveSubMenu (item.item)) | |||
actions.addAction (AccessibilityActionType::showMenu, [&item] | |||
{ | |||
item.parentWindow.showSubMenuFor (&item); | |||
if (auto* subMenu = item.parentWindow.activeSubMenu.get()) | |||
subMenu->setCurrentlyHighlightedChild (subMenu->items.getFirst()); | |||
}); | |||
return actions; | |||
} | |||
ItemComponent& itemComponent; | |||
}; | |||
//============================================================================== | |||
MenuWindow& parentWindow; | |||
const PopupMenu::Options& options; | |||
// NB: we use a copy of the one from the item info in case we're using our own section comp | |||
ReferenceCountedObjectPtr<CustomComponent> customComp; | |||
@@ -223,6 +309,7 @@ struct MenuWindow : public Component | |||
setWantsKeyboardFocus (false); | |||
setMouseClickGrabsKeyboardFocus (false); | |||
setAlwaysOnTop (true); | |||
setFocusContainerType (FocusContainerType::focusContainer); | |||
setLookAndFeel (parent != nullptr ? &(parent->getLookAndFeel()) | |||
: menu.lookAndFeel.get()); | |||
@@ -275,11 +362,19 @@ struct MenuWindow : public Component | |||
if (auto visibleID = options.getItemThatMustBeVisible()) | |||
{ | |||
auto targetPosition = parentComponent != nullptr ? parentComponent->getLocalPoint (nullptr, targetArea.getTopLeft()) | |||
: targetArea.getTopLeft(); | |||
for (auto* item : items) | |||
{ | |||
if (item->item.itemID == visibleID) | |||
{ | |||
auto targetPosition = parentComponent != nullptr ? parentComponent->getLocalPoint (nullptr, targetArea.getTopLeft()) | |||
: targetArea.getTopLeft(); | |||
auto y = targetPosition.getY() - windowPos.getY(); | |||
ensureItemComponentIsVisible (*item, isPositiveAndBelow (y, windowPos.getHeight()) ? y : -1); | |||
auto y = targetPosition.getY() - windowPos.getY(); | |||
ensureItemIsVisible (visibleID, isPositiveAndBelow (y, windowPos.getHeight()) ? y : -1); | |||
break; | |||
} | |||
} | |||
} | |||
resizeToBestWindowPos(); | |||
@@ -887,47 +982,36 @@ struct MenuWindow : public Component | |||
return correctColumnWidths (maxMenuW); | |||
} | |||
void ensureItemIsVisible (const int itemID, int wantedY) | |||
void ensureItemComponentIsVisible (const ItemComponent& itemComp, int wantedY) | |||
{ | |||
jassert (itemID != 0); | |||
for (int i = items.size(); --i >= 0;) | |||
if (windowPos.getHeight() > PopupMenuSettings::scrollZone * 4) | |||
{ | |||
if (auto* m = items.getUnchecked (i)) | |||
{ | |||
if (m->item.itemID == itemID | |||
&& windowPos.getHeight() > PopupMenuSettings::scrollZone * 4) | |||
{ | |||
auto currentY = m->getY(); | |||
auto currentY = itemComp.getY(); | |||
if (wantedY > 0 || currentY < 0 || m->getBottom() > windowPos.getHeight()) | |||
{ | |||
if (wantedY < 0) | |||
wantedY = jlimit (PopupMenuSettings::scrollZone, | |||
jmax (PopupMenuSettings::scrollZone, | |||
windowPos.getHeight() - (PopupMenuSettings::scrollZone + m->getHeight())), | |||
currentY); | |||
auto parentArea = getParentArea (windowPos.getPosition(), parentComponent) / scaleFactor; | |||
auto deltaY = wantedY - currentY; | |||
if (wantedY > 0 || currentY < 0 || itemComp.getBottom() > windowPos.getHeight()) | |||
{ | |||
if (wantedY < 0) | |||
wantedY = jlimit (PopupMenuSettings::scrollZone, | |||
jmax (PopupMenuSettings::scrollZone, | |||
windowPos.getHeight() - (PopupMenuSettings::scrollZone + itemComp.getHeight())), | |||
currentY); | |||
windowPos.setSize (jmin (windowPos.getWidth(), parentArea.getWidth()), | |||
jmin (windowPos.getHeight(), parentArea.getHeight())); | |||
auto parentArea = getParentArea (windowPos.getPosition(), parentComponent) / scaleFactor; | |||
auto deltaY = wantedY - currentY; | |||
auto newY = jlimit (parentArea.getY(), | |||
parentArea.getBottom() - windowPos.getHeight(), | |||
windowPos.getY() + deltaY); | |||
windowPos.setSize (jmin (windowPos.getWidth(), parentArea.getWidth()), | |||
jmin (windowPos.getHeight(), parentArea.getHeight())); | |||
deltaY -= newY - windowPos.getY(); | |||
auto newY = jlimit (parentArea.getY(), | |||
parentArea.getBottom() - windowPos.getHeight(), | |||
windowPos.getY() + deltaY); | |||
childYOffset -= deltaY; | |||
windowPos.setPosition (windowPos.getX(), newY); | |||
deltaY -= newY - windowPos.getY(); | |||
updateYPositions(); | |||
} | |||
childYOffset -= deltaY; | |||
windowPos.setPosition (windowPos.getX(), newY); | |||
break; | |||
} | |||
updateYPositions(); | |||
} | |||
} | |||
} | |||
@@ -1016,6 +1100,9 @@ struct MenuWindow : public Component | |||
void setCurrentlyHighlightedChild (ItemComponent* child) | |||
{ | |||
if (currentChild == child) | |||
return; | |||
if (currentChild != nullptr) | |||
currentChild->setHighlighted (false); | |||
@@ -1026,6 +1113,9 @@ struct MenuWindow : public Component | |||
currentChild->setHighlighted (true); | |||
timeEnteredCurrentChildComp = Time::getApproximateMillisecondCounter(); | |||
} | |||
if (auto* handler = getAccessibilityHandler()) | |||
handler->notifyAccessibilityEvent (AccessibilityEvent::rowSelectionChanged); | |||
} | |||
bool isSubMenuVisible() const noexcept { return activeSubMenu != nullptr && activeSubMenu->isVisible(); } | |||
@@ -1119,6 +1209,19 @@ struct MenuWindow : public Component | |||
bool isTopScrollZoneActive() const noexcept { return canScroll() && childYOffset > 0; } | |||
bool isBottomScrollZoneActive() const noexcept { return canScroll() && childYOffset < contentHeight - windowPos.getHeight(); } | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, | |||
AccessibilityRole::popupMenu, | |||
AccessibilityActions().addAction (AccessibilityActionType::focus, [this] | |||
{ | |||
if (currentChild != nullptr) | |||
if (auto* handler = currentChild->getAccessibilityHandler()) | |||
handler->grabFocus(); | |||
})); | |||
} | |||
//============================================================================== | |||
MenuWindow* parent; | |||
const Options options; | |||
@@ -1925,6 +2028,9 @@ int PopupMenu::showWithOptionalCallback (const Options& options, | |||
window->toFront (false); // need to do this after making it modal, or it could | |||
// be stuck behind other comps that are already modal.. | |||
if (auto* handler = window->getAccessibilityHandler()) | |||
handler->grabFocus(); | |||
#if JUCE_MODAL_LOOPS_PERMITTED | |||
if (userCallback == nullptr && canBeModal) | |||
return window->runModalLoop(); | |||
@@ -156,6 +156,10 @@ public: | |||
const Rectangle<float>& body) = 0; | |||
}; | |||
//============================================================================== | |||
/** @internal */ | |||
void paint (Graphics&) override; | |||
protected: | |||
//============================================================================== | |||
/** Subclasses should override this to return the size of the content they | |||
@@ -170,10 +174,6 @@ protected: | |||
*/ | |||
virtual void paintContent (Graphics& g, int width, int height) = 0; | |||
public: | |||
/** @internal */ | |||
void paint (Graphics&) override; | |||
private: | |||
Rectangle<int> content; | |||
Point<int> arrowTip; | |||
@@ -33,6 +33,7 @@ public: | |||
: target (comp), shadow (ds) | |||
{ | |||
setVisible (true); | |||
setAccessible (false); | |||
setInterceptsMouseClicks (false, false); | |||
if (comp->isOnDesktop()) | |||
@@ -188,6 +188,12 @@ void JUCESplashScreen::mouseUp (const MouseEvent&) | |||
juceWebsite.launchInDefaultBrowser(); | |||
} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> JUCESplashScreen::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::splashScreen); | |||
} | |||
// END SECTION A | |||
} // namespace juce |
@@ -55,6 +55,10 @@ public: | |||
static std::unique_ptr<Drawable> getSplashScreenLogo(); | |||
//============================================================================== | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
private: | |||
void paint (Graphics&) override; | |||
void timerCallback() override; | |||
@@ -0,0 +1,270 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
static bool isStartingUpOrShuttingDown() | |||
{ | |||
if (auto* app = JUCEApplicationBase::getInstance()) | |||
if (app->isInitialising()) | |||
return true; | |||
if (auto* mm = MessageManager::getInstanceWithoutCreating()) | |||
if (mm->hasStopMessageBeenSent()) | |||
return true; | |||
return false; | |||
} | |||
static bool isHandlerValid (const AccessibilityHandler& handler) | |||
{ | |||
if (auto* provider = handler.getNativeImplementation()) | |||
return provider->isElementValid(); | |||
return false; | |||
} | |||
//============================================================================== | |||
class AccessibilityHandler::AccessibilityNativeImpl | |||
{ | |||
public: | |||
explicit AccessibilityNativeImpl (AccessibilityHandler& owner) | |||
: accessibilityElement (new AccessibilityNativeHandle (owner)) | |||
{ | |||
++providerCount; | |||
} | |||
~AccessibilityNativeImpl() | |||
{ | |||
accessibilityElement->invalidateElement(); | |||
if (auto* wrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) | |||
{ | |||
ComSmartPtr<IRawElementProviderSimple> provider; | |||
accessibilityElement->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress())); | |||
wrapper->disconnectProvider (provider); | |||
if (--providerCount == 0) | |||
wrapper->disconnectAllProviders(); | |||
} | |||
} | |||
//============================================================================== | |||
ComSmartPtr<AccessibilityNativeHandle> accessibilityElement; | |||
static int providerCount; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityNativeImpl) | |||
}; | |||
int AccessibilityHandler::AccessibilityNativeImpl::providerCount = 0; | |||
//============================================================================== | |||
AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const | |||
{ | |||
return nativeImpl->accessibilityElement; | |||
} | |||
template <typename Callback> | |||
void getProviderWithCheckedWrapper (const AccessibilityHandler& handler, Callback&& callback) | |||
{ | |||
if (isStartingUpOrShuttingDown() || ! isHandlerValid (handler)) | |||
return; | |||
if (auto* wrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) | |||
{ | |||
if (! wrapper->clientsAreListening()) | |||
return; | |||
ComSmartPtr<IRawElementProviderSimple> provider; | |||
handler.getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress())); | |||
callback (wrapper, provider); | |||
} | |||
} | |||
void sendAccessibilityAutomationEvent (const AccessibilityHandler& handler, EVENTID event) | |||
{ | |||
jassert (event != EVENTID{}); | |||
getProviderWithCheckedWrapper (handler, [event] (WindowsUIAWrapper* wrapper, ComSmartPtr<IRawElementProviderSimple>& provider) | |||
{ | |||
wrapper->raiseAutomationEvent (provider, event); | |||
}); | |||
} | |||
void sendAccessibilityPropertyChangedEvent (const AccessibilityHandler& handler, PROPERTYID property, VARIANT newValue) | |||
{ | |||
jassert (property != PROPERTYID{}); | |||
getProviderWithCheckedWrapper (handler, [property, newValue] (WindowsUIAWrapper* wrapper, ComSmartPtr<IRawElementProviderSimple>& provider) | |||
{ | |||
VARIANT oldValue; | |||
VariantHelpers::clear (&oldValue); | |||
wrapper->raiseAutomationPropertyChangedEvent (provider, property, oldValue, newValue); | |||
}); | |||
} | |||
void notifyAccessibilityEventInternal (const AccessibilityHandler& handler, InternalAccessibilityEvent eventType) | |||
{ | |||
auto event = [eventType]() -> EVENTID | |||
{ | |||
switch (eventType) | |||
{ | |||
case InternalAccessibilityEvent::elementCreated: | |||
case InternalAccessibilityEvent::elementDestroyed: return UIA_StructureChangedEventId; | |||
case InternalAccessibilityEvent::focusChanged: return UIA_AutomationFocusChangedEventId; | |||
case InternalAccessibilityEvent::windowOpened: return UIA_Window_WindowOpenedEventId; | |||
case InternalAccessibilityEvent::windowClosed: return UIA_Window_WindowClosedEventId; | |||
} | |||
return {}; | |||
}(); | |||
if (event != EVENTID{}) | |||
sendAccessibilityAutomationEvent (handler, event); | |||
} | |||
void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent eventType) const | |||
{ | |||
auto event = [eventType] () -> EVENTID | |||
{ | |||
switch (eventType) | |||
{ | |||
case AccessibilityEvent::textSelectionChanged: return UIA_Text_TextSelectionChangedEventId; | |||
case AccessibilityEvent::textChanged: return UIA_Text_TextChangedEventId; | |||
case AccessibilityEvent::structureChanged: return UIA_StructureChangedEventId; | |||
case AccessibilityEvent::rowSelectionChanged: return UIA_SelectionItem_ElementSelectedEventId; | |||
case AccessibilityEvent::valueChanged: break; | |||
} | |||
return {}; | |||
}(); | |||
if (event != EVENTID{}) | |||
sendAccessibilityAutomationEvent (*this, event); | |||
} | |||
struct SpVoiceWrapper : public DeletedAtShutdown | |||
{ | |||
SpVoiceWrapper() | |||
{ | |||
auto hr = voice.CoCreateInstance (CLSID_SpVoice); | |||
jassert (SUCCEEDED (hr)); | |||
ignoreUnused (hr); | |||
} | |||
~SpVoiceWrapper() override | |||
{ | |||
clearSingletonInstance(); | |||
} | |||
ComSmartPtr<ISpVoice> voice; | |||
JUCE_DECLARE_SINGLETON (SpVoiceWrapper, false) | |||
}; | |||
JUCE_IMPLEMENT_SINGLETON (SpVoiceWrapper) | |||
void AccessibilityHandler::postAnnouncement (const String& announcementString, AnnouncementPriority priority) | |||
{ | |||
if (auto* sharedVoice = SpVoiceWrapper::getInstance()) | |||
{ | |||
auto voicePriority = [priority] | |||
{ | |||
switch (priority) | |||
{ | |||
case AnnouncementPriority::low: return SPVPRI_OVER; | |||
case AnnouncementPriority::medium: return SPVPRI_NORMAL; | |||
case AnnouncementPriority::high: return SPVPRI_ALERT; | |||
} | |||
jassertfalse; | |||
return SPVPRI_OVER; | |||
}(); | |||
sharedVoice->voice->SetPriority (voicePriority); | |||
sharedVoice->voice->Speak (announcementString.toWideCharPointer(), SPF_ASYNC, nullptr); | |||
} | |||
} | |||
AccessibilityHandler::AccessibilityNativeImpl* AccessibilityHandler::createNativeImpl (AccessibilityHandler& handler) | |||
{ | |||
return new AccessibilityHandler::AccessibilityNativeImpl (handler); | |||
} | |||
void AccessibilityHandler::DestroyNativeImpl::operator() (AccessibilityHandler::AccessibilityNativeImpl* impl) const noexcept | |||
{ | |||
delete impl; | |||
} | |||
//============================================================================== | |||
namespace WindowsAccessibility | |||
{ | |||
void initialiseUIAWrapper() | |||
{ | |||
WindowsUIAWrapper::getInstance(); | |||
} | |||
long getUiaRootObjectId() | |||
{ | |||
return static_cast<long> (UiaRootObjectId); | |||
} | |||
bool handleWmGetObject (AccessibilityHandler* handler, WPARAM wParam, LPARAM lParam, LRESULT* res) | |||
{ | |||
if (isStartingUpOrShuttingDown() || (handler == nullptr || ! isHandlerValid (*handler))) | |||
return false; | |||
if (auto* wrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) | |||
{ | |||
ComSmartPtr<IRawElementProviderSimple> provider; | |||
handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress())); | |||
if (! wrapper->isProviderDisconnecting (provider)) | |||
*res = wrapper->returnRawElementProvider ((HWND) handler->getComponent().getWindowHandle(), wParam, lParam, provider); | |||
return true; | |||
} | |||
return false; | |||
} | |||
void revokeUIAMapEntriesForWindow (HWND hwnd) | |||
{ | |||
if (auto* wrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) | |||
wrapper->returnRawElementProvider (hwnd, 0, 0, nullptr); | |||
} | |||
} | |||
JUCE_IMPLEMENT_SINGLETON (WindowsUIAWrapper) | |||
} // namespace juce |
@@ -0,0 +1,558 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
int AccessibilityNativeHandle::idCounter = 0; | |||
//============================================================================== | |||
static String getAutomationId (const AccessibilityHandler& handler) | |||
{ | |||
auto result = handler.getTitle(); | |||
auto* parentComponent = handler.getComponent().getParentComponent(); | |||
while (parentComponent != nullptr) | |||
{ | |||
if (auto* parentHandler = parentComponent->getAccessibilityHandler()) | |||
{ | |||
auto parentTitle = parentHandler->getTitle(); | |||
result << "." << (parentTitle.isNotEmpty() ? parentTitle : "<empty>"); | |||
} | |||
parentComponent = parentComponent->getParentComponent(); | |||
} | |||
return result; | |||
} | |||
static long roleToControlTypeId (AccessibilityRole roleType) | |||
{ | |||
switch (roleType) | |||
{ | |||
case AccessibilityRole::button: return UIA_ButtonControlTypeId; | |||
case AccessibilityRole::toggleButton: return UIA_CheckBoxControlTypeId; | |||
case AccessibilityRole::radioButton: return UIA_RadioButtonControlTypeId; | |||
case AccessibilityRole::comboBox: return UIA_ComboBoxControlTypeId; | |||
case AccessibilityRole::image: return UIA_ImageControlTypeId; | |||
case AccessibilityRole::slider: return UIA_SliderControlTypeId; | |||
case AccessibilityRole::staticText: return UIA_TextControlTypeId; | |||
case AccessibilityRole::editableText: return UIA_EditControlTypeId; | |||
case AccessibilityRole::menuItem: return UIA_MenuItemControlTypeId; | |||
case AccessibilityRole::menuBar: return UIA_MenuBarControlTypeId; | |||
case AccessibilityRole::popupMenu: return UIA_WindowControlTypeId; | |||
case AccessibilityRole::table: return UIA_TableControlTypeId; | |||
case AccessibilityRole::tableHeader: return UIA_HeaderControlTypeId; | |||
case AccessibilityRole::column: return UIA_HeaderItemControlTypeId; | |||
case AccessibilityRole::row: return UIA_HeaderItemControlTypeId; | |||
case AccessibilityRole::cell: return UIA_DataItemControlTypeId; | |||
case AccessibilityRole::hyperlink: return UIA_HyperlinkControlTypeId; | |||
case AccessibilityRole::list: return UIA_ListControlTypeId; | |||
case AccessibilityRole::listItem: return UIA_ListItemControlTypeId; | |||
case AccessibilityRole::tree: return UIA_TreeControlTypeId; | |||
case AccessibilityRole::treeItem: return UIA_TreeItemControlTypeId; | |||
case AccessibilityRole::progressBar: return UIA_ProgressBarControlTypeId; | |||
case AccessibilityRole::group: return UIA_GroupControlTypeId; | |||
case AccessibilityRole::dialogWindow: return UIA_WindowControlTypeId; | |||
case AccessibilityRole::window: return UIA_WindowControlTypeId; | |||
case AccessibilityRole::scrollBar: return UIA_ScrollBarControlTypeId; | |||
case AccessibilityRole::tooltip: return UIA_ToolTipControlTypeId; | |||
case AccessibilityRole::splashScreen: return UIA_WindowControlTypeId; | |||
case AccessibilityRole::ignored: return UIA_CustomControlTypeId; | |||
case AccessibilityRole::unspecified: return UIA_CustomControlTypeId; | |||
}; | |||
return UIA_CustomControlTypeId; | |||
} | |||
static bool isEditableText (const AccessibilityHandler& handler) | |||
{ | |||
return handler.getRole() == AccessibilityRole::editableText | |||
&& handler.getTextInterface() != nullptr; | |||
} | |||
//============================================================================== | |||
AccessibilityNativeHandle::AccessibilityNativeHandle (AccessibilityHandler& handler) | |||
: ComBaseClassHelper (0), | |||
accessibilityHandler (handler) | |||
{ | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT AccessibilityNativeHandle::QueryInterface (REFIID refId, void** result) | |||
{ | |||
*result = nullptr; | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
if ((refId == __uuidof (IRawElementProviderFragmentRoot) && ! isFragmentRoot())) | |||
return E_NOINTERFACE; | |||
return ComBaseClassHelper::QueryInterface (refId, result); | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT AccessibilityNativeHandle::get_HostRawElementProvider (IRawElementProviderSimple** pRetVal) | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
if (isFragmentRoot()) | |||
if (auto* wrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) | |||
return wrapper->hostProviderFromHwnd ((HWND) accessibilityHandler.getComponent().getWindowHandle(), pRetVal); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT AccessibilityNativeHandle::get_ProviderOptions (ProviderOptions* options) | |||
{ | |||
if (options == nullptr) | |||
return E_INVALIDARG; | |||
*options = ProviderOptions_ServerSideProvider | ProviderOptions_UseComThreading; | |||
return S_OK; | |||
} | |||
JUCE_COMRESULT AccessibilityNativeHandle::GetPatternProvider (PATTERNID pId, IUnknown** pRetVal) | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
*pRetVal = [&]() -> IUnknown* | |||
{ | |||
const auto role = accessibilityHandler.getRole(); | |||
const auto fragmentRoot = isFragmentRoot(); | |||
switch (pId) | |||
{ | |||
case UIA_WindowPatternId: | |||
{ | |||
if (fragmentRoot) | |||
return new UIAWindowProvider (this); | |||
break; | |||
} | |||
case UIA_TransformPatternId: | |||
{ | |||
if (fragmentRoot) | |||
return new UIATransformProvider (this); | |||
break; | |||
} | |||
case UIA_TextPatternId: | |||
case UIA_TextPattern2Id: | |||
{ | |||
if (accessibilityHandler.getTextInterface() != nullptr) | |||
return new UIATextProvider (this); | |||
break; | |||
} | |||
case UIA_ValuePatternId: | |||
{ | |||
auto editableText = isEditableText (accessibilityHandler); | |||
if (accessibilityHandler.getValueInterface() != nullptr || editableText) | |||
return new UIAValueProvider (this, editableText); | |||
break; | |||
} | |||
case UIA_RangeValuePatternId: | |||
{ | |||
if (accessibilityHandler.getValueInterface() != nullptr | |||
&& accessibilityHandler.getValueInterface()->getRange().isValid()) | |||
{ | |||
return new UIARangeValueProvider (this); | |||
} | |||
break; | |||
} | |||
case UIA_TogglePatternId: | |||
{ | |||
if (accessibilityHandler.getActions().contains (AccessibilityActionType::toggle) | |||
&& accessibilityHandler.getCurrentState().isCheckable()) | |||
{ | |||
return new UIAToggleProvider (this); | |||
} | |||
break; | |||
} | |||
case UIA_SelectionPatternId: | |||
{ | |||
if (role == AccessibilityRole::list | |||
|| role == AccessibilityRole::popupMenu | |||
|| role == AccessibilityRole::tree) | |||
{ | |||
return new UIASelectionProvider (this); | |||
} | |||
break; | |||
} | |||
case UIA_SelectionItemPatternId: | |||
{ | |||
auto state = accessibilityHandler.getCurrentState(); | |||
if (state.isSelectable() || state.isMultiSelectable() | |||
|| role == AccessibilityRole::radioButton) | |||
{ | |||
return new UIASelectionItemProvider (this); | |||
} | |||
break; | |||
} | |||
case UIA_GridPatternId: | |||
{ | |||
if ((role == AccessibilityRole::table || role == AccessibilityRole::tree) | |||
&& accessibilityHandler.getTableInterface() != nullptr) | |||
{ | |||
return new UIAGridProvider (this); | |||
} | |||
break; | |||
} | |||
case UIA_GridItemPatternId: | |||
{ | |||
if ((role == AccessibilityRole::cell || role == AccessibilityRole::treeItem) | |||
&& accessibilityHandler.getCellInterface() != nullptr) | |||
{ | |||
return new UIAGridItemProvider (this); | |||
} | |||
break; | |||
} | |||
case UIA_InvokePatternId: | |||
{ | |||
if (accessibilityHandler.getActions().contains (AccessibilityActionType::press)) | |||
return new UIAInvokeProvider (this); | |||
break; | |||
} | |||
case UIA_ExpandCollapsePatternId: | |||
{ | |||
if (role == AccessibilityRole::menuItem | |||
&& accessibilityHandler.getActions().contains (AccessibilityActionType::showMenu)) | |||
{ | |||
return new UIAExpandCollapseProvider (this); | |||
} | |||
break; | |||
} | |||
default: | |||
break; | |||
} | |||
return nullptr; | |||
}(); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT AccessibilityNativeHandle::GetPropertyValue (PROPERTYID propertyId, VARIANT* pRetVal) | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
VariantHelpers::clear (pRetVal); | |||
const auto fragmentRoot = isFragmentRoot(); | |||
switch (propertyId) | |||
{ | |||
case UIA_AutomationIdPropertyId: | |||
VariantHelpers::setString (getAutomationId (accessibilityHandler), pRetVal); | |||
break; | |||
case UIA_ControlTypePropertyId: | |||
VariantHelpers::setInt (roleToControlTypeId (accessibilityHandler.getRole()), | |||
pRetVal); | |||
break; | |||
case UIA_FrameworkIdPropertyId: | |||
VariantHelpers::setString ("JUCE", pRetVal); | |||
break; | |||
case UIA_FullDescriptionPropertyId: | |||
VariantHelpers::setString (accessibilityHandler.getDescription(), pRetVal); | |||
break; | |||
case UIA_HelpTextPropertyId: | |||
VariantHelpers::setString (accessibilityHandler.getHelp(), pRetVal); | |||
break; | |||
case UIA_IsContentElementPropertyId: | |||
VariantHelpers::setBool (! accessibilityHandler.isIgnored(), pRetVal); | |||
break; | |||
case UIA_IsControlElementPropertyId: | |||
VariantHelpers::setBool (true, pRetVal); | |||
break; | |||
case UIA_IsDialogPropertyId: | |||
VariantHelpers::setBool (accessibilityHandler.getRole() == AccessibilityRole::dialogWindow, pRetVal); | |||
break; | |||
case UIA_IsEnabledPropertyId: | |||
VariantHelpers::setBool (accessibilityHandler.getComponent().isEnabled(), pRetVal); | |||
break; | |||
case UIA_IsKeyboardFocusablePropertyId: | |||
VariantHelpers::setBool (accessibilityHandler.getCurrentState().isFocusable(), pRetVal); | |||
break; | |||
case UIA_HasKeyboardFocusPropertyId: | |||
VariantHelpers::setBool (accessibilityHandler.hasFocus (true), pRetVal); | |||
break; | |||
case UIA_IsOffscreenPropertyId: | |||
VariantHelpers::setBool (false, pRetVal); | |||
break; | |||
case UIA_IsPasswordPropertyId: | |||
if (auto* textInterface = accessibilityHandler.getTextInterface()) | |||
VariantHelpers::setBool (textInterface->isDisplayingProtectedText(), pRetVal); | |||
break; | |||
case UIA_IsPeripheralPropertyId: | |||
VariantHelpers::setBool (accessibilityHandler.getRole() == AccessibilityRole::tooltip | |||
|| accessibilityHandler.getRole() == AccessibilityRole::popupMenu | |||
|| accessibilityHandler.getRole() == AccessibilityRole::splashScreen, | |||
pRetVal); | |||
break; | |||
case UIA_NamePropertyId: | |||
VariantHelpers::setString (getElementName(), pRetVal); | |||
break; | |||
case UIA_ProcessIdPropertyId: | |||
VariantHelpers::setInt ((int) GetCurrentProcessId(), pRetVal); | |||
break; | |||
case UIA_NativeWindowHandlePropertyId: | |||
if (fragmentRoot) | |||
VariantHelpers::setInt ((int) (pointer_sized_int) accessibilityHandler.getComponent().getWindowHandle(), pRetVal); | |||
break; | |||
default: | |||
break; | |||
} | |||
return S_OK; | |||
}); | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT AccessibilityNativeHandle::Navigate (NavigateDirection direction, IRawElementProviderFragment** pRetVal) | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
auto* handler = [&]() -> AccessibilityHandler* | |||
{ | |||
if (direction == NavigateDirection_Parent) | |||
return accessibilityHandler.getParent(); | |||
if (direction == NavigateDirection_FirstChild | |||
|| direction == NavigateDirection_LastChild) | |||
{ | |||
auto children = accessibilityHandler.getChildren(); | |||
return children.empty() ? nullptr | |||
: (direction == NavigateDirection_FirstChild ? children.front() | |||
: children.back()); | |||
} | |||
if (direction == NavigateDirection_NextSibling | |||
|| direction == NavigateDirection_PreviousSibling) | |||
{ | |||
if (auto* parent = accessibilityHandler.getParent()) | |||
{ | |||
const auto siblings = parent->getChildren(); | |||
const auto iter = std::find (siblings.cbegin(), siblings.cend(), &accessibilityHandler); | |||
if (iter == siblings.end()) | |||
return nullptr; | |||
if (direction == NavigateDirection_NextSibling && iter != std::prev (siblings.cend())) | |||
return *std::next (iter); | |||
if (direction == NavigateDirection_PreviousSibling && iter != siblings.cbegin()) | |||
return *std::prev (iter); | |||
} | |||
} | |||
return nullptr; | |||
}(); | |||
if (handler != nullptr) | |||
if (auto* provider = handler->getNativeImplementation()) | |||
if (provider->isElementValid()) | |||
provider->QueryInterface (IID_PPV_ARGS (pRetVal)); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT AccessibilityNativeHandle::GetRuntimeId (SAFEARRAY** pRetVal) | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
if (! isFragmentRoot()) | |||
{ | |||
*pRetVal = SafeArrayCreateVector (VT_I4, 0, 2); | |||
if (*pRetVal == nullptr) | |||
return E_OUTOFMEMORY; | |||
for (LONG i = 0; i < 2; ++i) | |||
{ | |||
auto hr = SafeArrayPutElement (*pRetVal, &i, &rtid[i]); | |||
if (FAILED (hr)) | |||
return E_FAIL; | |||
} | |||
} | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT AccessibilityNativeHandle::get_BoundingRectangle (UiaRect* pRetVal) | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
auto bounds = Desktop::getInstance().getDisplays() | |||
.logicalToPhysical (accessibilityHandler.getComponent().getScreenBounds()); | |||
pRetVal->left = bounds.getX(); | |||
pRetVal->top = bounds.getY(); | |||
pRetVal->width = bounds.getWidth(); | |||
pRetVal->height = bounds.getHeight(); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT AccessibilityNativeHandle::GetEmbeddedFragmentRoots (SAFEARRAY** pRetVal) | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [] | |||
{ | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT AccessibilityNativeHandle::SetFocus() | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
accessibilityHandler.grabFocus(); | |||
return S_OK; | |||
} | |||
JUCE_COMRESULT AccessibilityNativeHandle::get_FragmentRoot (IRawElementProviderFragmentRoot** pRetVal) | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT | |||
{ | |||
auto* handler = [&]() -> AccessibilityHandler* | |||
{ | |||
if (isFragmentRoot()) | |||
return &accessibilityHandler; | |||
if (auto* peer = accessibilityHandler.getComponent().getPeer()) | |||
if (auto* handler = peer->getComponent().getAccessibilityHandler()) | |||
return handler; | |||
return nullptr; | |||
}(); | |||
if (handler != nullptr) | |||
{ | |||
handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); | |||
return S_OK; | |||
} | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
}); | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT AccessibilityNativeHandle::ElementProviderFromPoint (double x, double y, IRawElementProviderFragment** pRetVal) | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
auto* handler = [&] | |||
{ | |||
auto logicalScreenPoint = Desktop::getInstance().getDisplays() | |||
.physicalToLogical (Point<int> (roundToInt (x), | |||
roundToInt (y))); | |||
if (auto* child = accessibilityHandler.getChildAt (logicalScreenPoint)) | |||
return child; | |||
return &accessibilityHandler; | |||
}(); | |||
handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT AccessibilityNativeHandle::GetFocus (IRawElementProviderFragment** pRetVal) | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
const auto getFocusHandler = [this]() -> AccessibilityHandler* | |||
{ | |||
if (auto* modal = Component::getCurrentlyModalComponent()) | |||
{ | |||
const auto& component = accessibilityHandler.getComponent(); | |||
if (! component.isParentOf (modal) | |||
&& component.isCurrentlyBlockedByAnotherModalComponent()) | |||
{ | |||
if (auto* modalHandler = modal->getAccessibilityHandler()) | |||
{ | |||
if (auto* focusChild = modalHandler->getChildFocus()) | |||
return focusChild; | |||
return modalHandler; | |||
} | |||
} | |||
} | |||
if (auto* focusChild = accessibilityHandler.getChildFocus()) | |||
return focusChild; | |||
return nullptr; | |||
}; | |||
if (auto* focusHandler = getFocusHandler()) | |||
focusHandler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); | |||
return S_OK; | |||
}); | |||
} | |||
//============================================================================== | |||
String AccessibilityNativeHandle::getElementName() const | |||
{ | |||
if (accessibilityHandler.getRole() == AccessibilityRole::tooltip) | |||
return accessibilityHandler.getDescription(); | |||
auto name = accessibilityHandler.getTitle(); | |||
if (name.isEmpty() && isFragmentRoot()) | |||
return getAccessibleApplicationOrPluginName(); | |||
return name; | |||
} | |||
} // namespace juce |
@@ -0,0 +1,80 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
#define UIA_FullDescriptionPropertyId 30159 | |||
#define UIA_IsDialogPropertyId 30174 | |||
class AccessibilityNativeHandle : public ComBaseClassHelper<IRawElementProviderSimple, | |||
IRawElementProviderFragment, | |||
IRawElementProviderFragmentRoot> | |||
{ | |||
public: | |||
explicit AccessibilityNativeHandle (AccessibilityHandler& handler); | |||
//============================================================================== | |||
void invalidateElement() noexcept { valid = false; } | |||
bool isElementValid() const noexcept { return valid; } | |||
const AccessibilityHandler& getHandler() { return accessibilityHandler; } | |||
//============================================================================== | |||
JUCE_COMRESULT QueryInterface (REFIID refId, void** result) override; | |||
//============================================================================== | |||
JUCE_COMRESULT get_HostRawElementProvider (IRawElementProviderSimple** provider) override; | |||
JUCE_COMRESULT get_ProviderOptions (ProviderOptions* options) override; | |||
JUCE_COMRESULT GetPatternProvider (PATTERNID pId, IUnknown** provider) override; | |||
JUCE_COMRESULT GetPropertyValue (PROPERTYID propertyId, VARIANT* pRetVal) override; | |||
JUCE_COMRESULT Navigate (NavigateDirection direction, IRawElementProviderFragment** pRetVal) override; | |||
JUCE_COMRESULT GetRuntimeId (SAFEARRAY** pRetVal) override; | |||
JUCE_COMRESULT get_BoundingRectangle (UiaRect* pRetVal) override; | |||
JUCE_COMRESULT GetEmbeddedFragmentRoots (SAFEARRAY** pRetVal) override; | |||
JUCE_COMRESULT SetFocus() override; | |||
JUCE_COMRESULT get_FragmentRoot (IRawElementProviderFragmentRoot** pRetVal) override; | |||
JUCE_COMRESULT ElementProviderFromPoint (double x, double y, IRawElementProviderFragment** pRetVal) override; | |||
JUCE_COMRESULT GetFocus (IRawElementProviderFragment** pRetVal) override; | |||
private: | |||
//============================================================================== | |||
String getElementName() const; | |||
bool isFragmentRoot() const { return accessibilityHandler.getComponent().isOnDesktop(); } | |||
//============================================================================== | |||
AccessibilityHandler& accessibilityHandler; | |||
static int idCounter; | |||
std::array<int, 2> rtid { UiaAppendRuntimeId, ++idCounter }; | |||
bool valid = true; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityNativeHandle) | |||
}; | |||
} |
@@ -0,0 +1,86 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
class UIAExpandCollapseProvider : public UIAProviderBase, | |||
public ComBaseClassHelper<IExpandCollapseProvider> | |||
{ | |||
public: | |||
explicit UIAExpandCollapseProvider (AccessibilityNativeHandle* nativeHandle) | |||
: UIAProviderBase (nativeHandle) | |||
{ | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT Expand() override | |||
{ | |||
return invokeShowMenu(); | |||
} | |||
JUCE_COMRESULT Collapse() override | |||
{ | |||
return invokeShowMenu(); | |||
} | |||
JUCE_COMRESULT get_ExpandCollapseState (ExpandCollapseState* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
*pRetVal = getHandler().getCurrentState().isExpanded() | |||
? ExpandCollapseState_Expanded | |||
: ExpandCollapseState_Collapsed; | |||
return S_OK; | |||
}); | |||
} | |||
private: | |||
JUCE_COMRESULT invokeShowMenu() | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
const auto& handler = getHandler(); | |||
if (handler.getActions().invoke (AccessibilityActionType::showMenu)) | |||
{ | |||
sendAccessibilityAutomationEvent (handler, handler.getCurrentState().isExpanded() | |||
? UIA_MenuOpenedEventId | |||
: UIA_MenuClosedEventId); | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAExpandCollapseProvider) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,101 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
class UIAGridItemProvider : public UIAProviderBase, | |||
public ComBaseClassHelper<IGridItemProvider> | |||
{ | |||
public: | |||
explicit UIAGridItemProvider (AccessibilityNativeHandle* nativeHandle) | |||
: UIAProviderBase (nativeHandle) | |||
{ | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT get_Row (int* pRetVal) override | |||
{ | |||
return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) | |||
{ | |||
*pRetVal = cellInterface.getRowIndex(); | |||
}); | |||
} | |||
JUCE_COMRESULT get_Column (int* pRetVal) override | |||
{ | |||
return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) | |||
{ | |||
*pRetVal = cellInterface.getColumnIndex(); | |||
}); | |||
} | |||
JUCE_COMRESULT get_RowSpan (int* pRetVal) override | |||
{ | |||
return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) | |||
{ | |||
*pRetVal = cellInterface.getRowSpan(); | |||
}); | |||
} | |||
JUCE_COMRESULT get_ColumnSpan (int* pRetVal) override | |||
{ | |||
return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) | |||
{ | |||
*pRetVal = cellInterface.getColumnSpan(); | |||
}); | |||
} | |||
JUCE_COMRESULT get_ContainingGrid (IRawElementProviderSimple** pRetVal) override | |||
{ | |||
return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) | |||
{ | |||
if (auto* handler = cellInterface.getTableHandler()) | |||
handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); | |||
}); | |||
} | |||
private: | |||
template <typename Value, typename Callback> | |||
JUCE_COMRESULT withCellInterface (Value* pRetVal, Callback&& callback) const | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT | |||
{ | |||
if (auto* cellInterface = getHandler().getCellInterface()) | |||
{ | |||
callback (*cellInterface); | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
}); | |||
} | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAGridItemProvider) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,90 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
class UIAGridProvider : public UIAProviderBase, | |||
public ComBaseClassHelper<IGridProvider> | |||
{ | |||
public: | |||
explicit UIAGridProvider (AccessibilityNativeHandle* nativeHandle) | |||
: UIAProviderBase (nativeHandle) | |||
{ | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT GetItem (int row, int column, IRawElementProviderSimple** pRetVal) override | |||
{ | |||
return withTableInterface (pRetVal, [&] (const AccessibilityTableInterface& tableInterface) | |||
{ | |||
if (! isPositiveAndBelow (row, tableInterface.getNumRows()) | |||
|| ! isPositiveAndBelow (column, tableInterface.getNumColumns())) | |||
return E_INVALIDARG; | |||
if (auto* handler = tableInterface.getCellHandler (row, column)) | |||
handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT get_RowCount (int* pRetVal) override | |||
{ | |||
return withTableInterface (pRetVal, [&] (const AccessibilityTableInterface& tableInterface) | |||
{ | |||
*pRetVal = tableInterface.getNumRows(); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT get_ColumnCount (int* pRetVal) override | |||
{ | |||
return withTableInterface (pRetVal, [&] (const AccessibilityTableInterface& tableInterface) | |||
{ | |||
*pRetVal = tableInterface.getNumColumns(); | |||
return S_OK; | |||
}); | |||
} | |||
private: | |||
template <typename Value, typename Callback> | |||
JUCE_COMRESULT withTableInterface (Value* pRetVal, Callback&& callback) const | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT | |||
{ | |||
if (auto* tableInterface = getHandler().getTableInterface()) | |||
return callback (*tableInterface); | |||
return UIA_E_NOTSUPPORTED; | |||
}); | |||
} | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAGridProvider) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,103 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
namespace VariantHelpers | |||
{ | |||
inline void clear (VARIANT* variant) | |||
{ | |||
variant->vt = VT_EMPTY; | |||
} | |||
inline void setInt (int value, VARIANT* variant) | |||
{ | |||
variant->vt = VT_I4; | |||
variant->lVal = value; | |||
} | |||
inline void setBool (bool value, VARIANT* variant) | |||
{ | |||
variant->vt = VT_BOOL; | |||
variant->boolVal = value ? -1 : 0; | |||
} | |||
inline void setString (const String& value, VARIANT* variant) | |||
{ | |||
variant->vt = VT_BSTR; | |||
variant->bstrVal = SysAllocString ((const OLECHAR*) value.toWideCharPointer()); | |||
} | |||
inline void setDouble (double value, VARIANT* variant) | |||
{ | |||
variant->vt = VT_R8; | |||
variant->dblVal = value; | |||
} | |||
} | |||
JUCE_COMRESULT addHandlersToArray (const std::vector<const AccessibilityHandler*>& handlers, SAFEARRAY** pRetVal) | |||
{ | |||
auto numHandlers = handlers.size(); | |||
*pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, (ULONG) numHandlers); | |||
if (pRetVal != nullptr) | |||
{ | |||
for (LONG i = 0; i < (LONG) numHandlers; ++i) | |||
{ | |||
auto* handler = handlers[i]; | |||
if (handler == nullptr) | |||
continue; | |||
ComSmartPtr<IRawElementProviderSimple> provider; | |||
handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress())); | |||
auto hr = SafeArrayPutElement (*pRetVal, &i, provider); | |||
if (FAILED (hr)) | |||
return E_FAIL; | |||
} | |||
} | |||
return S_OK; | |||
} | |||
template <typename Value, typename Object, typename Callback> | |||
JUCE_COMRESULT withCheckedComArgs (Value* pRetVal, Object& handle, Callback&& callback) | |||
{ | |||
if (pRetVal == nullptr) | |||
return E_INVALIDARG; | |||
*pRetVal = Value{}; | |||
if (! handle.isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
return callback(); | |||
} | |||
} // namespace juce |
@@ -0,0 +1,62 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
class UIAInvokeProvider : public UIAProviderBase, | |||
public ComBaseClassHelper<IInvokeProvider> | |||
{ | |||
public: | |||
explicit UIAInvokeProvider (AccessibilityNativeHandle* nativeHandle) | |||
: UIAProviderBase (nativeHandle) | |||
{ | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT Invoke() override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
const auto& handler = getHandler(); | |||
if (handler.getActions().invoke (AccessibilityActionType::press)) | |||
{ | |||
if (isElementValid()) | |||
sendAccessibilityAutomationEvent (handler, UIA_Invoke_InvokedEventId); | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAInvokeProvider) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,58 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
class UIAProviderBase | |||
{ | |||
public: | |||
explicit UIAProviderBase (AccessibilityNativeHandle* nativeHandleIn) | |||
: nativeHandle (nativeHandleIn) | |||
{ | |||
} | |||
bool isElementValid() const | |||
{ | |||
if (nativeHandle != nullptr) | |||
return nativeHandle->isElementValid(); | |||
return false; | |||
} | |||
const AccessibilityHandler& getHandler() const | |||
{ | |||
return nativeHandle->getHandler(); | |||
} | |||
private: | |||
ComSmartPtr<AccessibilityNativeHandle> nativeHandle; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAProviderBase) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,43 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
void sendAccessibilityAutomationEvent (const AccessibilityHandler&, EVENTID); | |||
void sendAccessibilityPropertyChangedEvent (const AccessibilityHandler&, PROPERTYID, VARIANT); | |||
} | |||
#include "juce_win32_UIAProviderBase.h" | |||
#include "juce_win32_UIAExpandCollapseProvider.h" | |||
#include "juce_win32_UIAGridItemProvider.h" | |||
#include "juce_win32_UIAGridProvider.h" | |||
#include "juce_win32_UIAInvokeProvider.h" | |||
#include "juce_win32_UIARangeValueProvider.h" | |||
#include "juce_win32_UIASelectionProvider.h" | |||
#include "juce_win32_UIATextProvider.h" | |||
#include "juce_win32_UIAToggleProvider.h" | |||
#include "juce_win32_UIATransformProvider.h" | |||
#include "juce_win32_UIAValueProvider.h" | |||
#include "juce_win32_UIAWindowProvider.h" |
@@ -0,0 +1,140 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
class UIARangeValueProvider : public UIAProviderBase, | |||
public ComBaseClassHelper<IRangeValueProvider> | |||
{ | |||
public: | |||
explicit UIARangeValueProvider (AccessibilityNativeHandle* nativeHandle) | |||
: UIAProviderBase (nativeHandle) | |||
{ | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT SetValue (double val) override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
const auto& handler = getHandler(); | |||
if (auto* valueInterface = handler.getValueInterface()) | |||
{ | |||
auto range = valueInterface->getRange(); | |||
if (range.isValid()) | |||
{ | |||
if (val < range.getMinimumValue() || val > range.getMaximumValue()) | |||
return E_INVALIDARG; | |||
if (! valueInterface->isReadOnly()) | |||
{ | |||
valueInterface->setValue (val); | |||
VARIANT newValue; | |||
VariantHelpers::setDouble (valueInterface->getCurrentValue(), &newValue); | |||
sendAccessibilityPropertyChangedEvent (handler, UIA_RangeValueValuePropertyId, newValue); | |||
return S_OK; | |||
} | |||
} | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
JUCE_COMRESULT get_Value (double* pRetVal) override | |||
{ | |||
return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface) | |||
{ | |||
return valueInterface.getCurrentValue(); | |||
}); | |||
} | |||
JUCE_COMRESULT get_IsReadOnly (BOOL* pRetVal) override | |||
{ | |||
return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface) | |||
{ | |||
return valueInterface.isReadOnly(); | |||
}); | |||
} | |||
JUCE_COMRESULT get_Maximum (double* pRetVal) override | |||
{ | |||
return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface) | |||
{ | |||
return valueInterface.getRange().getMaximumValue(); | |||
}); | |||
} | |||
JUCE_COMRESULT get_Minimum (double* pRetVal) override | |||
{ | |||
return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface) | |||
{ | |||
return valueInterface.getRange().getMinimumValue(); | |||
}); | |||
} | |||
JUCE_COMRESULT get_LargeChange (double* pRetVal) override | |||
{ | |||
return get_SmallChange (pRetVal); | |||
} | |||
JUCE_COMRESULT get_SmallChange (double* pRetVal) override | |||
{ | |||
return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface) | |||
{ | |||
return valueInterface.getRange().getInterval(); | |||
}); | |||
} | |||
private: | |||
template <typename Value, typename Callback> | |||
JUCE_COMRESULT withValueInterface (Value* pRetVal, Callback&& callback) const | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT | |||
{ | |||
if (auto* valueInterface = getHandler().getValueInterface()) | |||
{ | |||
if (valueInterface->getRange().isValid()) | |||
{ | |||
*pRetVal = callback (*valueInterface); | |||
return S_OK; | |||
} | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
}); | |||
} | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIARangeValueProvider) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,252 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
JUCE_COMCLASS (ISelectionProvider2, "14f68475-ee1c-44f6-a869-d239381f0fe7") : public ISelectionProvider | |||
{ | |||
JUCE_COMCALL get_FirstSelectedItem (IRawElementProviderSimple** retVal) = 0; | |||
JUCE_COMCALL get_LastSelectedItem (IRawElementProviderSimple** retVal) = 0; | |||
JUCE_COMCALL get_CurrentSelectedItem (IRawElementProviderSimple** retVal) = 0; | |||
JUCE_COMCALL get_ItemCount (int* retVal) = 0; | |||
}; | |||
//============================================================================== | |||
class UIASelectionItemProvider : public UIAProviderBase, | |||
public ComBaseClassHelper<ISelectionItemProvider> | |||
{ | |||
public: | |||
explicit UIASelectionItemProvider (AccessibilityNativeHandle* nativeHandle) | |||
: UIAProviderBase (nativeHandle), | |||
isRadioButton (getHandler().getRole() == AccessibilityRole::radioButton) | |||
{ | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT AddToSelection() override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
const auto& handler = getHandler(); | |||
if (isRadioButton) | |||
{ | |||
handler.getActions().invoke (AccessibilityActionType::press); | |||
sendAccessibilityAutomationEvent (handler, UIA_SelectionItem_ElementSelectedEventId); | |||
return S_OK; | |||
} | |||
handler.getActions().invoke (AccessibilityActionType::toggle); | |||
handler.getActions().invoke (AccessibilityActionType::press); | |||
return S_OK; | |||
} | |||
JUCE_COMRESULT get_IsSelected (BOOL* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
const auto state = getHandler().getCurrentState(); | |||
*pRetVal = isRadioButton ? state.isChecked() : state.isSelected(); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT get_SelectionContainer (IRawElementProviderSimple** pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
if (! isRadioButton) | |||
if (auto* parent = getHandler().getParent()) | |||
parent->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT RemoveFromSelection() override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
if (! isRadioButton) | |||
{ | |||
const auto& handler = getHandler(); | |||
if (handler.getCurrentState().isSelected()) | |||
getHandler().getActions().invoke (AccessibilityActionType::toggle); | |||
} | |||
return S_OK; | |||
} | |||
JUCE_COMRESULT Select() override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
AddToSelection(); | |||
if (! isRadioButton) | |||
{ | |||
const auto& handler = getHandler(); | |||
if (auto* parent = handler.getParent()) | |||
for (auto* child : parent->getChildren()) | |||
if (child != &handler && child->getCurrentState().isSelected()) | |||
child->getActions().invoke (AccessibilityActionType::toggle); | |||
} | |||
return S_OK; | |||
} | |||
private: | |||
const bool isRadioButton; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIASelectionItemProvider) | |||
}; | |||
//============================================================================== | |||
class UIASelectionProvider : public UIAProviderBase, | |||
public ComBaseClassHelper<ISelectionProvider2> | |||
{ | |||
public: | |||
explicit UIASelectionProvider (AccessibilityNativeHandle* nativeHandle) | |||
: UIAProviderBase (nativeHandle) | |||
{ | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT QueryInterface (REFIID iid, void** result) override | |||
{ | |||
if (iid == _uuidof (IUnknown) || iid == _uuidof (ISelectionProvider)) | |||
return castToType<ISelectionProvider> (result); | |||
if (iid == _uuidof (ISelectionProvider2)) | |||
return castToType<ISelectionProvider2> (result); | |||
*result = nullptr; | |||
return E_NOINTERFACE; | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT get_CanSelectMultiple (BOOL* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
*pRetVal = isMultiSelectable(); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT get_IsSelectionRequired (BOOL* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
*pRetVal = getSelectedChildren().size() > 0 && ! isMultiSelectable(); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT GetSelection (SAFEARRAY** pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
return addHandlersToArray (getSelectedChildren(), pRetVal); | |||
}); | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT get_FirstSelectedItem (IRawElementProviderSimple** pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
auto selectedChildren = getSelectedChildren(); | |||
if (! selectedChildren.empty()) | |||
selectedChildren.front()->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT get_LastSelectedItem (IRawElementProviderSimple** pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
auto selectedChildren = getSelectedChildren(); | |||
if (! selectedChildren.empty()) | |||
selectedChildren.back()->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT get_CurrentSelectedItem (IRawElementProviderSimple** pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
get_FirstSelectedItem (pRetVal); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT get_ItemCount (int* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
*pRetVal = (int) getSelectedChildren().size(); | |||
return S_OK; | |||
}); | |||
} | |||
private: | |||
bool isMultiSelectable() const noexcept | |||
{ | |||
return getHandler().getCurrentState().isMultiSelectable(); | |||
} | |||
std::vector<const AccessibilityHandler*> getSelectedChildren() const | |||
{ | |||
std::vector<const AccessibilityHandler*> selectedHandlers; | |||
for (auto* child : getHandler().getComponent().getChildren()) | |||
if (auto* handler = child->getAccessibilityHandler()) | |||
if (handler->getCurrentState().isSelected()) | |||
selectedHandlers.push_back (handler); | |||
return selectedHandlers; | |||
} | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIASelectionProvider) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,664 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
class UIATextProvider : public UIAProviderBase, | |||
public ComBaseClassHelper<ITextProvider2> | |||
{ | |||
public: | |||
explicit UIATextProvider (AccessibilityNativeHandle* nativeHandle) | |||
: UIAProviderBase (nativeHandle) | |||
{ | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT QueryInterface (REFIID iid, void** result) override | |||
{ | |||
if (iid == _uuidof (IUnknown) || iid == _uuidof (ITextProvider)) | |||
return castToType<ITextProvider> (result); | |||
if (iid == _uuidof (ITextProvider2)) | |||
return castToType<ITextProvider2> (result); | |||
*result = nullptr; | |||
return E_NOINTERFACE; | |||
} | |||
//============================================================================= | |||
JUCE_COMRESULT get_DocumentRange (ITextRangeProvider** pRetVal) override | |||
{ | |||
return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) | |||
{ | |||
*pRetVal = new UIATextRangeProvider (*this, { 0, textInterface.getTotalNumCharacters() }); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT get_SupportedTextSelection (SupportedTextSelection* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
*pRetVal = SupportedTextSelection_Single; | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT GetSelection (SAFEARRAY** pRetVal) override | |||
{ | |||
return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) | |||
{ | |||
*pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, 1); | |||
if (pRetVal != nullptr) | |||
{ | |||
auto selection = textInterface.getSelection(); | |||
auto hasSelection = ! selection.isEmpty(); | |||
auto cursorPos = textInterface.getTextInsertionOffset(); | |||
auto* rangeProvider = new UIATextRangeProvider (*this, | |||
{ hasSelection ? selection.getStart() : cursorPos, | |||
hasSelection ? selection.getEnd() : cursorPos }); | |||
LONG pos = 0; | |||
auto hr = SafeArrayPutElement (*pRetVal, &pos, static_cast<IUnknown*> (rangeProvider)); | |||
if (FAILED (hr)) | |||
return E_FAIL; | |||
rangeProvider->Release(); | |||
} | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT GetVisibleRanges (SAFEARRAY** pRetVal) override | |||
{ | |||
return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) | |||
{ | |||
*pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, 1); | |||
if (pRetVal != nullptr) | |||
{ | |||
auto* rangeProvider = new UIATextRangeProvider (*this, { 0, textInterface.getTotalNumCharacters() }); | |||
LONG pos = 0; | |||
auto hr = SafeArrayPutElement (*pRetVal, &pos, static_cast<IUnknown*> (rangeProvider)); | |||
if (FAILED (hr)) | |||
return E_FAIL; | |||
rangeProvider->Release(); | |||
} | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT RangeFromChild (IRawElementProviderSimple*, ITextRangeProvider** pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [] | |||
{ | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT RangeFromPoint (UiaPoint point, ITextRangeProvider** pRetVal) override | |||
{ | |||
return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) | |||
{ | |||
auto offset = textInterface.getOffsetAtPoint ({ roundToInt (point.x), roundToInt (point.y) }); | |||
if (offset > 0) | |||
*pRetVal = new UIATextRangeProvider (*this, { offset, offset }); | |||
return S_OK; | |||
}); | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT GetCaretRange (BOOL* isActive, ITextRangeProvider** pRetVal) override | |||
{ | |||
return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) | |||
{ | |||
*isActive = getHandler().hasFocus (false); | |||
auto cursorPos = textInterface.getTextInsertionOffset(); | |||
*pRetVal = new UIATextRangeProvider (*this, { cursorPos, cursorPos }); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT RangeFromAnnotation (IRawElementProviderSimple*, ITextRangeProvider** pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [] | |||
{ | |||
return S_OK; | |||
}); | |||
} | |||
private: | |||
//============================================================================== | |||
template <typename Value, typename Callback> | |||
JUCE_COMRESULT withTextInterface (Value* pRetVal, Callback&& callback) const | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT | |||
{ | |||
if (auto* textInterface = getHandler().getTextInterface()) | |||
return callback (*textInterface); | |||
return UIA_E_NOTSUPPORTED; | |||
}); | |||
} | |||
//============================================================================== | |||
class UIATextRangeProvider : public UIAProviderBase, | |||
public ComBaseClassHelper<ITextRangeProvider> | |||
{ | |||
public: | |||
UIATextRangeProvider (UIATextProvider& textProvider, Range<int> range) | |||
: UIAProviderBase (textProvider.getHandler().getNativeImplementation()), | |||
owner (&textProvider), | |||
selectionRange (range) | |||
{ | |||
} | |||
//============================================================================== | |||
Range<int> getSelectionRange() const noexcept { return selectionRange; } | |||
//============================================================================== | |||
JUCE_COMRESULT AddToSelection() override | |||
{ | |||
return Select(); | |||
} | |||
JUCE_COMRESULT Clone (ITextRangeProvider** pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
*pRetVal = new UIATextRangeProvider (*owner, selectionRange); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT Compare (ITextRangeProvider* range, BOOL* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
*pRetVal = (selectionRange == static_cast<UIATextRangeProvider*> (range)->getSelectionRange()); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT CompareEndpoints (TextPatternRangeEndpoint endpoint, | |||
ITextRangeProvider* targetRange, | |||
TextPatternRangeEndpoint targetEndpoint, | |||
int* pRetVal) override | |||
{ | |||
if (targetRange == nullptr) | |||
return E_INVALIDARG; | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
auto offset = (endpoint == TextPatternRangeEndpoint_Start ? selectionRange.getStart() | |||
: selectionRange.getEnd()); | |||
auto otherRange = static_cast<UIATextRangeProvider*> (targetRange)->getSelectionRange(); | |||
auto otherOffset = (targetEndpoint == TextPatternRangeEndpoint_Start ? otherRange.getStart() | |||
: otherRange.getEnd()); | |||
*pRetVal = offset - otherOffset; | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT ExpandToEnclosingUnit (TextUnit unit) override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
if (auto* textInterface = getHandler().getTextInterface()) | |||
{ | |||
auto numCharacters = textInterface->getTotalNumCharacters(); | |||
if (numCharacters == 0) | |||
{ | |||
selectionRange = {}; | |||
return S_OK; | |||
} | |||
if (unit == TextUnit_Character) | |||
{ | |||
selectionRange.setStart (jlimit (0, numCharacters - 1, selectionRange.getStart())); | |||
selectionRange.setEnd (selectionRange.getStart() + 1); | |||
return S_OK; | |||
} | |||
if (unit == TextUnit_Paragraph | |||
|| unit == TextUnit_Page | |||
|| unit == TextUnit_Document) | |||
{ | |||
selectionRange = { 0, textInterface->getTotalNumCharacters() }; | |||
return S_OK; | |||
} | |||
auto start = getNextEndpointPosition (*textInterface, | |||
selectionRange.getStart(), | |||
unit, | |||
NextEndpointDirection::backwards); | |||
if (start >= 0) | |||
{ | |||
auto end = getNextEndpointPosition (*textInterface, | |||
start, | |||
unit, | |||
NextEndpointDirection::forwards); | |||
if (end >= 0) | |||
selectionRange = Range<int> (start, end); | |||
} | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
JUCE_COMRESULT FindAttribute (TEXTATTRIBUTEID, VARIANT, BOOL, ITextRangeProvider** pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [] | |||
{ | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT FindText (BSTR text, BOOL backward, BOOL ignoreCase, | |||
ITextRangeProvider** pRetVal) override | |||
{ | |||
return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) | |||
{ | |||
auto selectionText = textInterface.getText (selectionRange); | |||
String textToSearchFor (text); | |||
auto offset = (backward ? (ignoreCase ? selectionText.lastIndexOfIgnoreCase (textToSearchFor) : selectionText.lastIndexOf (textToSearchFor)) | |||
: (ignoreCase ? selectionText.indexOfIgnoreCase (textToSearchFor) : selectionText.indexOf (textToSearchFor))); | |||
if (offset != -1) | |||
*pRetVal = new UIATextRangeProvider (*owner, { offset, offset + textToSearchFor.length() }); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT GetAttributeValue (TEXTATTRIBUTEID attributeId, VARIANT* pRetVal) override | |||
{ | |||
return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) | |||
{ | |||
VariantHelpers::clear (pRetVal); | |||
const auto& handler = getHandler(); | |||
switch (attributeId) | |||
{ | |||
case UIA_IsReadOnlyAttributeId: | |||
{ | |||
const auto readOnly = [&] | |||
{ | |||
if (auto* valueInterface = handler.getValueInterface()) | |||
return valueInterface->isReadOnly(); | |||
return false; | |||
}(); | |||
VariantHelpers::setBool (readOnly, pRetVal); | |||
break; | |||
} | |||
case UIA_CaretPositionAttributeId: | |||
{ | |||
auto cursorPos = textInterface.getTextInsertionOffset(); | |||
auto caretPos = [&] | |||
{ | |||
if (cursorPos == 0) | |||
return CaretPosition_BeginningOfLine; | |||
if (cursorPos == textInterface.getTotalNumCharacters()) | |||
return CaretPosition_EndOfLine; | |||
return CaretPosition_Unknown; | |||
}(); | |||
VariantHelpers::setInt (caretPos, pRetVal); | |||
break; | |||
} | |||
default: | |||
break; | |||
} | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT GetBoundingRectangles (SAFEARRAY** pRetVal) override | |||
{ | |||
return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) | |||
{ | |||
auto rectangleList = textInterface.getTextBounds (selectionRange); | |||
auto numRectangles = rectangleList.getNumRectangles(); | |||
*pRetVal = SafeArrayCreateVector (VT_R8, 0, 4 * numRectangles); | |||
if (*pRetVal == nullptr) | |||
return E_FAIL; | |||
if (numRectangles > 0) | |||
{ | |||
double* doubleArr = nullptr; | |||
if (FAILED (SafeArrayAccessData (*pRetVal, reinterpret_cast<void**> (&doubleArr)))) | |||
{ | |||
SafeArrayDestroy (*pRetVal); | |||
return E_FAIL; | |||
} | |||
for (int i = 0; i < numRectangles; ++i) | |||
{ | |||
auto r = Desktop::getInstance().getDisplays().logicalToPhysical (rectangleList.getRectangle (i)); | |||
doubleArr[i * 4] = r.getX(); | |||
doubleArr[i * 4 + 1] = r.getY(); | |||
doubleArr[i * 4 + 2] = r.getWidth(); | |||
doubleArr[i * 4 + 3] = r.getHeight(); | |||
} | |||
if (FAILED (SafeArrayUnaccessData (*pRetVal))) | |||
{ | |||
SafeArrayDestroy (*pRetVal); | |||
return E_FAIL; | |||
} | |||
} | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT GetChildren (SAFEARRAY** pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
*pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, 0); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT GetEnclosingElement (IRawElementProviderSimple** pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
getHandler().getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT GetText (int maxLength, BSTR* pRetVal) override | |||
{ | |||
return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) | |||
{ | |||
auto text = textInterface.getText (selectionRange); | |||
if (maxLength >= 0 && text.length() > maxLength) | |||
text = text.substring (0, maxLength); | |||
*pRetVal = SysAllocString ((const OLECHAR*) text.toWideCharPointer()); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT Move (TextUnit unit, int count, int* pRetVal) override | |||
{ | |||
return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface&) | |||
{ | |||
if (count > 0) | |||
{ | |||
MoveEndpointByUnit (TextPatternRangeEndpoint_End, unit, count, pRetVal); | |||
MoveEndpointByUnit (TextPatternRangeEndpoint_Start, unit, count, pRetVal); | |||
} | |||
else if (count < 0) | |||
{ | |||
MoveEndpointByUnit (TextPatternRangeEndpoint_Start, unit, count, pRetVal); | |||
MoveEndpointByUnit (TextPatternRangeEndpoint_End, unit, count, pRetVal); | |||
} | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT MoveEndpointByRange (TextPatternRangeEndpoint endpoint, | |||
ITextRangeProvider* targetRange, | |||
TextPatternRangeEndpoint targetEndpoint) override | |||
{ | |||
if (targetRange == nullptr) | |||
return E_INVALIDARG; | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
if (auto* textInterface = getHandler().getTextInterface()) | |||
{ | |||
auto otherRange = static_cast<UIATextRangeProvider*> (targetRange)->getSelectionRange(); | |||
auto targetPoint = (targetEndpoint == TextPatternRangeEndpoint_Start ? otherRange.getStart() | |||
: otherRange.getEnd()); | |||
setEndpointChecked (endpoint, targetPoint); | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
JUCE_COMRESULT MoveEndpointByUnit (TextPatternRangeEndpoint endpoint, | |||
TextUnit unit, | |||
int count, | |||
int* pRetVal) override | |||
{ | |||
return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface) | |||
{ | |||
auto numCharacters = textInterface.getTotalNumCharacters(); | |||
if (count == 0 || numCharacters == 0) | |||
return S_OK; | |||
auto isStart = (endpoint == TextPatternRangeEndpoint_Start); | |||
auto endpointToMove = (isStart ? selectionRange.getStart() : selectionRange.getEnd()); | |||
if (unit == TextUnit_Character) | |||
{ | |||
auto targetPoint = jlimit (0, numCharacters, endpointToMove + count); | |||
*pRetVal = targetPoint - endpointToMove; | |||
setEndpointChecked (endpoint, targetPoint); | |||
return S_OK; | |||
} | |||
auto direction = (count > 0 ? NextEndpointDirection::forwards | |||
: NextEndpointDirection::backwards); | |||
if (unit == TextUnit_Paragraph | |||
|| unit == TextUnit_Page | |||
|| unit == TextUnit_Document) | |||
{ | |||
*pRetVal = (direction == NextEndpointDirection::forwards ? 1 : -1); | |||
setEndpointChecked (endpoint, numCharacters); | |||
return S_OK; | |||
} | |||
for (int i = 0; i < std::abs (count); ++i) | |||
{ | |||
auto nextEndpoint = getNextEndpointPosition (textInterface, | |||
endpointToMove, | |||
unit, | |||
direction); | |||
if (nextEndpoint < 0) | |||
{ | |||
*pRetVal = (direction == NextEndpointDirection::forwards ? i : -i); | |||
setEndpointChecked (endpoint, endpointToMove); | |||
return S_OK; | |||
} | |||
endpointToMove = nextEndpoint; | |||
} | |||
*pRetVal = count; | |||
setEndpointChecked (endpoint, endpointToMove); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT RemoveFromSelection() override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
if (auto* textInterface = getHandler().getTextInterface()) | |||
{ | |||
textInterface->setSelection ({}); | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
JUCE_COMRESULT ScrollIntoView (BOOL) override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
JUCE_COMRESULT Select() override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
if (auto* textInterface = getHandler().getTextInterface()) | |||
{ | |||
textInterface->setSelection ({}); | |||
textInterface->setSelection (selectionRange); | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
private: | |||
enum class NextEndpointDirection { forwards, backwards }; | |||
static int getNextEndpointPosition (const AccessibilityTextInterface& textInterface, | |||
int currentPosition, | |||
TextUnit unit, | |||
NextEndpointDirection direction) | |||
{ | |||
auto isTextUnitSeparator = [unit] (const juce_wchar c) | |||
{ | |||
return ((unit == TextUnit_Word || unit == TextUnit_Format) && CharacterFunctions::isWhitespace (c)) | |||
|| (unit == TextUnit_Line && (c == '\r' || c == '\n')); | |||
}; | |||
constexpr int textBufferSize = 1024; | |||
int numChars = 0; | |||
if (direction == NextEndpointDirection::forwards) | |||
{ | |||
auto textBuffer = textInterface.getText ({ currentPosition, | |||
jmin (textInterface.getTotalNumCharacters(), currentPosition + textBufferSize) }); | |||
for (auto charPtr = textBuffer.getCharPointer(); ! charPtr.isEmpty();) | |||
{ | |||
auto character = charPtr.getAndAdvance(); | |||
++numChars; | |||
if (isTextUnitSeparator (character)) | |||
return currentPosition + numChars; | |||
} | |||
} | |||
else | |||
{ | |||
auto textBuffer = textInterface.getText ({ jmax (0, currentPosition - textBufferSize), | |||
currentPosition }); | |||
for (auto charPtr = textBuffer.end() - 1; charPtr != textBuffer.begin(); --charPtr) | |||
{ | |||
auto character = *charPtr; | |||
if (isTextUnitSeparator (character)) | |||
return currentPosition - numChars; | |||
++numChars; | |||
} | |||
} | |||
return -1; | |||
} | |||
void setEndpointChecked (TextPatternRangeEndpoint endpoint, int newEndpoint) | |||
{ | |||
if (endpoint == TextPatternRangeEndpoint_Start) | |||
{ | |||
if (selectionRange.getEnd() < newEndpoint) | |||
selectionRange.setEnd (newEndpoint); | |||
selectionRange.setStart (newEndpoint); | |||
} | |||
else | |||
{ | |||
if (selectionRange.getStart() > newEndpoint) | |||
selectionRange.setStart (newEndpoint); | |||
selectionRange.setEnd (newEndpoint); | |||
} | |||
} | |||
ComSmartPtr<UIATextProvider> owner; | |||
Range<int> selectionRange; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIATextRangeProvider) | |||
}; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIATextProvider) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,80 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
class UIAToggleProvider : public UIAProviderBase, | |||
public ComBaseClassHelper<IToggleProvider> | |||
{ | |||
public: | |||
explicit UIAToggleProvider (AccessibilityNativeHandle* nativeHandle) | |||
: UIAProviderBase (nativeHandle) | |||
{ | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT Toggle() override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
const auto& handler = getHandler(); | |||
if (handler.getActions().invoke (AccessibilityActionType::toggle)) | |||
{ | |||
VARIANT newValue; | |||
VariantHelpers::setInt (getCurrentToggleState(), &newValue); | |||
sendAccessibilityPropertyChangedEvent (handler, UIA_ToggleToggleStatePropertyId, newValue); | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
JUCE_COMRESULT get_ToggleState (ToggleState* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
*pRetVal = getCurrentToggleState(); | |||
return S_OK; | |||
}); | |||
} | |||
private: | |||
ToggleState getCurrentToggleState() const | |||
{ | |||
return getHandler().getCurrentState().isChecked() ? ToggleState_On | |||
: ToggleState_Off; | |||
} | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAToggleProvider) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,125 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
class UIATransformProvider : public UIAProviderBase, | |||
public ComBaseClassHelper<ITransformProvider> | |||
{ | |||
public: | |||
explicit UIATransformProvider (AccessibilityNativeHandle* nativeHandle) | |||
: UIAProviderBase (nativeHandle) | |||
{ | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT Move (double x, double y) override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
if (auto* peer = getPeer()) | |||
{ | |||
RECT rect; | |||
GetWindowRect ((HWND) peer->getNativeHandle(), &rect); | |||
rect.left = roundToInt (x); | |||
rect.top = roundToInt (y); | |||
auto bounds = Rectangle<int>::leftTopRightBottom (rect.left, rect.top, rect.right, rect.bottom); | |||
peer->setBounds (Desktop::getInstance().getDisplays().physicalToLogical (bounds), | |||
peer->isFullScreen()); | |||
} | |||
return S_OK; | |||
} | |||
JUCE_COMRESULT Resize (double width, double height) override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
if (auto* peer = getPeer()) | |||
{ | |||
auto scale = peer->getPlatformScaleFactor(); | |||
peer->getComponent().setSize (roundToInt (width / scale), | |||
roundToInt (height / scale)); | |||
} | |||
return S_OK; | |||
} | |||
JUCE_COMRESULT Rotate (double) override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
JUCE_COMRESULT get_CanMove (BOOL* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
*pRetVal = true; | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT get_CanResize (BOOL* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
if (auto* peer = getPeer()) | |||
*pRetVal = ((peer->getStyleFlags() & ComponentPeer::windowIsResizable) != 0); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT get_CanRotate (BOOL* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
*pRetVal = false; | |||
return S_OK; | |||
}); | |||
} | |||
private: | |||
ComponentPeer* getPeer() const | |||
{ | |||
return getHandler().getComponent().getPeer(); | |||
} | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIATransformProvider) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,121 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
class UIAValueProvider : public UIAProviderBase, | |||
public ComBaseClassHelper<IValueProvider> | |||
{ | |||
public: | |||
UIAValueProvider (AccessibilityNativeHandle* nativeHandle, bool editableText) | |||
: UIAProviderBase (nativeHandle), | |||
isEditableText (editableText) | |||
{ | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT SetValue (LPCWSTR val) override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
const auto& handler = getHandler(); | |||
const auto sendValuePropertyChangeMessage = [&]() | |||
{ | |||
VARIANT newValue; | |||
VariantHelpers::setString (getCurrentValueString(), &newValue); | |||
sendAccessibilityPropertyChangedEvent (handler, UIA_ValueValuePropertyId, newValue); | |||
}; | |||
if (isEditableText) | |||
{ | |||
handler.getTextInterface()->setText (String (val)); | |||
sendValuePropertyChangeMessage(); | |||
return S_OK; | |||
} | |||
if (auto* valueInterface = handler.getValueInterface()) | |||
{ | |||
if (! valueInterface->isReadOnly()) | |||
{ | |||
valueInterface->setValueAsString (String (val)); | |||
sendValuePropertyChangeMessage(); | |||
return S_OK; | |||
} | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
JUCE_COMRESULT get_Value (BSTR* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
auto currentValue = getCurrentValueString(); | |||
*pRetVal = SysAllocString ((const OLECHAR*) currentValue.toWideCharPointer()); | |||
return S_OK; | |||
}); | |||
} | |||
JUCE_COMRESULT get_IsReadOnly (BOOL* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&] | |||
{ | |||
if (! isEditableText) | |||
if (auto* valueInterface = getHandler().getValueInterface()) | |||
*pRetVal = valueInterface->isReadOnly(); | |||
return S_OK; | |||
}); | |||
} | |||
private: | |||
String getCurrentValueString() const | |||
{ | |||
if (isEditableText) | |||
if (auto* textInterface = getHandler().getTextInterface()) | |||
return textInterface->getText ({ 0, textInterface->getTotalNumCharacters() }); | |||
if (auto* valueInterface = getHandler().getValueInterface()) | |||
return valueInterface->getCurrentValueAsString(); | |||
jassertfalse; | |||
return {}; | |||
} | |||
const bool isEditableText; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAValueProvider) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,197 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
class UIAWindowProvider : public UIAProviderBase, | |||
public ComBaseClassHelper<IWindowProvider> | |||
{ | |||
public: | |||
explicit UIAWindowProvider (AccessibilityNativeHandle* nativeHandle) | |||
: UIAProviderBase (nativeHandle) | |||
{ | |||
} | |||
//============================================================================== | |||
JUCE_COMRESULT SetVisualState (WindowVisualState state) override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
if (auto* peer = getPeer()) | |||
{ | |||
switch (state) | |||
{ | |||
case WindowVisualState_Maximized: | |||
peer->setFullScreen (true); | |||
break; | |||
case WindowVisualState_Minimized: | |||
peer->setMinimised (true); | |||
break; | |||
case WindowVisualState_Normal: | |||
peer->setFullScreen (false); | |||
peer->setMinimised (false); | |||
break; | |||
default: | |||
break; | |||
} | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
JUCE_COMRESULT Close() override | |||
{ | |||
if (! isElementValid()) | |||
return UIA_E_ELEMENTNOTAVAILABLE; | |||
if (auto* peer = getPeer()) | |||
{ | |||
peer->handleUserClosingWindow(); | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
JUCE_COMRESULT WaitForInputIdle (int, BOOL* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [] | |||
{ | |||
return UIA_E_NOTSUPPORTED; | |||
}); | |||
} | |||
JUCE_COMRESULT get_CanMaximize (BOOL* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT | |||
{ | |||
if (auto* peer = getPeer()) | |||
{ | |||
*pRetVal = (peer->getStyleFlags() & ComponentPeer::windowHasMaximiseButton) != 0; | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
}); | |||
} | |||
JUCE_COMRESULT get_CanMinimize (BOOL* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT | |||
{ | |||
if (auto* peer = getPeer()) | |||
{ | |||
*pRetVal = (peer->getStyleFlags() & ComponentPeer::windowHasMinimiseButton) != 0; | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
}); | |||
} | |||
JUCE_COMRESULT get_IsModal (BOOL* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT | |||
{ | |||
if (auto* peer = getPeer()) | |||
{ | |||
*pRetVal = peer->getComponent().isCurrentlyModal(); | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
}); | |||
} | |||
JUCE_COMRESULT get_WindowVisualState (WindowVisualState* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT | |||
{ | |||
if (auto* peer = getPeer()) | |||
{ | |||
if (peer->isFullScreen()) | |||
*pRetVal = WindowVisualState_Maximized; | |||
else if (peer->isMinimised()) | |||
*pRetVal = WindowVisualState_Minimized; | |||
else | |||
*pRetVal = WindowVisualState_Normal; | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
}); | |||
} | |||
JUCE_COMRESULT get_WindowInteractionState (WindowInteractionState* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT | |||
{ | |||
if (auto* peer = getPeer()) | |||
{ | |||
*pRetVal = peer->getComponent().isCurrentlyBlockedByAnotherModalComponent() | |||
? WindowInteractionState::WindowInteractionState_BlockedByModalWindow | |||
: WindowInteractionState::WindowInteractionState_Running; | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
}); | |||
} | |||
JUCE_COMRESULT get_IsTopmost (BOOL* pRetVal) override | |||
{ | |||
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT | |||
{ | |||
if (auto* peer = getPeer()) | |||
{ | |||
*pRetVal = peer->isFocused(); | |||
return S_OK; | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
}); | |||
} | |||
private: | |||
ComponentPeer* getPeer() const | |||
{ | |||
return getHandler().getComponent().getPeer(); | |||
} | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAWindowProvider) | |||
}; | |||
} // namespace juce |
@@ -0,0 +1,158 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2020 - Raw Material Software Limited | |||
JUCE is an open source library subject to commercial or open-source | |||
licensing. | |||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License | |||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). | |||
End User License Agreement: www.juce.com/juce-6-licence | |||
Privacy Policy: www.juce.com/juce-privacy-policy | |||
Or: You may also use this code under the terms of the GPL v3 (see | |||
www.gnu.org/licenses). | |||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||
DISCLAIMED. | |||
============================================================================== | |||
*/ | |||
namespace juce | |||
{ | |||
class WindowsUIAWrapper : public DeletedAtShutdown | |||
{ | |||
public: | |||
bool isLoaded() const noexcept | |||
{ | |||
return uiaReturnRawElementProvider != nullptr | |||
&& uiaHostProviderFromHwnd != nullptr | |||
&& uiaRaiseAutomationPropertyChangedEvent != nullptr | |||
&& uiaRaiseAutomationEvent != nullptr | |||
&& uiaClientsAreListening != nullptr | |||
&& uiaDisconnectProvider != nullptr | |||
&& uiaDisconnectAllProviders != nullptr; | |||
} | |||
//============================================================================== | |||
LRESULT returnRawElementProvider (HWND hwnd, WPARAM wParam, LPARAM lParam, IRawElementProviderSimple* provider) | |||
{ | |||
return uiaReturnRawElementProvider != nullptr ? uiaReturnRawElementProvider (hwnd, wParam, lParam, provider) | |||
: (LRESULT) nullptr; | |||
} | |||
JUCE_COMRESULT hostProviderFromHwnd (HWND hwnd, IRawElementProviderSimple** provider) | |||
{ | |||
return uiaHostProviderFromHwnd != nullptr ? uiaHostProviderFromHwnd (hwnd, provider) | |||
: UIA_E_NOTSUPPORTED; | |||
} | |||
JUCE_COMRESULT raiseAutomationPropertyChangedEvent (IRawElementProviderSimple* provider, PROPERTYID propID, VARIANT oldValue, VARIANT newValue) | |||
{ | |||
return uiaRaiseAutomationPropertyChangedEvent != nullptr ? uiaRaiseAutomationPropertyChangedEvent (provider, propID, oldValue, newValue) | |||
: UIA_E_NOTSUPPORTED; | |||
} | |||
JUCE_COMRESULT raiseAutomationEvent (IRawElementProviderSimple* provider, EVENTID eventID) | |||
{ | |||
return uiaRaiseAutomationEvent != nullptr ? uiaRaiseAutomationEvent (provider, eventID) | |||
: UIA_E_NOTSUPPORTED; | |||
} | |||
BOOL clientsAreListening() | |||
{ | |||
return uiaClientsAreListening != nullptr ? uiaClientsAreListening() | |||
: false; | |||
} | |||
JUCE_COMRESULT disconnectProvider (IRawElementProviderSimple* provider) | |||
{ | |||
if (uiaDisconnectProvider != nullptr) | |||
{ | |||
const ScopedValueSetter<IRawElementProviderSimple*> disconnectingProviderSetter (disconnectingProvider, provider); | |||
return uiaDisconnectProvider (provider); | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
JUCE_COMRESULT disconnectAllProviders() | |||
{ | |||
if (uiaDisconnectAllProviders != nullptr) | |||
{ | |||
const ScopedValueSetter<bool> disconnectingAllProvidersSetter (disconnectingAllProviders, true); | |||
return uiaDisconnectAllProviders(); | |||
} | |||
return UIA_E_NOTSUPPORTED; | |||
} | |||
//============================================================================== | |||
bool isProviderDisconnecting (IRawElementProviderSimple* provider) | |||
{ | |||
return disconnectingProvider == provider || disconnectingAllProviders; | |||
} | |||
//============================================================================== | |||
JUCE_DECLARE_SINGLETON_SINGLETHREADED_MINIMAL (WindowsUIAWrapper) | |||
private: | |||
//============================================================================== | |||
WindowsUIAWrapper() | |||
{ | |||
// force UIA COM library initialisation here to prevent an exception when calling methods from SendMessage() | |||
if (isLoaded()) | |||
returnRawElementProvider (nullptr, 0, 0, nullptr); | |||
else | |||
jassertfalse; // UIAutomationCore could not be loaded! | |||
} | |||
~WindowsUIAWrapper() | |||
{ | |||
disconnectAllProviders(); | |||
if (uiaHandle != nullptr) | |||
::FreeLibrary (uiaHandle); | |||
clearSingletonInstance(); | |||
} | |||
//============================================================================== | |||
template<typename FuncType> | |||
static FuncType getUiaFunction (HMODULE module, StringRef funcName) | |||
{ | |||
return (FuncType) GetProcAddress (module, funcName); | |||
} | |||
//============================================================================== | |||
using UiaReturnRawElementProviderFunc = LRESULT (WINAPI*) (HWND, WPARAM, LPARAM, IRawElementProviderSimple*); | |||
using UiaHostProviderFromHwndFunc = HRESULT (WINAPI*) (HWND, IRawElementProviderSimple**); | |||
using UiaRaiseAutomationPropertyChangedEventFunc = HRESULT (WINAPI*) (IRawElementProviderSimple*, PROPERTYID, VARIANT, VARIANT); | |||
using UiaRaiseAutomationEventFunc = HRESULT (WINAPI*) (IRawElementProviderSimple*, EVENTID); | |||
using UiaClientsAreListeningFunc = BOOL (WINAPI*) (); | |||
using UiaDisconnectProviderFunc = HRESULT (WINAPI*) (IRawElementProviderSimple*); | |||
using UiaDisconnectAllProvidersFunc = HRESULT (WINAPI*) (); | |||
HMODULE uiaHandle = ::LoadLibraryA ("UIAutomationCore.dll"); | |||
UiaReturnRawElementProviderFunc uiaReturnRawElementProvider = getUiaFunction<UiaReturnRawElementProviderFunc> (uiaHandle, "UiaReturnRawElementProvider"); | |||
UiaHostProviderFromHwndFunc uiaHostProviderFromHwnd = getUiaFunction<UiaHostProviderFromHwndFunc> (uiaHandle, "UiaHostProviderFromHwnd"); | |||
UiaRaiseAutomationPropertyChangedEventFunc uiaRaiseAutomationPropertyChangedEvent = getUiaFunction<UiaRaiseAutomationPropertyChangedEventFunc> (uiaHandle, "UiaRaiseAutomationPropertyChangedEvent"); | |||
UiaRaiseAutomationEventFunc uiaRaiseAutomationEvent = getUiaFunction<UiaRaiseAutomationEventFunc> (uiaHandle, "UiaRaiseAutomationEvent"); | |||
UiaClientsAreListeningFunc uiaClientsAreListening = getUiaFunction<UiaClientsAreListeningFunc> (uiaHandle, "UiaClientsAreListening"); | |||
UiaDisconnectProviderFunc uiaDisconnectProvider = getUiaFunction<UiaDisconnectProviderFunc> (uiaHandle, "UiaDisconnectProvider"); | |||
UiaDisconnectAllProvidersFunc uiaDisconnectAllProviders = getUiaFunction<UiaDisconnectAllProvidersFunc> (uiaHandle, "UiaDisconnectAllProviders"); | |||
IRawElementProviderSimple* disconnectingProvider = nullptr; | |||
bool disconnectingAllProviders = false; | |||
//============================================================================== | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WindowsUIAWrapper) | |||
}; | |||
} // namespace juce |
@@ -56,26 +56,6 @@ namespace juce | |||
namespace juce | |||
{ | |||
//============================================================================== | |||
static CGFloat getMainScreenHeight() noexcept | |||
{ | |||
if ([[NSScreen screens] count] == 0) | |||
return 0.0f; | |||
return [[[NSScreen screens] objectAtIndex: 0] frame].size.height; | |||
} | |||
static void flipScreenRect (NSRect& r) noexcept | |||
{ | |||
r.origin.y = getMainScreenHeight() - (r.origin.y + r.size.height); | |||
} | |||
static NSRect flippedScreenRect (NSRect r) noexcept | |||
{ | |||
flipScreenRect (r); | |||
return r; | |||
} | |||
//============================================================================== | |||
class NSViewComponentPeer : public ComponentPeer, | |||
private Timer | |||
@@ -124,7 +104,7 @@ public: | |||
{ | |||
r.origin.x = (CGFloat) component.getX(); | |||
r.origin.y = (CGFloat) component.getY(); | |||
flipScreenRect (r); | |||
r = flippedScreenRect (r); | |||
window = [createWindowInstance() initWithContentRect: r | |||
styleMask: getNSWindowStyleMask (windowStyleFlags) | |||
@@ -323,7 +303,7 @@ public: | |||
r = [[view superview] convertRect: r toView: nil]; | |||
r = [viewWindow convertRectToScreen: r]; | |||
flipScreenRect (r); | |||
r = flippedScreenRect (r); | |||
} | |||
return convertToRectInt (r); | |||
@@ -1669,68 +1649,100 @@ const SEL NSViewComponentPeer::becomeKeySelector = @selector (becomeKey:); | |||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE | |||
//============================================================================== | |||
struct JuceNSViewClass : public ObjCClass<NSView> | |||
template <typename Base> | |||
struct NSViewComponentPeerWrapper : public Base | |||
{ | |||
JuceNSViewClass() : ObjCClass<NSView> ("JUCEView_") | |||
{ | |||
addIvar<NSViewComponentPeer*> ("owner"); | |||
addMethod (@selector (isOpaque), isOpaque, "c@:"); | |||
addMethod (@selector (drawRect:), drawRect, "v@:", @encode (NSRect)); | |||
addMethod (@selector (mouseDown:), mouseDown, "v@:@"); | |||
addMethod (@selector (mouseUp:), mouseUp, "v@:@"); | |||
addMethod (@selector (mouseDragged:), mouseDragged, "v@:@"); | |||
addMethod (@selector (mouseMoved:), mouseMoved, "v@:@"); | |||
addMethod (@selector (mouseEntered:), mouseEntered, "v@:@"); | |||
addMethod (@selector (mouseExited:), mouseExited, "v@:@"); | |||
addMethod (@selector (rightMouseDown:), mouseDown, "v@:@"); | |||
addMethod (@selector (rightMouseDragged:), mouseDragged, "v@:@"); | |||
addMethod (@selector (rightMouseUp:), mouseUp, "v@:@"); | |||
addMethod (@selector (otherMouseDown:), mouseDown, "v@:@"); | |||
addMethod (@selector (otherMouseDragged:), mouseDragged, "v@:@"); | |||
addMethod (@selector (otherMouseUp:), mouseUp, "v@:@"); | |||
addMethod (@selector (scrollWheel:), scrollWheel, "v@:@"); | |||
addMethod (@selector (magnifyWithEvent:), magnify, "v@:@"); | |||
addMethod (@selector (acceptsFirstMouse:), acceptsFirstMouse, "c@:@"); | |||
addMethod (@selector (windowWillMiniaturize:), windowWillMiniaturize, "v@:@"); | |||
addMethod (@selector (windowDidDeminiaturize:), windowDidDeminiaturize, "v@:@"); | |||
addMethod (@selector (wantsDefaultClipping), wantsDefaultClipping, "c@:"); | |||
addMethod (@selector (worksWhenModal), worksWhenModal, "c@:"); | |||
addMethod (@selector (viewWillMoveToWindow:), willMoveToWindow, "v@:@"); | |||
addMethod (@selector (viewDidMoveToWindow), viewDidMoveToWindow, "v@:"); | |||
addMethod (@selector (viewWillDraw), viewWillDraw, "v@:"); | |||
addMethod (@selector (keyDown:), keyDown, "v@:@"); | |||
addMethod (@selector (keyUp:), keyUp, "v@:@"); | |||
addMethod (@selector (insertText:), insertText, "v@:@"); | |||
addMethod (@selector (doCommandBySelector:), doCommandBySelector, "v@::"); | |||
addMethod (@selector (setMarkedText:selectedRange:), setMarkedText, "v@:@", @encode (NSRange)); | |||
addMethod (@selector (unmarkText), unmarkText, "v@:"); | |||
addMethod (@selector (hasMarkedText), hasMarkedText, "c@:"); | |||
addMethod (@selector (conversationIdentifier), conversationIdentifier, "l@:"); | |||
addMethod (@selector (attributedSubstringFromRange:), attributedSubstringFromRange, "@@:", @encode (NSRange)); | |||
addMethod (@selector (markedRange), markedRange, @encode (NSRange), "@:"); | |||
addMethod (@selector (selectedRange), selectedRange, @encode (NSRange), "@:"); | |||
addMethod (@selector (firstRectForCharacterRange:), firstRectForCharacterRange, @encode (NSRect), "@:", @encode (NSRange)); | |||
addMethod (@selector (characterIndexForPoint:), characterIndexForPoint, "L@:", @encode (NSPoint)); | |||
addMethod (@selector (validAttributesForMarkedText), validAttributesForMarkedText, "@@:"); | |||
addMethod (@selector (flagsChanged:), flagsChanged, "v@:@"); | |||
addMethod (@selector (becomeFirstResponder), becomeFirstResponder, "c@:"); | |||
addMethod (@selector (resignFirstResponder), resignFirstResponder, "c@:"); | |||
addMethod (@selector (acceptsFirstResponder), acceptsFirstResponder, "c@:"); | |||
addMethod (@selector (draggingEntered:), draggingEntered, @encode (NSDragOperation), "@:@"); | |||
addMethod (@selector (draggingUpdated:), draggingUpdated, @encode (NSDragOperation), "@:@"); | |||
addMethod (@selector (draggingEnded:), draggingEnded, "v@:@"); | |||
addMethod (@selector (draggingExited:), draggingExited, "v@:@"); | |||
addMethod (@selector (prepareForDragOperation:), prepareForDragOperation, "c@:@"); | |||
addMethod (@selector (performDragOperation:), performDragOperation, "c@:@"); | |||
addMethod (@selector (concludeDragOperation:), concludeDragOperation, "v@:@"); | |||
addMethod (@selector (paste:), paste, "v@:@"); | |||
addMethod (@selector (copy:), copy, "v@:@"); | |||
addMethod (@selector (cut:), cut, "v@:@"); | |||
addMethod (@selector (selectAll:), selectAll, "v@:@"); | |||
explicit NSViewComponentPeerWrapper (const char* baseName) | |||
: Base (baseName) | |||
{ | |||
Base::template addIvar<NSViewComponentPeer*> ("owner"); | |||
} | |||
static NSViewComponentPeer* getOwner (id self) | |||
{ | |||
return Base::template getIvar<NSViewComponentPeer*> (self, "owner"); | |||
} | |||
static id getAccessibleChild (id self) | |||
{ | |||
if (auto* owner = getOwner (self)) | |||
if (auto* handler = owner->getComponent().getAccessibilityHandler()) | |||
return (id) handler->getNativeImplementation(); | |||
return nil; | |||
} | |||
}; | |||
struct JuceNSViewClass : public NSViewComponentPeerWrapper<ObjCClass<NSView>> | |||
{ | |||
JuceNSViewClass() : NSViewComponentPeerWrapper ("JUCEView_") | |||
{ | |||
addMethod (@selector (isOpaque), isOpaque, "c@:"); | |||
addMethod (@selector (drawRect:), drawRect, "v@:", @encode (NSRect)); | |||
addMethod (@selector (mouseDown:), mouseDown, "v@:@"); | |||
addMethod (@selector (mouseUp:), mouseUp, "v@:@"); | |||
addMethod (@selector (mouseDragged:), mouseDragged, "v@:@"); | |||
addMethod (@selector (mouseMoved:), mouseMoved, "v@:@"); | |||
addMethod (@selector (mouseEntered:), mouseEntered, "v@:@"); | |||
addMethod (@selector (mouseExited:), mouseExited, "v@:@"); | |||
addMethod (@selector (rightMouseDown:), mouseDown, "v@:@"); | |||
addMethod (@selector (rightMouseDragged:), mouseDragged, "v@:@"); | |||
addMethod (@selector (rightMouseUp:), mouseUp, "v@:@"); | |||
addMethod (@selector (otherMouseDown:), mouseDown, "v@:@"); | |||
addMethod (@selector (otherMouseDragged:), mouseDragged, "v@:@"); | |||
addMethod (@selector (otherMouseUp:), mouseUp, "v@:@"); | |||
addMethod (@selector (scrollWheel:), scrollWheel, "v@:@"); | |||
addMethod (@selector (magnifyWithEvent:), magnify, "v@:@"); | |||
addMethod (@selector (acceptsFirstMouse:), acceptsFirstMouse, "c@:@"); | |||
addMethod (@selector (windowWillMiniaturize:), windowWillMiniaturize, "v@:@"); | |||
addMethod (@selector (windowDidDeminiaturize:), windowDidDeminiaturize, "v@:@"); | |||
addMethod (@selector (wantsDefaultClipping), wantsDefaultClipping, "c@:"); | |||
addMethod (@selector (worksWhenModal), worksWhenModal, "c@:"); | |||
addMethod (@selector (viewDidMoveToWindow), viewDidMoveToWindow, "v@:"); | |||
addMethod (@selector (viewWillDraw), viewWillDraw, "v@:"); | |||
addMethod (@selector (keyDown:), keyDown, "v@:@"); | |||
addMethod (@selector (keyUp:), keyUp, "v@:@"); | |||
addMethod (@selector (insertText:), insertText, "v@:@"); | |||
addMethod (@selector (doCommandBySelector:), doCommandBySelector, "v@::"); | |||
addMethod (@selector (setMarkedText:selectedRange:), setMarkedText, "v@:@", @encode (NSRange)); | |||
addMethod (@selector (unmarkText), unmarkText, "v@:"); | |||
addMethod (@selector (hasMarkedText), hasMarkedText, "c@:"); | |||
addMethod (@selector (conversationIdentifier), conversationIdentifier, "l@:"); | |||
addMethod (@selector (attributedSubstringFromRange:), attributedSubstringFromRange, "@@:", @encode (NSRange)); | |||
addMethod (@selector (markedRange), markedRange, @encode (NSRange), "@:"); | |||
addMethod (@selector (selectedRange), selectedRange, @encode (NSRange), "@:"); | |||
addMethod (@selector (firstRectForCharacterRange:), firstRectForCharacterRange, @encode (NSRect), "@:", @encode (NSRange)); | |||
addMethod (@selector (characterIndexForPoint:), characterIndexForPoint, "L@:", @encode (NSPoint)); | |||
addMethod (@selector (validAttributesForMarkedText), validAttributesForMarkedText, "@@:"); | |||
addMethod (@selector (flagsChanged:), flagsChanged, "v@:@"); | |||
addMethod (@selector (becomeFirstResponder), becomeFirstResponder, "c@:"); | |||
addMethod (@selector (resignFirstResponder), resignFirstResponder, "c@:"); | |||
addMethod (@selector (acceptsFirstResponder), acceptsFirstResponder, "c@:"); | |||
addMethod (@selector (draggingEntered:), draggingEntered, @encode (NSDragOperation), "@:@"); | |||
addMethod (@selector (draggingUpdated:), draggingUpdated, @encode (NSDragOperation), "@:@"); | |||
addMethod (@selector (draggingEnded:), draggingEnded, "v@:@"); | |||
addMethod (@selector (draggingExited:), draggingExited, "v@:@"); | |||
addMethod (@selector (prepareForDragOperation:), prepareForDragOperation, "c@:@"); | |||
addMethod (@selector (performDragOperation:), performDragOperation, "c@:@"); | |||
addMethod (@selector (concludeDragOperation:), concludeDragOperation, "v@:@"); | |||
addMethod (@selector (paste:), paste, "v@:@"); | |||
addMethod (@selector (copy:), copy, "v@:@"); | |||
addMethod (@selector (cut:), cut, "v@:@"); | |||
addMethod (@selector (selectAll:), selectAll, "v@:@"); | |||
addMethod (@selector (viewWillMoveToWindow:), willMoveToWindow, "v@:@"); | |||
addMethod (@selector (isAccessibilityElement), getIsAccessibilityElement, "c@:"); | |||
addMethod (@selector (accessibilityChildren), getAccessibilityChildren, "@@:"); | |||
addMethod (@selector (accessibilityHitTest:), accessibilityHitTest, "@@:", @encode (NSPoint)); | |||
addMethod (@selector (accessibilityFocusedUIElement), getAccessibilityFocusedUIElement, "@@:"); | |||
// deprecated methods required for backwards compatibility | |||
addMethod (@selector (accessibilityIsIgnored), getAccessibilityIsIgnored, "c@:"); | |||
addMethod (@selector (accessibilityAttributeValue:), getAccessibilityAttributeValue, "@@:@"); | |||
addMethod (@selector (isFlipped), isFlipped, "c@:"); | |||
@@ -1746,11 +1758,6 @@ struct JuceNSViewClass : public ObjCClass<NSView> | |||
} | |||
private: | |||
static NSViewComponentPeer* getOwner (id self) | |||
{ | |||
return getIvar<NSViewComponentPeer*> (self, "owner"); | |||
} | |||
static void mouseDown (id self, SEL s, NSEvent* ev) | |||
{ | |||
if (JUCEApplicationBase::isStandaloneApp()) | |||
@@ -2072,15 +2079,47 @@ private: | |||
} | |||
static void concludeDragOperation (id, SEL, id<NSDraggingInfo>) {} | |||
//============================================================================== | |||
static BOOL getIsAccessibilityElement (id, SEL) | |||
{ | |||
return NO; | |||
} | |||
static NSArray* getAccessibilityChildren (id self, SEL) | |||
{ | |||
return NSAccessibilityUnignoredChildrenForOnlyChild (getAccessibleChild (self)); | |||
} | |||
static id accessibilityHitTest (id self, SEL, NSPoint point) | |||
{ | |||
return [getAccessibleChild (self) accessibilityHitTest: point]; | |||
} | |||
static id getAccessibilityFocusedUIElement (id self, SEL) | |||
{ | |||
return [getAccessibleChild (self) accessibilityFocusedUIElement]; | |||
} | |||
static BOOL getAccessibilityIsIgnored (id self, SEL) | |||
{ | |||
return ! [self isAccessibilityElement]; | |||
} | |||
static id getAccessibilityAttributeValue (id self, SEL, NSString* attribute) | |||
{ | |||
if ([attribute isEqualToString: NSAccessibilityChildrenAttribute]) | |||
return getAccessibilityChildren (self, {}); | |||
return sendSuperclassMessage<id> (self, @selector (accessibilityAttributeValue:), attribute); | |||
} | |||
}; | |||
//============================================================================== | |||
struct JuceNSWindowClass : public ObjCClass<NSWindow> | |||
struct JuceNSWindowClass : public NSViewComponentPeerWrapper<ObjCClass<NSWindow>> | |||
{ | |||
JuceNSWindowClass() : ObjCClass<NSWindow> ("JUCEWindow_") | |||
JuceNSWindowClass() : NSViewComponentPeerWrapper ("JUCEWindow_") | |||
{ | |||
addIvar<NSViewComponentPeer*> ("owner"); | |||
addMethod (@selector (canBecomeKeyWindow), canBecomeKeyWindow, "c@:"); | |||
addMethod (@selector (canBecomeMainWindow), canBecomeMainWindow, "c@:"); | |||
addMethod (@selector (becomeKeyWindow), becomeKeyWindow, "v@:"); | |||
@@ -2096,6 +2135,12 @@ struct JuceNSWindowClass : public ObjCClass<NSWindow> | |||
addMethod (@selector (window:shouldPopUpDocumentPathMenu:), shouldPopUpPathMenu, "B@:@", @encode (NSMenu*)); | |||
addMethod (@selector (isFlipped), isFlipped, "c@:"); | |||
addMethod (@selector (accessibilityLabel), getAccessibilityLabel, "@@:"); | |||
addMethod (@selector (accessibilityTopLevelUIElement), getAccessibilityWindow, "@@:"); | |||
addMethod (@selector (accessibilityWindow), getAccessibilityWindow, "@@:"); | |||
addMethod (@selector (accessibilityRole), getAccessibilityRole, "@@:"); | |||
addMethod (@selector (accessibilitySubrole), getAccessibilitySubrole, "@@:"); | |||
addMethod (@selector (window:shouldDragDocumentWithEvent:from:withPasteboard:), | |||
shouldAllowIconDrag, "B@:@", @encode (NSEvent*), @encode (NSPoint), @encode (NSPasteboard*)); | |||
@@ -2105,11 +2150,6 @@ struct JuceNSWindowClass : public ObjCClass<NSWindow> | |||
} | |||
private: | |||
static NSViewComponentPeer* getOwner (id self) | |||
{ | |||
return getIvar<NSViewComponentPeer*> (self, "owner"); | |||
} | |||
//============================================================================== | |||
static BOOL isFlipped (id, SEL) { return true; } | |||
@@ -2249,6 +2289,26 @@ private: | |||
return false; | |||
} | |||
static NSString* getAccessibilityLabel (id self, SEL) | |||
{ | |||
return [getAccessibleChild (self) accessibilityLabel]; | |||
} | |||
static id getAccessibilityWindow (id self, SEL) | |||
{ | |||
return self; | |||
} | |||
static NSAccessibilityRole getAccessibilityRole (id, SEL) | |||
{ | |||
return NSAccessibilityWindowRole; | |||
} | |||
static NSAccessibilityRole getAccessibilitySubrole (id self, SEL) | |||
{ | |||
return [getAccessibleChild (self) accessibilitySubrole]; | |||
} | |||
}; | |||
NSView* NSViewComponentPeer::createViewInstance() | |||
@@ -63,6 +63,14 @@ static bool shouldDeactivateTitleBar = true; | |||
void* getUser32Function (const char*); | |||
namespace WindowsAccessibility | |||
{ | |||
void initialiseUIAWrapper(); | |||
long getUiaRootObjectId(); | |||
bool handleWmGetObject (AccessibilityHandler*, WPARAM, LPARAM, LRESULT*); | |||
void revokeUIAMapEntriesForWindow (HWND); | |||
} | |||
#if JUCE_DEBUG | |||
int numActiveScopedDpiAwarenessDisablers = 0; | |||
bool isInScopedDPIAwarenessDisabler() { return numActiveScopedDpiAwarenessDisablers > 0; } | |||
@@ -1372,12 +1380,14 @@ public: | |||
parentToAddTo (parent), | |||
currentRenderingEngine (softwareRenderingEngine) | |||
{ | |||
// make sure that the UIA wrapper singleton is loaded | |||
WindowsAccessibility::initialiseUIAWrapper(); | |||
callFunctionIfNotLocked (&createWindowCallback, this); | |||
setTitle (component.getName()); | |||
updateShadower(); | |||
// make sure that the on-screen keyboard code is loaded | |||
OnScreenKeyboard::getInstance(); | |||
getNativeRealtimeModifiers = [] | |||
@@ -1397,13 +1407,15 @@ public: | |||
~HWNDComponentPeer() | |||
{ | |||
// do this first to avoid messages arriving for this window before it's destroyed | |||
JuceWindowIdentifier::setAsJUCEWindow (hwnd, false); | |||
if (isAccessibilityActive) | |||
WindowsAccessibility::revokeUIAMapEntriesForWindow (hwnd); | |||
shadower = nullptr; | |||
currentTouches.deleteAllTouchesForPeer (this); | |||
// do this before the next bit to avoid messages arriving for this window | |||
// before it's destroyed | |||
JuceWindowIdentifier::setAsJUCEWindow (hwnd, false); | |||
callFunctionIfNotLocked (&destroyWindowCallback, (void*) hwnd); | |||
if (currentWindowIcon != nullptr) | |||
@@ -1989,6 +2001,8 @@ private: | |||
double scaleFactor = 1.0; | |||
bool isInDPIChange = false; | |||
bool isAccessibilityActive = false; | |||
//============================================================================== | |||
static MultiTouchMapper<DWORD> currentTouches; | |||
@@ -3907,6 +3921,24 @@ private: | |||
case WM_GETDLGCODE: | |||
return DLGC_WANTALLKEYS; | |||
case WM_GETOBJECT: | |||
{ | |||
if (static_cast<long> (lParam) == WindowsAccessibility::getUiaRootObjectId()) | |||
{ | |||
if (auto* handler = component.getAccessibilityHandler()) | |||
{ | |||
LRESULT res = 0; | |||
if (WindowsAccessibility::handleWmGetObject (handler, wParam, lParam, &res)) | |||
{ | |||
isAccessibilityActive = true; | |||
return res; | |||
} | |||
} | |||
} | |||
break; | |||
} | |||
default: | |||
break; | |||
} | |||
@@ -204,7 +204,7 @@ void PropertyPanel::init() | |||
addAndMakeVisible (viewport); | |||
viewport.setViewedComponent (propertyHolderComponent = new PropertyHolderComponent()); | |||
viewport.setFocusContainer (true); | |||
viewport.setFocusContainerType (FocusContainerType::keyboardFocusContainer); | |||
} | |||
PropertyPanel::~PropertyPanel() | |||
@@ -424,6 +424,7 @@ void ComboBox::lookAndFeelChanged() | |||
label->onTextChange = [this] { triggerAsyncUpdate(); }; | |||
label->addMouseListener (this, false); | |||
label->setAccessible (labelEditableState == labelIsEditable); | |||
label->setColour (Label::backgroundColourId, Colours::transparentBlack); | |||
label->setColour (Label::textColourId, findColour (ComboBox::textColourId)); | |||
@@ -641,4 +642,10 @@ void ComboBox::setSelectedItemIndex (const int index, const bool dontSendChange) | |||
void ComboBox::setSelectedId (const int newItemId, const bool dontSendChange) { setSelectedId (newItemId, dontSendChange ? dontSendNotification : sendNotification); } | |||
void ComboBox::setText (const String& newText, const bool dontSendChange) { setText (newText, dontSendChange ? dontSendNotification : sendNotification); } | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> ComboBox::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<ComboBoxAccessibilityHandler> (*this); | |||
} | |||
} // namespace juce |
@@ -419,6 +419,8 @@ public: | |||
void valueChanged (Value&) override; | |||
/** @internal */ | |||
void parentHierarchyChanged() override; | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
// These methods' bool parameters have changed: see their new method signatures. | |||
JUCE_DEPRECATED (void clear (bool)); | |||
@@ -80,4 +80,10 @@ void ImageComponent::paint (Graphics& g) | |||
g.drawImage (image, getLocalBounds().toFloat(), placement); | |||
} | |||
//============================================================================== | |||
std::unique_ptr<AccessibilityHandler> ImageComponent::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::image); | |||
} | |||
} // namespace juce |
@@ -68,6 +68,8 @@ public: | |||
//============================================================================== | |||
/** @internal */ | |||
void paint (Graphics&) override; | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
private: | |||
Image image; | |||
@@ -105,8 +105,13 @@ void Label::setEditable (bool editOnSingleClick, | |||
editDoubleClick = editOnDoubleClick; | |||
lossOfFocusDiscardsChanges = lossOfFocusDiscards; | |||
setWantsKeyboardFocus (editOnSingleClick || editOnDoubleClick); | |||
setFocusContainer (editOnSingleClick || editOnDoubleClick); | |||
const auto isKeybordFocusable = (editOnSingleClick || editOnDoubleClick); | |||
setWantsKeyboardFocus (isKeybordFocusable); | |||
setFocusContainerType (isKeybordFocusable ? FocusContainerType::keyboardFocusContainer | |||
: FocusContainerType::none); | |||
invalidateAccessibilityHandler(); | |||
} | |||
void Label::setJustificationType (Justification newJustification) | |||
@@ -221,6 +226,7 @@ void Label::showEditor() | |||
if (editor == nullptr) | |||
{ | |||
editor.reset (createEditorComponent()); | |||
editor->setSize (10, 10); | |||
addAndMakeVisible (editor.get()); | |||
editor->setText (getText(), false); | |||
editor->setKeyboardType (keyboardType); | |||
@@ -351,7 +357,9 @@ void Label::mouseDoubleClick (const MouseEvent& e) | |||
if (editDoubleClick | |||
&& isEnabled() | |||
&& ! e.mods.isPopupMenu()) | |||
{ | |||
showEditor(); | |||
} | |||
} | |||
void Label::resized() | |||
@@ -364,8 +372,11 @@ void Label::focusGained (FocusChangeType cause) | |||
{ | |||
if (editSingleClick | |||
&& isEnabled() | |||
&& cause == focusChangedByTabKey) | |||
&& (cause == focusChangedByTabKey | |||
|| (cause == focusChangedDirectly && ! isCurrentlyModal()))) | |||
{ | |||
showEditor(); | |||
} | |||
} | |||
void Label::enablementChanged() | |||
@@ -393,21 +404,45 @@ void Label::setMinimumHorizontalScale (const float newScale) | |||
class LabelKeyboardFocusTraverser : public KeyboardFocusTraverser | |||
{ | |||
public: | |||
LabelKeyboardFocusTraverser() {} | |||
explicit LabelKeyboardFocusTraverser (Label& l) : owner (l) {} | |||
Component* getDefaultComponent (Component* parent) override | |||
{ | |||
auto getContainer = [&] | |||
{ | |||
if (owner.getCurrentTextEditor() != nullptr && parent == &owner) | |||
return owner.findKeyboardFocusContainer(); | |||
return parent; | |||
}; | |||
Component* getNextComponent (Component* c) override { return KeyboardFocusTraverser::getNextComponent (getComp (c)); } | |||
Component* getPreviousComponent (Component* c) override { return KeyboardFocusTraverser::getPreviousComponent (getComp (c)); } | |||
if (auto* container = getContainer()) | |||
KeyboardFocusTraverser::getDefaultComponent (container); | |||
static Component* getComp (Component* current) | |||
return nullptr; | |||
} | |||
Component* getNextComponent (Component* c) override { return KeyboardFocusTraverser::getNextComponent (getComp (c)); } | |||
Component* getPreviousComponent (Component* c) override { return KeyboardFocusTraverser::getPreviousComponent (getComp (c)); } | |||
private: | |||
Component* getComp (Component* current) const | |||
{ | |||
return dynamic_cast<TextEditor*> (current) != nullptr | |||
? current->getParentComponent() : current; | |||
if (auto* ed = owner.getCurrentTextEditor()) | |||
if (current == ed) | |||
return current->getParentComponent(); | |||
return current; | |||
} | |||
Label& owner; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LabelKeyboardFocusTraverser) | |||
}; | |||
KeyboardFocusTraverser* Label::createFocusTraverser() | |||
std::unique_ptr<ComponentTraverser> Label::createKeyboardFocusTraverser() | |||
{ | |||
return new LabelKeyboardFocusTraverser(); | |||
return std::make_unique<LabelKeyboardFocusTraverser> (*this); | |||
} | |||
//============================================================================== | |||
@@ -480,4 +515,9 @@ void Label::textEditorFocusLost (TextEditor& ed) | |||
textEditorTextChanged (ed); | |||
} | |||
std::unique_ptr<AccessibilityHandler> Label::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<LabelAccessibilityHandler> (*this); | |||
} | |||
} // namespace juce |
@@ -323,7 +323,7 @@ protected: | |||
/** @internal */ | |||
void enablementChanged() override; | |||
/** @internal */ | |||
KeyboardFocusTraverser* createFocusTraverser() override; | |||
std::unique_ptr<ComponentTraverser> createKeyboardFocusTraverser() override; | |||
/** @internal */ | |||
void textEditorTextChanged (TextEditor&) override; | |||
/** @internal */ | |||
@@ -338,6 +338,8 @@ protected: | |||
void valueChanged (Value&) override; | |||
/** @internal */ | |||
void callChangeListeners(); | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
private: | |||
//============================================================================== | |||
@@ -26,6 +26,34 @@ | |||
namespace juce | |||
{ | |||
template<typename RowHandlerType, typename RowComponent> | |||
static AccessibilityActions getListRowAccessibilityActions (RowHandlerType& handler, RowComponent& rowComponent) | |||
{ | |||
auto onFocus = [&rowComponent] | |||
{ | |||
rowComponent.owner.scrollToEnsureRowIsOnscreen (rowComponent.row); | |||
rowComponent.owner.selectRow (rowComponent.row); | |||
}; | |||
auto onPress = [&rowComponent, onFocus] | |||
{ | |||
onFocus(); | |||
rowComponent.owner.keyPressed (KeyPress (KeyPress::returnKey)); | |||
}; | |||
auto onToggle = [&handler, &rowComponent, onFocus] | |||
{ | |||
if (handler.getCurrentState().isSelected()) | |||
rowComponent.owner.deselectRow (rowComponent.row); | |||
else | |||
onFocus(); | |||
}; | |||
return AccessibilityActions().addAction (AccessibilityActionType::focus, std::move (onFocus)) | |||
.addAction (AccessibilityActionType::press, std::move (onPress)) | |||
.addAction (AccessibilityActionType::toggle, std::move (onToggle)); | |||
} | |||
class ListBox::RowComponent : public Component, | |||
public TooltipClient | |||
{ | |||
@@ -35,16 +63,28 @@ public: | |||
void paint (Graphics& g) override | |||
{ | |||
if (auto* m = owner.getModel()) | |||
m->paintListBoxItem (row, g, getWidth(), getHeight(), selected); | |||
m->paintListBoxItem (row, g, getWidth(), getHeight(), isSelected); | |||
} | |||
void update (const int newRow, const bool nowSelected) | |||
{ | |||
if (row != newRow || selected != nowSelected) | |||
const auto rowHasChanged = (row != newRow); | |||
const auto selectionHasChanged = (isSelected != nowSelected); | |||
if (rowHasChanged || selectionHasChanged) | |||
{ | |||
repaint(); | |||
row = newRow; | |||
selected = nowSelected; | |||
if (rowHasChanged) | |||
row = newRow; | |||
if (selectionHasChanged) | |||
{ | |||
isSelected = nowSelected; | |||
if (auto* handler = getAccessibilityHandler()) | |||
isSelected ? handler->grabFocus() : handler->giveAwayFocus(); | |||
} | |||
} | |||
if (auto* m = owner.getModel()) | |||
@@ -57,6 +97,9 @@ public: | |||
{ | |||
addAndMakeVisible (customComponent.get()); | |||
customComponent->setBounds (getLocalBounds()); | |||
if (customComponent->getAccessibilityHandler() != nullptr) | |||
invalidateAccessibilityHandler(); | |||
} | |||
} | |||
} | |||
@@ -85,7 +128,7 @@ public: | |||
if (isEnabled()) | |||
{ | |||
if (owner.selectOnMouseDown && ! (selected || isInDragToScrollViewport())) | |||
if (owner.selectOnMouseDown && ! (isSelected || isInDragToScrollViewport())) | |||
performSelection (e, false); | |||
else | |||
selectRowOnMouseUp = true; | |||
@@ -150,10 +193,62 @@ public: | |||
return {}; | |||
} | |||
//============================================================================== | |||
class RowAccessibilityHandler : public AccessibilityHandler | |||
{ | |||
public: | |||
explicit RowAccessibilityHandler (RowComponent& rowComponentToWrap) | |||
: AccessibilityHandler (rowComponentToWrap, | |||
AccessibilityRole::listItem, | |||
getListRowAccessibilityActions (*this, rowComponentToWrap)), | |||
rowComponent (rowComponentToWrap) | |||
{ | |||
} | |||
String getTitle() const override | |||
{ | |||
if (auto* m = rowComponent.owner.getModel()) | |||
return m->getNameForRow (rowComponent.row); | |||
return {}; | |||
} | |||
AccessibleState getCurrentState() const override | |||
{ | |||
if (auto* m = rowComponent.owner.getModel()) | |||
if (rowComponent.row >= m->getNumRows()) | |||
return AccessibleState().withIgnored(); | |||
auto state = AccessibilityHandler::getCurrentState().withAccessibleOffscreen(); | |||
if (rowComponent.owner.multipleSelection) | |||
state = state.withMultiSelectable(); | |||
else | |||
state = state.withSelectable(); | |||
if (rowComponent.isSelected) | |||
state = state.withSelected(); | |||
return state; | |||
} | |||
private: | |||
RowComponent& rowComponent; | |||
}; | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override | |||
{ | |||
if (customComponent != nullptr && customComponent->getAccessibilityHandler() != nullptr) | |||
return nullptr; | |||
return std::make_unique<RowAccessibilityHandler> (*this); | |||
} | |||
//============================================================================== | |||
ListBox& owner; | |||
std::unique_ptr<Component> customComponent; | |||
int row = -1; | |||
bool selected = false, isDragging = false, isDraggingToScroll = false, selectRowOnMouseUp = false; | |||
bool isSelected = false, isDragging = false, isDraggingToScroll = false, selectRowOnMouseUp = false; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RowComponent) | |||
}; | |||
@@ -166,10 +261,13 @@ public: | |||
ListViewport (ListBox& lb) : owner (lb) | |||
{ | |||
setWantsKeyboardFocus (false); | |||
setAccessible (false); | |||
auto content = new Component(); | |||
setViewedComponent (content); | |||
auto content = std::make_unique<Component>(); | |||
content->setWantsKeyboardFocus (false); | |||
content->setAccessible (false); | |||
setViewedComponent (content.release()); | |||
} | |||
RowComponent* getComponentForRow (const int row) const noexcept | |||
@@ -233,13 +331,12 @@ public: | |||
auto y = getViewPositionY(); | |||
auto w = content.getWidth(); | |||
const int numNeeded = 2 + getMaximumVisibleHeight() / rowH; | |||
const int numNeeded = 4 + getMaximumVisibleHeight() / rowH; | |||
rows.removeRange (numNeeded, rows.size()); | |||
while (numNeeded > rows.size()) | |||
{ | |||
auto newRow = new RowComponent (owner); | |||
rows.add (newRow); | |||
auto* newRow = rows.add (new RowComponent (owner)); | |||
content.addAndMakeVisible (newRow); | |||
} | |||
@@ -247,9 +344,11 @@ public: | |||
firstWholeIndex = (y + rowH - 1) / rowH; | |||
lastWholeIndex = (y + getMaximumVisibleHeight() - 1) / rowH; | |||
auto startIndex = jmax (0, firstIndex - 1); | |||
for (int i = 0; i < numNeeded; ++i) | |||
{ | |||
const int row = i + firstIndex; | |||
const int row = i + startIndex; | |||
if (auto* rowComp = getComponentForRow (row)) | |||
{ | |||
@@ -379,8 +478,9 @@ ListBox::ListBox (const String& name, ListBoxModel* const m) | |||
viewport.reset (new ListViewport (*this)); | |||
addAndMakeVisible (viewport.get()); | |||
ListBox::setWantsKeyboardFocus (true); | |||
ListBox::colourChanged(); | |||
setWantsKeyboardFocus (true); | |||
setFocusContainerType (FocusContainerType::focusContainer); | |||
colourChanged(); | |||
} | |||
ListBox::~ListBox() | |||
@@ -938,6 +1038,11 @@ void ListBox::startDragAndDrop (const MouseEvent& e, const SparseSet<int>& rowsT | |||
} | |||
} | |||
std::unique_ptr<AccessibilityHandler> ListBox::createAccessibilityHandler() | |||
{ | |||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::list); | |||
} | |||
//============================================================================== | |||
Component* ListBoxModel::refreshComponentForRow (int, bool, Component* existingComponentToUpdate) | |||
{ | |||
@@ -946,6 +1051,7 @@ Component* ListBoxModel::refreshComponentForRow (int, bool, Component* existingC | |||
return nullptr; | |||
} | |||
String ListBoxModel::getNameForRow (int rowNumber) { return "Row " + String (rowNumber + 1); } | |||
void ListBoxModel::listBoxItemClicked (int, const MouseEvent&) {} | |||
void ListBoxModel::listBoxItemDoubleClicked (int, const MouseEvent&) {} | |||
void ListBoxModel::backgroundClicked (const MouseEvent&) {} | |||
@@ -86,6 +86,12 @@ public: | |||
virtual Component* refreshComponentForRow (int rowNumber, bool isRowSelected, | |||
Component* existingComponentToUpdate); | |||
/** This can be overridden to return a name for the specified row. | |||
By default this will just return a string containing the row number. | |||
*/ | |||
virtual String getNameForRow (int rowNumber); | |||
/** This can be overridden to react to the user clicking on a row. | |||
@see listBoxItemDoubleClicked | |||
*/ | |||
@@ -565,6 +571,8 @@ public: | |||
/** @internal */ | |||
void startDragAndDrop (const MouseEvent&, const SparseSet<int>& rowsToDrag, | |||
const var& dragDescription, bool allowDraggingToOtherWindows); | |||
/** @internal */ | |||
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override; | |||
private: | |||
//============================================================================== | |||