diff options
| author | Felix Ernst <[email protected]> | 2024-11-02 00:39:19 +0100 |
|---|---|---|
| committer | Felix Ernst <[email protected]> | 2024-12-05 15:42:53 +0000 |
| commit | 179e53591b726acc0c5272e728e398de41fb51c9 (patch) | |
| tree | 3fa159c1f044a3d2ad144332ab64f6a495119b14 /src/kitemviews/accessibility/kitemlistviewaccessible.cpp | |
| parent | daf5a04d6d34e40ba0ad03d778566be0c3ebea95 (diff) | |
Adapt to Orca 47
The screen reader Orca has seen some fundamental changes between
Orca 46 and Orca 47. While they are improvements overall, they do
require changes to Dolphin to preserve the intended user
experience for Orca users.
The biggest change is perhaps that Orca will now not only announce
changes to the currently focused item, but also of its parent,
which means we do not need to pass focus around between file items
and the main view within Dolphin, but can keep focus on the file
items most of the time. This commit implements this.
The only exception of when we cannot have focus on the items within
the main view is when the current location is empty or not loaded
yet. Only then is the focus moved to the view itself and the
placeholderMessage is announced.
This commit worsens the UX for users of Orca 46 or older, so this
should only be merged once most users are on Orca 47 or later.
Diffstat (limited to 'src/kitemviews/accessibility/kitemlistviewaccessible.cpp')
| -rw-r--r-- | src/kitemviews/accessibility/kitemlistviewaccessible.cpp | 181 |
1 files changed, 97 insertions, 84 deletions
diff --git a/src/kitemviews/accessibility/kitemlistviewaccessible.cpp b/src/kitemviews/accessibility/kitemlistviewaccessible.cpp index 2643eb302..f8c14bf4a 100644 --- a/src/kitemviews/accessibility/kitemlistviewaccessible.cpp +++ b/src/kitemviews/accessibility/kitemlistviewaccessible.cpp @@ -36,14 +36,11 @@ KItemListViewAccessible::KItemListViewAccessible(KItemListView *view_, KItemList Q_CHECK_PTR(parent); m_accessibleDelegates.resize(childCount()); - m_announceDescriptionChangeTimer = new QTimer{view_}; - m_announceDescriptionChangeTimer->setSingleShot(true); - m_announceDescriptionChangeTimer->setInterval(100); - KItemListGroupHeader::connect(m_announceDescriptionChangeTimer, &QTimer::timeout, view_, [this]() { - // The below will have no effect if one of the list items has focus and not the view itself. Still we announce the accessibility description change - // here in case the view itself has focus e.g. after tabbing there or after opening a new location. - QAccessibleEvent announceAccessibleDescriptionEvent(this, QAccessible::DescriptionChanged); - QAccessible::updateAccessibility(&announceAccessibleDescriptionEvent); + m_announceCurrentItemTimer = new QTimer{view_}; + m_announceCurrentItemTimer->setSingleShot(true); + m_announceCurrentItemTimer->setInterval(100); + KItemListGroupHeader::connect(m_announceCurrentItemTimer, &QTimer::timeout, view_, [this]() { + slotAnnounceCurrentItemTimerTimeout(); }); } @@ -70,10 +67,6 @@ void *KItemListViewAccessible::interface_cast(QAccessible::InterfaceType type) } } -void KItemListViewAccessible::modelReset() -{ -} - QAccessibleInterface *KItemListViewAccessible::accessibleDelegate(int index) const { if (index < 0 || index >= view()->model()->count()) { @@ -263,7 +256,13 @@ QString KItemListViewAccessible::text(QAccessible::Text t) const if (t != QAccessible::Description) { return QString(); } - const auto currentItem = child(controller->selectionManager()->currentItem()); + + QAccessibleInterface *currentItem = child(controller->selectionManager()->currentItem()); + + /** + * Always announce the path last because it might be very long. + * We do not need to announce the total count of items here because accessibility software like Orca alrady announces this automatically for lists. + */ if (!currentItem) { return i18nc("@info 1 states that the folder is empty and sometimes why, 2 is the full filesystem path", "%1 at location %2", @@ -271,59 +270,36 @@ QString KItemListViewAccessible::text(QAccessible::Text t) const modelRootUrl.toDisplayString()); } - const QString selectionStateString{isSelected(currentItem) ? QString() - // i18n: There is a comma at the end because this is one property in an enumeration of - // properties that a file or folder has. Accessible text for accessibility software like screen - // readers. - : i18n("not selected,")}; - - QString expandableStateString; - if (currentItem->state().expandable) { - if (currentItem->state().collapsed) { - // i18n: There is a comma at the end because this is one property in an enumeration of properties that a folder in a tree view has. - // Accessible text for accessibility software like screen readers. - expandableStateString = i18n("collapsed,"); - } else { - // i18n: There is a comma at the end because this is one property in an enumeration of properties that a folder in a tree view has. - // Accessible text for accessibility software like screen readers. - expandableStateString = i18n("expanded,"); - } - } - - const QString selectedItemCountString{selectedItemCount() > 1 - // i18n: There is a "—" at the beginning because this is a followup sentence to a text that did not properly end - // with a period. Accessible text for accessibility software like screen readers. - ? i18np("— %1 selected item", "— %1 selected items", selectedItemCount()) - : QString()}; + const int numberOfSelectedItems = selectedItemCount(); // Determine if we should announce the item layout. For end users of the accessibility tree there is an expectation that a list can be scrolled through by // pressing the "Down" key repeatedly. This is not the case in the icon view mode, where pressing "Right" or "Left" moves through the whole list of items. // Therefore we need to announce this layout when in icon view mode. - QString layoutAnnouncementString; if (auto standardView = qobject_cast<const KStandardItemListView *>(view())) { if (standardView->itemLayout() == KStandardItemListView::ItemLayout::IconsLayout) { - layoutAnnouncementString = i18nc("@info refering to a file or folder", "in a grid layout"); + if (numberOfSelectedItems < 1 || (numberOfSelectedItems == 1 && isSelected(currentItem))) { + // We do not announce the number of selected items if the only selected item is the current item + // because the selection state of the current item is already announced elsewhere. + return i18nc("@info accessibility, 1 is path", "in a grid layout in location %1", modelRootUrl.toDisplayString()); + } + return i18ncp("@info accessibility, 2 is path", + "%1 selected item in a grid layout in location %2", + "%1 selected items in a grid layout in location %2", + numberOfSelectedItems, + modelRootUrl.toDisplayString()); } } - /** - * Announce it in this order so the most important information is at the beginning and the potentially very long path at the end: - * "$currentlyFocussedItemName, $currentlyFocussedItemDescription, $currentFolderPath". - * We do not need to announce the total count of items here because accessibility software like Orca alrady announces this automatically for lists. - * Normally for list items the selection and expandadable state are also automatically announced by Orca, however we are building the accessible - * description of the view here, so we need to manually add all infomation about the current item we also want to announce. - */ - return i18nc( - "@info 1 is currentlyFocussedItemName, 2 is empty or \"not selected, \", 3 is currentlyFocussedItemDescription, 3 is currentFolderName, 4 is " - "currentFolderPath", - "%1, %2 %3 %4 %5 %6 in location %7", - currentItem->text(QAccessible::Name), - selectionStateString, - expandableStateString, - currentItem->text(QAccessible::Description), - selectedItemCountString, - layoutAnnouncementString, - modelRootUrl.toDisplayString()); + if (numberOfSelectedItems < 1 || (numberOfSelectedItems == 1 && isSelected(currentItem))) { + // We do not announce the number of selected items if the only selected item is the current item + // because the selection state of the current item is already announced elsewhere. + return i18nc("@info accessibility, 1 is path", "in location %1", modelRootUrl.toDisplayString()); + } + return i18ncp("@info accessibility, 2 is path", + "%1 selected item in location %2", + "%1 selected items in location %2", + numberOfSelectedItems, + modelRootUrl.toDisplayString()); } QRect KItemListViewAccessible::rect() const @@ -443,37 +419,74 @@ KItemListView *KItemListViewAccessible::view() const return static_cast<KItemListView *>(object()); } -void KItemListViewAccessible::announceOverallViewState(const QString &placeholderMessage) +void KItemListViewAccessible::setAccessibleFocusAndAnnounceAll() +{ + const int currentItemIndex = view()->m_controller->selectionManager()->currentItem(); + if (currentItemIndex < 0) { + // The current item is invalid (perhaps because the folder is empty), so we set the focus to the view itself instead. + QAccessibleEvent accessibleFocusInEvent(this, QAccessible::Focus); + QAccessible::updateAccessibility(&accessibleFocusInEvent); + return; + } + + QAccessibleEvent accessibleFocusInEvent(this, QAccessible::Focus); + accessibleFocusInEvent.setChild(currentItemIndex); + QAccessible::updateAccessibility(&accessibleFocusInEvent); + m_shouldAnnounceLocation = true; + announceCurrentItem(); +} + +void KItemListViewAccessible::announceNewlyLoadedLocation(const QString &placeholderMessage) { m_placeholderMessage = placeholderMessage; + m_shouldAnnounceLocation = true; - // Make sure we announce this placeholderMessage. However, do not announce it when the focus is on an unrelated object currently. - // We for example do not want to announce "Loading cancelled" when the focus is currently on an error message explaining why the loading was cancelled. - if (view()->hasFocus() || !QApplication::focusWidget() || static_cast<QWidget *>(m_parent->object())->isAncestorOf(QApplication::focusWidget())) { - view()->setFocus(); - // If we move focus to an item and right after that the description of the item is changed, the item will be announced twice. - // We want to avoid that so we wait until after the description change was announced to move focus. - KItemListGroupHeader::connect( - m_announceDescriptionChangeTimer, - &QTimer::timeout, - view(), - [this]() { - if (view()->hasFocus() || !QApplication::focusWidget() - || static_cast<QWidget *>(m_parent->object())->isAncestorOf(QApplication::focusWidget())) { - QAccessibleEvent accessibleFocusEvent(this, QAccessible::Focus); - QAccessible::updateAccessibility(&accessibleFocusEvent); // This accessibility update is perhaps even too important: It is generally - // the last triggered update after changing the currently viewed folder. This call makes sure that we announce the new directory in - // full. Furthermore it also serves its original purpose of making sure we announce the placeholderMessage in empty folders. - } - }, - Qt::SingleShotConnection); - if (!m_announceDescriptionChangeTimer->isActive()) { - m_announceDescriptionChangeTimer->start(); - } - } + // Changes might still be happening in the view. We (re)start the timer to make it less likely that it announces a state that is still in flux. + m_announceCurrentItemTimer->start(); +} + +void KItemListViewAccessible::announceCurrentItem() +{ + m_announceCurrentItemTimer->start(); } -void KItemListViewAccessible::announceDescriptionChange() +void KItemListViewAccessible::slotAnnounceCurrentItemTimerTimeout() { - m_announceDescriptionChangeTimer->start(); + if (!view()->hasFocus() && QApplication::focusWidget() && QApplication::focusWidget()->isVisible() + && !static_cast<QWidget *>(m_parent->object())->isAncestorOf(QApplication::focusWidget())) { + // Something else than this view has focus, so we do not announce anything. + m_lastAnnouncedIndex = -1; // Reset this to -1 so we properly move focus to the current item the next time this method is called. + return; + } + + /// Announce the current item (or the view if there is no current item). + const int currentIndex = view()->m_controller->selectionManager()->currentItem(); + if (currentIndex < 0) { + // The current index is invalid! There might be no items in the list. Instead the list itself is announced. + m_shouldAnnounceLocation = true; + QAccessibleEvent announceEmptyViewPlaceholderMessageEvent(this, QAccessible::Focus); + QAccessible::updateAccessibility(&announceEmptyViewPlaceholderMessageEvent); + } else if (currentIndex != m_lastAnnouncedIndex) { + QAccessibleEvent announceNewlyFocusedItemEvent(this, QAccessible::Focus); + announceNewlyFocusedItemEvent.setChild(currentIndex); + QAccessible::updateAccessibility(&announceNewlyFocusedItemEvent); + } else { + QAccessibleEvent announceCurrentItemNameChangeEvent(this, QAccessible::NameChanged); + announceCurrentItemNameChangeEvent.setChild(currentIndex); + QAccessible::updateAccessibility(&announceCurrentItemNameChangeEvent); + QAccessibleEvent announceCurrentItemDescriptionChangeEvent(this, QAccessible::DescriptionChanged); + announceCurrentItemDescriptionChangeEvent.setChild(currentIndex); + QAccessible::updateAccessibility(&announceCurrentItemDescriptionChangeEvent); + } + m_lastAnnouncedIndex = currentIndex; + + /// Announce the location if we are not just moving within the same location. + if (m_shouldAnnounceLocation) { + m_shouldAnnounceLocation = false; + + QAccessibleEvent announceAccessibleDescriptionEvent1(this, QAccessible::NameChanged); + QAccessible::updateAccessibility(&announceAccessibleDescriptionEvent1); + QAccessibleEvent announceAccessibleDescriptionEvent(this, QAccessible::DescriptionChanged); + QAccessible::updateAccessibility(&announceAccessibleDescriptionEvent); + } } |
