mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
Accessibility: Fixed some bugs in Windows text navigation and readouts, improved selection and focus navigation
This commit is contained in:
parent
5080b29626
commit
b34e798f39
6 changed files with 134 additions and 124 deletions
|
|
@ -133,15 +133,24 @@ void sendAccessibilityPropertyChangedEvent (const AccessibilityHandler& handler,
|
|||
|
||||
void notifyAccessibilityEventInternal (const AccessibilityHandler& handler, InternalAccessibilityEvent eventType)
|
||||
{
|
||||
if (eventType == InternalAccessibilityEvent::elementCreated
|
||||
|| eventType == InternalAccessibilityEvent::elementDestroyed)
|
||||
{
|
||||
if (auto* parent = handler.getParent())
|
||||
sendAccessibilityAutomationEvent (*parent, UIA_LayoutInvalidatedEventId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
case InternalAccessibilityEvent::elementCreated:
|
||||
case InternalAccessibilityEvent::elementDestroyed: break;
|
||||
}
|
||||
|
||||
return {};
|
||||
|
|
|
|||
|
|
@ -87,12 +87,6 @@ static long roleToControlTypeId (AccessibilityRole roleType)
|
|||
return UIA_CustomControlTypeId;
|
||||
}
|
||||
|
||||
static bool isEditableText (const AccessibilityHandler& handler)
|
||||
{
|
||||
return handler.getRole() == AccessibilityRole::editableText
|
||||
&& handler.getTextInterface() != nullptr;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
AccessibilityNativeHandle::AccessibilityNativeHandle (AccessibilityHandler& handler)
|
||||
: ComBaseClassHelper (0),
|
||||
|
|
@ -171,10 +165,12 @@ JUCE_COMRESULT AccessibilityNativeHandle::GetPatternProvider (PATTERNID pId, IUn
|
|||
}
|
||||
case UIA_ValuePatternId:
|
||||
{
|
||||
auto editableText = isEditableText (accessibilityHandler);
|
||||
|
||||
if (accessibilityHandler.getValueInterface() != nullptr || editableText)
|
||||
return new UIAValueProvider (this, editableText);
|
||||
if (accessibilityHandler.getValueInterface() != nullptr
|
||||
|| isEditableText (accessibilityHandler)
|
||||
|| nameIsAccessibilityValue (role))
|
||||
{
|
||||
return new UIAValueProvider (this);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
@ -223,21 +219,15 @@ JUCE_COMRESULT AccessibilityNativeHandle::GetPatternProvider (PATTERNID pId, IUn
|
|||
}
|
||||
case UIA_GridPatternId:
|
||||
{
|
||||
if ((role == AccessibilityRole::table || role == AccessibilityRole::tree)
|
||||
&& accessibilityHandler.getTableInterface() != nullptr)
|
||||
{
|
||||
if (accessibilityHandler.getTableInterface() != nullptr)
|
||||
return new UIAGridProvider (this);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case UIA_GridItemPatternId:
|
||||
{
|
||||
if ((role == AccessibilityRole::cell || role == AccessibilityRole::treeItem)
|
||||
&& accessibilityHandler.getCellInterface() != nullptr)
|
||||
{
|
||||
if (accessibilityHandler.getCellInterface() != nullptr)
|
||||
return new UIAGridItemProvider (this);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
@ -250,11 +240,8 @@ JUCE_COMRESULT AccessibilityNativeHandle::GetPatternProvider (PATTERNID pId, IUn
|
|||
}
|
||||
case UIA_ExpandCollapsePatternId:
|
||||
{
|
||||
if (role == AccessibilityRole::menuItem
|
||||
&& accessibilityHandler.getActions().contains (AccessibilityActionType::showMenu))
|
||||
{
|
||||
if (accessibilityHandler.getActions().contains (AccessibilityActionType::showMenu))
|
||||
return new UIAExpandCollapseProvider (this);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
@ -277,14 +264,16 @@ JUCE_COMRESULT AccessibilityNativeHandle::GetPropertyValue (PROPERTYID propertyI
|
|||
|
||||
const auto fragmentRoot = isFragmentRoot();
|
||||
|
||||
const auto role = accessibilityHandler.getRole();
|
||||
const auto state = accessibilityHandler.getCurrentState();
|
||||
|
||||
switch (propertyId)
|
||||
{
|
||||
case UIA_AutomationIdPropertyId:
|
||||
VariantHelpers::setString (getAutomationId (accessibilityHandler), pRetVal);
|
||||
break;
|
||||
case UIA_ControlTypePropertyId:
|
||||
VariantHelpers::setInt (roleToControlTypeId (accessibilityHandler.getRole()),
|
||||
pRetVal);
|
||||
VariantHelpers::setInt (roleToControlTypeId (role), pRetVal);
|
||||
break;
|
||||
case UIA_FrameworkIdPropertyId:
|
||||
VariantHelpers::setString ("JUCE", pRetVal);
|
||||
|
|
@ -296,25 +285,26 @@ JUCE_COMRESULT AccessibilityNativeHandle::GetPropertyValue (PROPERTYID propertyI
|
|||
VariantHelpers::setString (accessibilityHandler.getHelp(), pRetVal);
|
||||
break;
|
||||
case UIA_IsContentElementPropertyId:
|
||||
VariantHelpers::setBool (! accessibilityHandler.isIgnored(), pRetVal);
|
||||
VariantHelpers::setBool (! accessibilityHandler.isIgnored() && accessibilityHandler.isVisibleWithinParent(),
|
||||
pRetVal);
|
||||
break;
|
||||
case UIA_IsControlElementPropertyId:
|
||||
VariantHelpers::setBool (true, pRetVal);
|
||||
break;
|
||||
case UIA_IsDialogPropertyId:
|
||||
VariantHelpers::setBool (accessibilityHandler.getRole() == AccessibilityRole::dialogWindow, pRetVal);
|
||||
VariantHelpers::setBool (role == AccessibilityRole::dialogWindow, pRetVal);
|
||||
break;
|
||||
case UIA_IsEnabledPropertyId:
|
||||
VariantHelpers::setBool (accessibilityHandler.getComponent().isEnabled(), pRetVal);
|
||||
break;
|
||||
case UIA_IsKeyboardFocusablePropertyId:
|
||||
VariantHelpers::setBool (accessibilityHandler.getCurrentState().isFocusable(), pRetVal);
|
||||
VariantHelpers::setBool (state.isFocusable(), pRetVal);
|
||||
break;
|
||||
case UIA_HasKeyboardFocusPropertyId:
|
||||
VariantHelpers::setBool (accessibilityHandler.hasFocus (true), pRetVal);
|
||||
break;
|
||||
case UIA_IsOffscreenPropertyId:
|
||||
VariantHelpers::setBool (false, pRetVal);
|
||||
VariantHelpers::setBool (! accessibilityHandler.isVisibleWithinParent(), pRetVal);
|
||||
break;
|
||||
case UIA_IsPasswordPropertyId:
|
||||
if (auto* textInterface = accessibilityHandler.getTextInterface())
|
||||
|
|
@ -322,9 +312,9 @@ JUCE_COMRESULT AccessibilityNativeHandle::GetPropertyValue (PROPERTYID propertyI
|
|||
|
||||
break;
|
||||
case UIA_IsPeripheralPropertyId:
|
||||
VariantHelpers::setBool (accessibilityHandler.getRole() == AccessibilityRole::tooltip
|
||||
|| accessibilityHandler.getRole() == AccessibilityRole::popupMenu
|
||||
|| accessibilityHandler.getRole() == AccessibilityRole::splashScreen,
|
||||
VariantHelpers::setBool (role == AccessibilityRole::tooltip
|
||||
|| role == AccessibilityRole::popupMenu
|
||||
|| role == AccessibilityRole::splashScreen,
|
||||
pRetVal);
|
||||
break;
|
||||
case UIA_NamePropertyId:
|
||||
|
|
@ -451,6 +441,11 @@ JUCE_COMRESULT AccessibilityNativeHandle::SetFocus()
|
|||
if (! isElementValid())
|
||||
return UIA_E_ELEMENTNOTAVAILABLE;
|
||||
|
||||
const WeakReference<Component> safeComponent (&accessibilityHandler.getComponent());
|
||||
|
||||
accessibilityHandler.getActions().invoke (AccessibilityActionType::focus);
|
||||
|
||||
if (safeComponent != nullptr)
|
||||
accessibilityHandler.grabFocus();
|
||||
|
||||
return S_OK;
|
||||
|
|
@ -544,7 +539,12 @@ JUCE_COMRESULT AccessibilityNativeHandle::GetFocus (IRawElementProviderFragment*
|
|||
//==============================================================================
|
||||
String AccessibilityNativeHandle::getElementName() const
|
||||
{
|
||||
if (accessibilityHandler.getRole() == AccessibilityRole::tooltip)
|
||||
const auto role = accessibilityHandler.getRole();
|
||||
|
||||
if (nameIsAccessibilityValue (role))
|
||||
return {};
|
||||
|
||||
if (role == AccessibilityRole::tooltip)
|
||||
return accessibilityHandler.getDescription();
|
||||
|
||||
auto name = accessibilityHandler.getTitle();
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ namespace VariantHelpers
|
|||
}
|
||||
}
|
||||
|
||||
JUCE_COMRESULT addHandlersToArray (const std::vector<const AccessibilityHandler*>& handlers, SAFEARRAY** pRetVal)
|
||||
inline JUCE_COMRESULT addHandlersToArray (const std::vector<const AccessibilityHandler*>& handlers, SAFEARRAY** pRetVal)
|
||||
{
|
||||
auto numHandlers = handlers.size();
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ JUCE_COMRESULT addHandlersToArray (const std::vector<const AccessibilityHandler*
|
|||
}
|
||||
|
||||
template <typename Value, typename Object, typename Callback>
|
||||
JUCE_COMRESULT withCheckedComArgs (Value* pRetVal, Object& handle, Callback&& callback)
|
||||
inline JUCE_COMRESULT withCheckedComArgs (Value* pRetVal, Object& handle, Callback&& callback)
|
||||
{
|
||||
if (pRetVal == nullptr)
|
||||
return E_INVALIDARG;
|
||||
|
|
@ -100,4 +100,15 @@ JUCE_COMRESULT withCheckedComArgs (Value* pRetVal, Object& handle, Callback&& ca
|
|||
return callback();
|
||||
}
|
||||
|
||||
inline bool isEditableText (const AccessibilityHandler& handler)
|
||||
{
|
||||
return handler.getRole() == AccessibilityRole::editableText
|
||||
&& handler.getTextInterface() != nullptr;
|
||||
}
|
||||
|
||||
inline bool nameIsAccessibilityValue (AccessibilityRole role)
|
||||
{
|
||||
return role == AccessibilityRole::staticText;
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ public:
|
|||
|
||||
AddToSelection();
|
||||
|
||||
if (! isRadioButton)
|
||||
if (isElementValid() && ! isRadioButton)
|
||||
{
|
||||
const auto& handler = getHandler();
|
||||
|
||||
|
|
|
|||
|
|
@ -257,31 +257,29 @@ private:
|
|||
{
|
||||
selectionRange.setStart (jlimit (0, numCharacters - 1, selectionRange.getStart()));
|
||||
selectionRange.setEnd (selectionRange.getStart() + 1);
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
if (unit == TextUnit_Paragraph
|
||||
else if (unit == TextUnit_Paragraph
|
||||
|| unit == TextUnit_Page
|
||||
|| unit == TextUnit_Document)
|
||||
{
|
||||
selectionRange = { 0, textInterface->getTotalNumCharacters() };
|
||||
return S_OK;
|
||||
selectionRange = { 0, numCharacters };
|
||||
}
|
||||
else if (unit == TextUnit_Word
|
||||
|| unit == TextUnit_Format
|
||||
|| unit == TextUnit_Line)
|
||||
{
|
||||
const auto boundaryType = (unit == TextUnit_Line ? BoundaryType::line : BoundaryType::word);
|
||||
|
||||
auto start = getNextEndpointPosition (*textInterface,
|
||||
auto start = findBoundary (*textInterface,
|
||||
selectionRange.getStart(),
|
||||
unit,
|
||||
boundaryType,
|
||||
NextEndpointDirection::backwards);
|
||||
|
||||
if (start >= 0)
|
||||
{
|
||||
auto end = getNextEndpointPosition (*textInterface,
|
||||
auto end = findBoundary (*textInterface,
|
||||
start,
|
||||
unit,
|
||||
boundaryType,
|
||||
NextEndpointDirection::forwards);
|
||||
|
||||
if (end >= 0)
|
||||
selectionRange = Range<int> (start, end);
|
||||
}
|
||||
|
||||
|
|
@ -495,12 +493,12 @@ private:
|
|||
if (count == 0 || numCharacters == 0)
|
||||
return S_OK;
|
||||
|
||||
auto isStart = (endpoint == TextPatternRangeEndpoint_Start);
|
||||
const auto isStart = (endpoint == TextPatternRangeEndpoint_Start);
|
||||
auto endpointToMove = (isStart ? selectionRange.getStart() : selectionRange.getEnd());
|
||||
|
||||
if (unit == TextUnit_Character)
|
||||
{
|
||||
auto targetPoint = jlimit (0, numCharacters, endpointToMove + count);
|
||||
const auto targetPoint = jlimit (0, numCharacters, endpointToMove + count);
|
||||
|
||||
*pRetVal = targetPoint - endpointToMove;
|
||||
setEndpointChecked (endpoint, targetPoint);
|
||||
|
|
@ -520,25 +518,30 @@ private:
|
|||
return S_OK;
|
||||
}
|
||||
|
||||
for (int i = 0; i < std::abs (count); ++i)
|
||||
if (unit == TextUnit_Word
|
||||
|| unit == TextUnit_Format
|
||||
|| unit == TextUnit_Line)
|
||||
{
|
||||
auto nextEndpoint = getNextEndpointPosition (textInterface,
|
||||
endpointToMove,
|
||||
unit,
|
||||
direction);
|
||||
const auto boundaryType = unit == TextUnit_Line ? BoundaryType::line : BoundaryType::word;
|
||||
|
||||
if (nextEndpoint < 0)
|
||||
// handle case where endpoint is on a boundary
|
||||
if (findBoundary (textInterface, endpointToMove, boundaryType, direction) == endpointToMove)
|
||||
endpointToMove += (direction == NextEndpointDirection::forwards ? 1 : -1);
|
||||
|
||||
int numMoved;
|
||||
for (numMoved = 0; numMoved < std::abs (count); ++numMoved)
|
||||
{
|
||||
*pRetVal = (direction == NextEndpointDirection::forwards ? i : -i);
|
||||
setEndpointChecked (endpoint, endpointToMove);
|
||||
return S_OK;
|
||||
}
|
||||
auto nextEndpoint = findBoundary (textInterface, endpointToMove, boundaryType, direction);
|
||||
|
||||
if (nextEndpoint == endpointToMove)
|
||||
break;
|
||||
|
||||
endpointToMove = nextEndpoint;
|
||||
}
|
||||
|
||||
*pRetVal = count;
|
||||
*pRetVal = numMoved;
|
||||
setEndpointChecked (endpoint, endpointToMove);
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
});
|
||||
|
|
@ -584,52 +587,27 @@ private:
|
|||
|
||||
private:
|
||||
enum class NextEndpointDirection { forwards, backwards };
|
||||
enum class BoundaryType { word, line };
|
||||
|
||||
static int getNextEndpointPosition (const AccessibilityTextInterface& textInterface,
|
||||
static int findBoundary (const AccessibilityTextInterface& textInterface,
|
||||
int currentPosition,
|
||||
TextUnit unit,
|
||||
BoundaryType boundary,
|
||||
NextEndpointDirection direction)
|
||||
{
|
||||
auto isTextUnitSeparator = [unit] (const juce_wchar c)
|
||||
const auto text = [&]() -> String
|
||||
{
|
||||
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) });
|
||||
return textInterface.getText ({ currentPosition, textInterface.getTotalNumCharacters() });
|
||||
|
||||
for (auto charPtr = textBuffer.getCharPointer(); ! charPtr.isEmpty();)
|
||||
{
|
||||
auto character = charPtr.getAndAdvance();
|
||||
++numChars;
|
||||
auto stdString = textInterface.getText ({ 0, currentPosition }).toStdString();
|
||||
std::reverse (stdString.begin(), stdString.end());
|
||||
return stdString;
|
||||
}();
|
||||
|
||||
if (isTextUnitSeparator (character))
|
||||
return currentPosition + numChars;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto textBuffer = textInterface.getText ({ jmax (0, currentPosition - textBufferSize),
|
||||
currentPosition });
|
||||
auto tokens = (boundary == BoundaryType::line ? StringArray::fromLines (text)
|
||||
: StringArray::fromTokens (text, false));
|
||||
|
||||
for (auto charPtr = textBuffer.end() - 1; charPtr != textBuffer.begin(); --charPtr)
|
||||
{
|
||||
auto character = *charPtr;
|
||||
|
||||
if (isTextUnitSeparator (character))
|
||||
return currentPosition - numChars;
|
||||
|
||||
++numChars;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
return currentPosition + (direction == NextEndpointDirection::forwards ? tokens[0].length() : -(tokens[0].length()));
|
||||
}
|
||||
|
||||
void setEndpointChecked (TextPatternRangeEndpoint endpoint, int newEndpoint)
|
||||
|
|
|
|||
|
|
@ -31,9 +31,8 @@ class UIAValueProvider : public UIAProviderBase,
|
|||
public ComBaseClassHelper<IValueProvider>
|
||||
{
|
||||
public:
|
||||
UIAValueProvider (AccessibilityNativeHandle* nativeHandle, bool editableText)
|
||||
: UIAProviderBase (nativeHandle),
|
||||
isEditableText (editableText)
|
||||
UIAValueProvider (AccessibilityNativeHandle* nativeHandle)
|
||||
: UIAProviderBase (nativeHandle)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +44,9 @@ public:
|
|||
|
||||
const auto& handler = getHandler();
|
||||
|
||||
if (nameIsAccessibilityValue (handler.getRole()))
|
||||
return UIA_E_NOTSUPPORTED;
|
||||
|
||||
const auto sendValuePropertyChangeMessage = [&]()
|
||||
{
|
||||
VARIANT newValue;
|
||||
|
|
@ -53,7 +55,7 @@ public:
|
|||
sendAccessibilityPropertyChangedEvent (handler, UIA_ValueValuePropertyId, newValue);
|
||||
};
|
||||
|
||||
if (isEditableText)
|
||||
if (isEditableText (handler))
|
||||
{
|
||||
handler.getTextInterface()->setText (String (val));
|
||||
sendValuePropertyChangeMessage();
|
||||
|
|
@ -90,9 +92,16 @@ public:
|
|||
{
|
||||
return withCheckedComArgs (pRetVal, *this, [&]
|
||||
{
|
||||
if (! isEditableText)
|
||||
if (auto* valueInterface = getHandler().getValueInterface())
|
||||
*pRetVal = valueInterface->isReadOnly();
|
||||
*pRetVal = true;
|
||||
|
||||
const auto& handler = getHandler();
|
||||
|
||||
if (isEditableText (handler)
|
||||
|| (handler.getValueInterface() != nullptr
|
||||
&& ! handler.getValueInterface()->isReadOnly()))
|
||||
{
|
||||
*pRetVal = false;
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
});
|
||||
|
|
@ -101,7 +110,12 @@ public:
|
|||
private:
|
||||
String getCurrentValueString() const
|
||||
{
|
||||
if (isEditableText)
|
||||
const auto& handler = getHandler();
|
||||
|
||||
if (nameIsAccessibilityValue (handler.getRole()))
|
||||
return handler.getTitle();
|
||||
|
||||
if (isEditableText (handler))
|
||||
if (auto* textInterface = getHandler().getTextInterface())
|
||||
return textInterface->getText ({ 0, textInterface->getTotalNumCharacters() });
|
||||
|
||||
|
|
@ -112,8 +126,6 @@ private:
|
|||
return {};
|
||||
}
|
||||
|
||||
const bool isEditableText;
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAValueProvider)
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue