1
0
Fork 0
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:
ed 2021-05-20 18:02:33 +01:00
parent 5080b29626
commit b34e798f39
6 changed files with 134 additions and 124 deletions

View file

@ -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 {};

View file

@ -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();

View file

@ -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

View file

@ -112,7 +112,7 @@ public:
AddToSelection();
if (! isRadioButton)
if (isElementValid() && ! isRadioButton)
{
const auto& handler = getHandler();

View file

@ -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)

View file

@ -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)
};