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.
+ if (m_selectionMode) {
+ return i18nc("@info accessibility, 1 is path", "in a grid layout in selection mode in location %1", modelRootUrl.toDisplayString());
+ }
+ return i18nc("@info accessibility, 1 is path", "in a grid layout in location %1", modelRootUrl.toDisplayString());
+ }
+ if (m_selectionMode) {
+ return i18ncp("@info accessibility, 2 is path",
+ "%1 selected item in a grid layout in selection mode in location %2",
+ "%1 selected items in a grid layout in selection mode in location %2",
+ numberOfSelectedItems,
+ 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.
+ if (m_selectionMode) {
+ return i18nc("@info accessibility, 1 is path", "in selection mode in location %1", modelRootUrl.toDisplayString());
+ }
+ return i18nc("@info accessibility, 1 is path", "in location %1", modelRootUrl.toDisplayString());
+ }
+ if (m_selectionMode) {
+ return i18ncp("@info accessibility, 2 is path",
+ "%1 selected item in selection mode in location %2",
+ "%1 selected items in selection mode in location %2",
+ numberOfSelectedItems,
+ 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::slotAnnounceCurrentItemTimerTimeout()
+{
+ 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);
}
}
-void KItemListViewAccessible::announceDescriptionChange()
+void KItemListViewAccessible::announceSelectionModeEnabled(const bool enabled)
{
- m_announceDescriptionChangeTimer->start();
+ m_selectionMode = enabled;
+#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) // QAccessibleAnnouncementEvent is only available since 6.8
+ QAccessibleAnnouncementEvent announceChangedControlsEvent(view(),
+ enabled ? i18nc("accessibility announcement", "Selection mode enabled")
+ : i18nc("accessibility announcement", "Selection mode disabled"));
+ QAccessible::updateAccessibility(&announceChangedControlsEvent);
+#endif
}