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();
});
}
}
}
-void KItemListViewAccessible::modelReset()
-{
-}
-
QAccessibleInterface *KItemListViewAccessible::accessibleDelegate(int index) const
{
if (index < 0 || index >= view()->model()->count()) {
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",
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
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);
+ }
}
KItemListView *view() const;
/**
- * Moves the focus to the list view itself so an overview over the state can be given.
- * @param placeholderMessage the message that should be announced when no items are visible (yet). This message is mostly identical to
+ * Called by KItemListContainer when it passes on focus to the view. Accessible focus is then meant to go towards this accessible interface and a detailed
+ * announcement of the current view state (current item and overall location state) should be triggered.
+ */
+ void setAccessibleFocusAndAnnounceAll();
+
+ /**
+ * Called multiple times while a new location is loading. A timer is restarted, and if this method has not been called for a split second, the newly loaded
+ * location is finally announced.
+ * Either the @p placeholderMessage is announced when there are no items in the view (yet), or the current item is announced together with the view state.
+ *
+ * @param placeholderMessage The message that should be announced when no items are visible (yet). This message is mostly identical to
* DolphinView::m_placeholderLabel in both content and purpose. @see DolphinView::updatePlaceHolderLabel().
+ *
+ * If there are items in the view and the placeholderMessage is therefore not visible, the current item and location is announced instead.
*/
- void announceOverallViewState(const QString &placeholderMessage);
+ void announceNewlyLoadedLocation(const QString &placeholderMessage);
/**
- * Announces that the description of the view has changed. The changed description will only be announced if the view has focus (from an accessibility
- * point of view). This method ensures that multiple calls to this method within a small time frame will only lead to a singular announcement instead of
- * multiple or already outdated ones, so calling this method instead of manually sending accessibility events for this view is preferred.
+ * Starts a timer that will trigger an announcement of the current item. The timer makes sure that quick changes to the current item will only lead to a
+ * singular announcement. This way when a new folder is loaded we only trigger a single announcement even if the items quickly change.
+ *
+ * When m_shouldAnnounceLocation is true, the current location will be announced following the announcement of the current item.
+ *
+ * If the current item is invalid, only the current location is announced, which has the responsibility of then telling why there is no valid item in the
+ * view.
*/
- void announceDescriptionChange();
+ void announceCurrentItem();
-protected:
- virtual void modelReset();
+private:
/**
* @returns a KItemListDelegateAccessible representing the file or folder at the @index. Returns nullptr for invalid indices.
* If a KItemListDelegateAccessible for an index does not yet exist, it will be created.
KItemListSelectionManager *selectionManager() const;
+private Q_SLOTS:
+ /**
+ * Is run in response to announceCurrentItem(). If the current item exists, it is announced. Otherwise the view is announced.
+ * Also announces some general information about the current location if it has changed recently.
+ */
+ void slotAnnounceCurrentItemTimerTimeout();
+
private:
/** @see setPlaceholderMessage(). */
QString m_placeholderMessage;
- QTimer *m_announceDescriptionChangeTimer;
+ /**
+ * Is started by announceCurrentItem().
+ * If we announce the current item as soon as it changes, we would announce multiple items while loading a folder.
+ * This timer makes sure we only announce the singular currently focused item when things have settled down.
+ */
+ QTimer *m_announceCurrentItemTimer;
+
+ /**
+ * If we want announceCurrentItem() to always announce the current item, we must be aware if this is equal to the previous current item, because
+ * - if the accessibility focus moves to a new item, it is automatically announced, but
+ * - if the focus is still on the item at the same index, the focus does not technically move to a new item even if the file at that index changed, so we
+ * need to instead send change events for the accessible name and accessible description.
+ */
+ int m_lastAnnouncedIndex = -1;
+
+ /**
+ * Is set to true in response to announceDescriptionChange(). When true, the next time slotAnnounceCurrentItemTimerTimeout() is called the description is
+ * also announced. Then this bool is set to false.
+ */
+ bool m_shouldAnnounceLocation = true;
class AccessibleIdWrapper
{
if (useAlternateBackgrounds()) {
updateAlternateBackgrounds();
}
-#ifndef QT_NO_ACCESSIBILITY
- if (QAccessible::isActive()) { // Announce that the count of items has changed.
- static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(this))->announceDescriptionChange();
- }
-#endif
}
void KItemListView::slotItemsRemoved(const KItemRangeList &itemRanges)
if (useAlternateBackgrounds()) {
updateAlternateBackgrounds();
}
-#ifndef QT_NO_ACCESSIBILITY
- if (QAccessible::isActive()) { // Announce that the count of items has changed.
- static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(this))->announceDescriptionChange();
- }
-#endif
}
void KItemListView::slotItemsMoved(const KItemRange &itemRange, const QList<int> &movedToIndexes)
updateVisibleGroupHeaders();
doLayout(NoAnimation);
}
-
-#ifndef QT_NO_ACCESSIBILITY
- QAccessibleTableModelChangeEvent ev(this, QAccessibleTableModelChangeEvent::DataChanged);
- ev.setFirstRow(itemRange.index);
- ev.setLastRow(itemRange.index + itemRange.count);
- QAccessible::updateAccessibility(&ev);
-#endif
}
-
doLayout(NoAnimation);
}
KItemListWidget *currentWidget = m_visibleItems.value(current, nullptr);
if (currentWidget) {
currentWidget->setCurrent(true);
- if (hasFocus() || (previousWidget && previousWidget->hasFocus())) {
- currentWidget->setFocus(); // Mostly for accessibility, because keyboard events are handled correctly either way.
- }
}
}
#ifndef QT_NO_ACCESSIBILITY
- if (QAccessible::isActive()) {
- if (current >= 0) {
- QAccessibleEvent accessibleFocusCurrentItemEvent(this, QAccessible::Focus);
- accessibleFocusCurrentItemEvent.setChild(current);
- QAccessible::updateAccessibility(&accessibleFocusCurrentItemEvent);
- }
- static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(this))->announceDescriptionChange();
+ if (current != previous && QAccessible::isActive()) {
+ static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(this))->announceCurrentItem();
}
#endif
}
// Let the screen reader announce "selected" or "not selected" for the active item.
const bool wasSelected(previous.contains(index));
if (isSelected != wasSelected) {
- QAccessibleEvent accessibleSelectionChangedEvent(this, QAccessible::Selection);
+ QAccessibleEvent accessibleSelectionChangedEvent(this, QAccessible::SelectionAdd);
accessibleSelectionChangedEvent.setChild(index);
QAccessible::updateAccessibility(&accessibleSelectionChangedEvent);
}
}
- // Usually the below does not have an effect because the view will not have focus at this moment but one of its list items. Still we announce the
- // change of the accessibility description just in case the user manually moved focus up by one.
- static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(this))->announceDescriptionChange();
#else
}
Q_UNUSED(previous)