]> cloud.milkyroute.net Git - dolphin.git/commitdiff
Overhaul main view accessibility
authorFelix Ernst <felixernst@kde.org>
Mon, 28 Oct 2024 13:25:10 +0000 (13:25 +0000)
committerFelix Ernst <felixernst@kde.org>
Mon, 28 Oct 2024 13:25:10 +0000 (13:25 +0000)
This commit brings the main view of Dolphin into a usable state
accessibility-wise. Users of screen readers should have a way better
experience while browsing files and folders and navigating along the
file system hierarchy.

This commit fixes most of the remaining already-identified
accessibility issues listed in
https://invent.kde.org/teams/accessibility/collaboration/-/issues/28,
but not all. Namely, these should now be fixed:

1. Orca should read the element type in dolphin (file, folder, device,
link to folder, link to file)
2. Orca should read complete label in icon and compact view mode,
currently it only speaks the name, but there could be additional
information like the number of elements or the file size.
3. Orca is not able to announce Selecting / Unselecting files in
Dolphin. It also never announces how many items are selected in total.
(Announcing the total selection can be done by reading out the view
element or by pressing the Tab key to get to the status bar with the
relevant information.)
4. Dolphin opens on the home directory, but Orca doesn't tell you so.
Consider enclosing the area in a frame/panel which updates its
accessible name each time you modify the current path by entering or
leaving a directory.
5. I don't know what the folder presentation widget is, but it should
be presented as a grid view. Currently, we have a terrible experience
because the entire row of folders is read at once, with no indication
that we can move left and right with the arrows to go between the
elements of a row. When I found that out, however, I discovered that
when you're on the last icon of the first row and press right arrow,
you get to the first icon of the next row, but that's not announced,
instead, the whole row is announced at once
6. Orca should announce the current elements instead of "layered pane"
when the Folder / File view gets the focus in dolphin
7. Orca reads only name in Table View only of Dolphin
8. Items are sometimes confusingly announced as "collapsed" in contexts
in which there is no concept of collapsing/expanding e.g. in icon view
mode.

A lot of code was moved around and renamed. The three accessibility
classes, which all used to be in the same file, are moved into separate
files.

*Acknowledgement*
Thanks to Christian Hempfling and bgt lover for testing as well as
originally identifying a lot of the pain points being addressed here.

This work is part of a my project funded through the NGI0 Entrust Fund,
a fund established by NLnet with financial support from the European
Commission's Next Generation Internet programme, under the aegis of DG
Communications Networks, Content and Technology.
https://kde.org/announcements/2024_ngi_openletter/

22 files changed:
src/CMakeLists.txt
src/animatedheightwidget.cpp
src/dolphintabwidget.cpp
src/kitemviews/accessibility/kitemlistcontaineraccessible.cpp [new file with mode: 0644]
src/kitemviews/accessibility/kitemlistcontaineraccessible.h [new file with mode: 0644]
src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp [new file with mode: 0644]
src/kitemviews/accessibility/kitemlistdelegateaccessible.h [new file with mode: 0644]
src/kitemviews/accessibility/kitemlistviewaccessible.cpp [new file with mode: 0644]
src/kitemviews/accessibility/kitemlistviewaccessible.h [new file with mode: 0644]
src/kitemviews/kfileitemlistwidget.cpp
src/kitemviews/kfileitemlistwidget.h
src/kitemviews/kfileitemmodel.cpp
src/kitemviews/kitemlistcontainer.cpp
src/kitemviews/kitemlistcontroller.cpp
src/kitemviews/kitemlistview.cpp
src/kitemviews/kitemlistview.h
src/kitemviews/kitemlistviewaccessible.cpp [deleted file]
src/kitemviews/kitemlistviewaccessible.h [deleted file]
src/kitemviews/kstandarditemlistwidget.cpp
src/kitemviews/kstandarditemlistwidget.h
src/statusbar/dolphinstatusbar.cpp
src/views/dolphinview.cpp

index 7d1206e48a6326c9bdb4c5aad63bbb4bf61add31..ef50cf77db8fa3444d5844902836371283b5425c 100644 (file)
@@ -2,6 +2,7 @@ include(ECMAddAppIcon)
 
 set(ADMIN_WORKER_PACKAGE_NAME "kio-admin")
 set(FILELIGHT_PACKAGE_NAME "filelight")
+
 configure_file(config-dolphin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-dolphin.h)
 
 add_definitions(
@@ -52,6 +53,18 @@ install(FILES ${dolphinvcs_LIB_HEADERS} DESTINATION "${KDE_INSTALL_INCLUDEDIR}/D
 
 add_library(dolphinprivate SHARED)
 
+if(NOT QT_NO_ACCESSIBILITY)
+    target_sources(dolphinprivate PRIVATE
+        kitemviews/accessibility/kitemlistcontaineraccessible.cpp
+        kitemviews/accessibility/kitemlistdelegateaccessible.cpp
+        kitemviews/accessibility/kitemlistviewaccessible.cpp
+
+        kitemviews/accessibility/kitemlistcontaineraccessible.h
+        kitemviews/accessibility/kitemlistdelegateaccessible.h
+        kitemviews/accessibility/kitemlistviewaccessible.h
+    )
+endif()
+
 target_sources(dolphinprivate PRIVATE
     kitemviews/kfileitemlistview.cpp
     kitemviews/kfileitemlistwidget.cpp
@@ -65,7 +78,6 @@ target_sources(dolphinprivate PRIVATE
     kitemviews/kitemlistselectionmanager.cpp
     kitemviews/kitemliststyleoption.cpp
     kitemviews/kitemlistview.cpp
-    kitemviews/kitemlistviewaccessible.cpp
     kitemviews/kitemlistwidget.cpp
     kitemviews/kitemmodelbase.cpp
     kitemviews/kitemset.cpp
@@ -120,7 +132,6 @@ target_sources(dolphinprivate PRIVATE
     kitemviews/kitemlistselectionmanager.h
     kitemviews/kitemliststyleoption.h
     kitemviews/kitemlistview.h
-    kitemviews/kitemlistviewaccessible.h
     kitemviews/kitemlistwidget.h
     kitemviews/kitemmodelbase.h
     kitemviews/kitemset.h
index cee1a4922256b9f273f85b524b9a22f772b5e5eb..f1631bb6fa4764f84d6cb3b21e1e9cfa862ace6f 100644 (file)
@@ -81,6 +81,7 @@ QWidget *AnimatedHeightWidget::prepareContentsContainer(QWidget *contentsContain
                "Another contentsContainer has already been prepared. There can only be one.");
     contentsContainer->setParent(m_contentsContainerParent);
     m_contentsContainerParent->setWidget(contentsContainer);
+    m_contentsContainerParent->setFocusProxy(contentsContainer);
     return contentsContainer;
 }
 
index f80b94ea7cc49e43775da1a0d57ae1dfa82920b0..825ff3c7f8c76642edf9175fca5e40a0193fa9a1 100644 (file)
@@ -21,6 +21,7 @@
 
 #include <QApplication>
 #include <QDropEvent>
+#include <QStackedWidget>
 
 DolphinTabWidget::DolphinTabWidget(DolphinNavigatorsWidgetAction *navigatorsWidget, QWidget *parent)
     : QTabWidget(parent)
@@ -43,6 +44,13 @@ DolphinTabWidget::DolphinTabWidget(DolphinNavigatorsWidgetAction *navigatorsWidg
     setElideMode(Qt::ElideRight);
     setUsesScrollButtons(true);
     setTabBarAutoHide(true);
+
+    auto stackWidget{findChild<QStackedWidget *>()};
+    // i18n: This accessible name will be announced any time the user moves keyboard focus e.g. from the toolbar or the places panel towards the main working
+    // area of Dolphin. It gives structure. This container does not only contain the main view but also the status bar, the search panel, filter, and selection
+    // mode bars, so calling it just a "View" is a bit wrong, but hopefully still gets the point across.
+    stackWidget->setAccessibleName(i18nc("accessible name of Dolphin's view container", "Location View")); // Without this call, the non-descript Qt provided
+                                                                                                           // "Layered Pane" role is announced.
 }
 
 DolphinTabPage *DolphinTabWidget::currentTabPage() const
diff --git a/src/kitemviews/accessibility/kitemlistcontaineraccessible.cpp b/src/kitemviews/accessibility/kitemlistcontaineraccessible.cpp
new file mode 100644 (file)
index 0000000..6abf450
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * SPDX-FileCopyrightText: 2012 Amandeep Singh <aman.dedman@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "kitemlistcontaineraccessible.h"
+
+#include "kitemlistcontaineraccessible.h"
+#include "kitemlistviewaccessible.h"
+#include "kitemviews/kitemlistcontainer.h"
+#include "kitemviews/kitemlistcontroller.h"
+#include "kitemviews/kitemlistselectionmanager.h"
+#include "kitemviews/kitemlistview.h"
+#include "kitemviews/kitemmodelbase.h"
+
+#include <KLocalizedString>
+
+KItemListContainerAccessible::KItemListContainerAccessible(KItemListContainer *container)
+    : QAccessibleWidget(container)
+{
+}
+
+KItemListContainerAccessible::~KItemListContainerAccessible()
+{
+}
+
+QString KItemListContainerAccessible::text(QAccessible::Text t) const
+{
+    Q_UNUSED(t)
+    return QString(); // This class should never have focus. Instead KItemListViewAccessible should be focused and read out.
+}
+
+int KItemListContainerAccessible::childCount() const
+{
+    return 1;
+}
+
+int KItemListContainerAccessible::indexOfChild(const QAccessibleInterface *child) const
+{
+    if (child == KItemListContainerAccessible::child(0)) {
+        return 0;
+    }
+    return -1;
+}
+
+QAccessibleInterface *KItemListContainerAccessible::child(int index) const
+{
+    if (index == 0) {
+        Q_CHECK_PTR(static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(container()->controller()->view())));
+        return QAccessible::queryAccessibleInterface(container()->controller()->view());
+    }
+    qWarning("Calling KItemListContainerAccessible::child(index) with index != 0 is always pointless.");
+    return nullptr;
+}
+
+QAccessibleInterface *KItemListContainerAccessible::focusChild() const
+{
+    return child(0);
+}
+
+QAccessible::State KItemListContainerAccessible::state() const
+{
+    auto state = QAccessibleWidget::state();
+    state.focusable = false;
+    state.focused = false;
+    return state;
+}
+
+void KItemListContainerAccessible::doAction(const QString &actionName)
+{
+    auto view = static_cast<KItemListViewAccessible *>(child(0));
+    Q_CHECK_PTR(view); // A container should always have a view. Otherwise it has no reason to exist.
+    if (actionName == setFocusAction() && view) {
+        view->doAction(actionName);
+        return;
+    }
+    QAccessibleWidget::doAction(actionName);
+}
+
+const KItemListContainer *KItemListContainerAccessible::container() const
+{
+    Q_CHECK_PTR(qobject_cast<KItemListContainer *>(object()));
+    return static_cast<KItemListContainer *>(object());
+}
diff --git a/src/kitemviews/accessibility/kitemlistcontaineraccessible.h b/src/kitemviews/accessibility/kitemlistcontaineraccessible.h
new file mode 100644 (file)
index 0000000..5a7147a
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SPDX-FileCopyrightText: 2012 Amandeep Singh <aman.dedman@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef KITEMLISTCONTAINERACCESSIBLE_H
+#define KITEMLISTCONTAINERACCESSIBLE_H
+
+#include "dolphin_export.h"
+
+#include <QAccessibleWidget>
+
+class KItemListContainer;
+class KItemListViewAccessible;
+
+/**
+ * The accessible interface for KItemListContainer.
+ *
+ * Truthfully, there is absolutely no reason for screen reader users to interact with this interface.
+ * It is only there to bridge the gap between custom accessible interfaces and the automatically by Qt and QWidgets provided accessible interfaces.
+ * Really, the main issue is that KItemListContainer itself is the last proper QWidget in the hierarchy while the actual main view is completely custom using
+ * QGraphicsView instead, so focus usually officially goes to KItemListContainer which messes with the custom accessibility hierarchy.
+ */
+class DOLPHIN_EXPORT KItemListContainerAccessible : public QAccessibleWidget
+{
+public:
+    explicit KItemListContainerAccessible(KItemListContainer *container);
+    ~KItemListContainerAccessible() override;
+
+    QString text(QAccessible::Text t) const override;
+
+    QAccessibleInterface *child(int index) const override;
+    QAccessibleInterface *focusChild() const override;
+    int childCount() const override;
+    int indexOfChild(const QAccessibleInterface *child) const override;
+
+    QAccessible::State state() const override;
+    void doAction(const QString &actionName) override;
+
+    /** @returns the object() of this interface cast to its actual class. */
+    const KItemListContainer *container() const;
+};
+
+#endif // KITEMLISTCONTAINERACCESSIBLE_H
diff --git a/src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp b/src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp
new file mode 100644 (file)
index 0000000..dcfe3af
--- /dev/null
@@ -0,0 +1,240 @@
+/*
+ * SPDX-FileCopyrightText: 2012 Amandeep Singh <aman.dedman@gmail.com>
+ * SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "kitemlistdelegateaccessible.h"
+#include "kitemviews/kfileitemlistwidget.h"
+#include "kitemviews/kfileitemmodel.h"
+#include "kitemviews/kitemlistcontroller.h"
+#include "kitemviews/kitemlistselectionmanager.h"
+#include "kitemviews/kitemlistview.h"
+#include "kitemviews/private/kitemlistviewlayouter.h"
+
+#include <KLocalizedString>
+
+#include <QGraphicsScene>
+#include <QGraphicsView>
+
+KItemListDelegateAccessible::KItemListDelegateAccessible(KItemListView *view, int index)
+    : m_view(view)
+    , m_index(index)
+{
+    Q_ASSERT(index >= 0 && index < view->model()->count());
+}
+
+void *KItemListDelegateAccessible::interface_cast(QAccessible::InterfaceType type)
+{
+    if (type == QAccessible::TableCellInterface) {
+        return static_cast<QAccessibleTableCellInterface *>(this);
+    }
+    return nullptr;
+}
+
+int KItemListDelegateAccessible::columnExtent() const
+{
+    return 1;
+}
+
+int KItemListDelegateAccessible::rowExtent() const
+{
+    return 1;
+}
+
+QList<QAccessibleInterface *> KItemListDelegateAccessible::rowHeaderCells() const
+{
+    return QList<QAccessibleInterface *>();
+}
+
+QList<QAccessibleInterface *> KItemListDelegateAccessible::columnHeaderCells() const
+{
+    return QList<QAccessibleInterface *>();
+}
+
+int KItemListDelegateAccessible::columnIndex() const
+{
+    return m_view->m_layouter->itemColumn(m_index);
+}
+
+int KItemListDelegateAccessible::rowIndex() const
+{
+    return m_view->m_layouter->itemRow(m_index);
+}
+
+bool KItemListDelegateAccessible::isSelected() const
+{
+    return m_view->controller()->selectionManager()->isSelected(m_index);
+}
+
+QAccessibleInterface *KItemListDelegateAccessible::table() const
+{
+    return QAccessible::queryAccessibleInterface(m_view);
+}
+
+QAccessible::Role KItemListDelegateAccessible::role() const
+{
+    return QAccessible::ListItem; // We could also return "Cell" here which would then announce the exact row and column of the item. However, different from
+    // applications that actually have a strong cell workflow -- like LibreOfficeCalc -- we have no advantage of announcing the row or column aside from us
+    // generally being interested in announcing that users in Icon View mode need to use the Left and Right arrow keys to arrive at every item. There are ways
+    // for users to figure this out regardless by paying attention to the index that is being announced for each list item. In KitemListViewAccessible in icon
+    // view mode it is also mentioned that the items are positioned in a grid, so the two-dimensionality should be clear enough.
+}
+
+QAccessible::State KItemListDelegateAccessible::state() const
+{
+    QAccessible::State state;
+
+    state.selectable = true;
+    if (isSelected()) {
+        state.selected = true;
+    }
+
+    state.focusable = true;
+    if (m_view->controller()->selectionManager()->currentItem() == m_index) {
+        state.focused = true;
+        state.active = true;
+    }
+
+    if (m_view->controller()->selectionBehavior() == KItemListController::MultiSelection) {
+        state.multiSelectable = true;
+    }
+
+    if (m_view->supportsItemExpanding() && m_view->model()->isExpandable(m_index)) {
+        state.expandable = true;
+        state.expanded = m_view->model()->isExpanded(m_index);
+        state.collapsed = !state.expanded;
+    }
+
+    return state;
+}
+
+bool KItemListDelegateAccessible::isExpandable() const
+{
+    return m_view->model()->isExpandable(m_index);
+}
+
+QRect KItemListDelegateAccessible::rect() const
+{
+    QRect rect = m_view->itemRect(m_index).toRect();
+
+    if (rect.isNull()) {
+        return QRect();
+    }
+
+    rect.translate(m_view->mapToScene(QPointF(0.0, 0.0)).toPoint());
+    rect.translate(m_view->scene()->views()[0]->mapToGlobal(QPoint(0, 0)));
+    return rect;
+}
+
+QString KItemListDelegateAccessible::text(QAccessible::Text t) const
+{
+    const QHash<QByteArray, QVariant> data = m_view->model()->data(m_index);
+    switch (t) {
+    case QAccessible::Name: {
+        return data["text"].toString();
+    }
+    case QAccessible::Description: {
+        QString description;
+
+        if (data["isHidden"].toBool()) {
+            description += i18nc("@info", "hidden");
+        }
+
+        QString mimeType{data["type"].toString()};
+        if (mimeType.isEmpty()) {
+            const KFileItemModel *model = qobject_cast<KFileItemModel *>(m_view->model());
+            if (model) {
+                mimeType = model->fileItem(m_index).mimeComment();
+            }
+            Q_ASSERT_X(!mimeType.isEmpty(), "KItemListDelegateAccessible::text", "Unable to retrieve mime type.");
+        }
+
+        if (data["isLink"].toBool()) {
+            QString linkDestination{data["destination"].toString()};
+            if (linkDestination.isEmpty()) {
+                const KFileItemModel *model = qobject_cast<KFileItemModel *>(m_view->model());
+                if (model) {
+                    linkDestination = model->fileItem(m_index).linkDest();
+                }
+                Q_ASSERT_X(!linkDestination.isEmpty(), "KItemListDelegateAccessible::text", "Unable to retrieve link destination.");
+            }
+
+            description += i18nc("@info enumeration saying this is a link to $1, %1 is mimeType", ", link to %1 at %2", mimeType, linkDestination);
+        } else {
+            description += i18nc("@info enumeration, %1 is mimeType", ", %1", mimeType);
+        }
+        const QList<QByteArray> additionallyShownInformation{m_view->visibleRoles()};
+        const KItemModelBase *model = m_view->model();
+        for (const auto &roleInformation : additionallyShownInformation) {
+            if (roleInformation == "text") {
+                continue;
+            }
+            KFileItemListWidgetInformant informant;
+            const auto roleText{informant.roleText(roleInformation, data, KFileItemListWidgetInformant::ForUsageAs::SpokenText)};
+            if (roleText.isEmpty()) {
+                continue; // No need to announce roles which are empty for this item.
+            }
+            description +=
+                // i18n: The text starts with a comma because multiple occurences of this text can follow after each others as an enumeration.
+                // Normally it would make sense to have a colon between property and value to make the relation between the property and its property value
+                // clear, however this is accessible text that will be read out by screen readers. That's why there is only a space between the two here,
+                // because screen readers would read the colon literally as "colon", which is just a waste of time for users who might go through a list of
+                // hundreds of items. So, if you want to add any more punctation there to improve structure, try to make sure that it will not lead to annoying
+                // announcements when read out by a screen reader.
+                i18nc("@info accessibility enumeration, %1 is property, %2 is value", ", %1 %2", model->roleDescription(roleInformation), roleText);
+        }
+        return description;
+    }
+    default:
+        break;
+    }
+
+    return QString();
+}
+
+void KItemListDelegateAccessible::setText(QAccessible::Text, const QString &)
+{
+}
+
+QAccessibleInterface *KItemListDelegateAccessible::child(int) const
+{
+    return nullptr;
+}
+
+bool KItemListDelegateAccessible::isValid() const
+{
+    return m_view && (m_index >= 0) && (m_index < m_view->model()->count());
+}
+
+QAccessibleInterface *KItemListDelegateAccessible::childAt(int, int) const
+{
+    return nullptr;
+}
+
+int KItemListDelegateAccessible::childCount() const
+{
+    return 0;
+}
+
+int KItemListDelegateAccessible::indexOfChild(const QAccessibleInterface *child) const
+{
+    Q_UNUSED(child)
+    return -1;
+}
+
+QAccessibleInterface *KItemListDelegateAccessible::parent() const
+{
+    return QAccessible::queryAccessibleInterface(m_view);
+}
+
+int KItemListDelegateAccessible::index() const
+{
+    return m_index;
+}
+
+QObject *KItemListDelegateAccessible::object() const
+{
+    return nullptr;
+}
diff --git a/src/kitemviews/accessibility/kitemlistdelegateaccessible.h b/src/kitemviews/accessibility/kitemlistdelegateaccessible.h
new file mode 100644 (file)
index 0000000..f9f6d57
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * SPDX-FileCopyrightText: 2012 Amandeep Singh <aman.dedman@gmail.com>
+ * SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef KITEMLISTDELEGATEACCESSIBLE_H
+#define KITEMLISTDELEGATEACCESSIBLE_H
+
+#include "dolphin_export.h"
+
+#include <QAccessibleInterface>
+#include <QAccessibleTableCellInterface>
+#include <QPointer>
+
+class KItemListView;
+
+/**
+ * The accessibility class that represents singular files or folders in the main view.
+ */
+class DOLPHIN_EXPORT KItemListDelegateAccessible : public QAccessibleInterface, public QAccessibleTableCellInterface
+{
+public:
+    KItemListDelegateAccessible(KItemListView *view, int m_index);
+
+    void *interface_cast(QAccessible::InterfaceType type) override;
+    QObject *object() const override;
+    bool isValid() const override;
+    QAccessible::Role role() const override;
+    QAccessible::State state() const override;
+    QRect rect() const override;
+    QString text(QAccessible::Text t) const override;
+    void setText(QAccessible::Text t, const QString &text) override;
+
+    QAccessibleInterface *child(int index) const override;
+    int childCount() const override;
+    QAccessibleInterface *childAt(int x, int y) const override;
+    int indexOfChild(const QAccessibleInterface *) const override;
+
+    QAccessibleInterface *parent() const override;
+    bool isExpandable() const;
+
+    // Cell Interface
+    int columnExtent() const override;
+    QList<QAccessibleInterface *> columnHeaderCells() const override;
+    int columnIndex() const override;
+    int rowExtent() const override;
+    QList<QAccessibleInterface *> rowHeaderCells() const override;
+    int rowIndex() const override;
+    bool isSelected() const override;
+    QAccessibleInterface *table() const override;
+
+    int index() const;
+
+private:
+    QPointer<KItemListView> m_view;
+    int m_index;
+};
+
+#endif // KITEMLISTDELEGATEACCESSIBLE_H
diff --git a/src/kitemviews/accessibility/kitemlistviewaccessible.cpp b/src/kitemviews/accessibility/kitemlistviewaccessible.cpp
new file mode 100644 (file)
index 0000000..2643eb3
--- /dev/null
@@ -0,0 +1,479 @@
+/*
+ * SPDX-FileCopyrightText: 2012 Amandeep Singh <aman.dedman@gmail.com>
+ * SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "kitemlistviewaccessible.h"
+#include "kitemlistcontaineraccessible.h"
+#include "kitemlistdelegateaccessible.h"
+
+#include "kitemviews/kitemlistcontainer.h"
+#include "kitemviews/kitemlistcontroller.h"
+#include "kitemviews/kitemlistselectionmanager.h"
+#include "kitemviews/kitemlistview.h"
+#include "kitemviews/kitemmodelbase.h"
+#include "kitemviews/kstandarditemlistview.h"
+#include "kitemviews/private/kitemlistviewlayouter.h"
+
+#include <KLocalizedString>
+
+#include <QApplication> // for figuring out if we should move focus to this view.
+#include <QGraphicsScene>
+#include <QGraphicsView>
+
+KItemListSelectionManager *KItemListViewAccessible::selectionManager() const
+{
+    return view()->controller()->selectionManager();
+}
+
+KItemListViewAccessible::KItemListViewAccessible(KItemListView *view_, KItemListContainerAccessible *parent)
+    : QAccessibleObject(view_)
+    , m_parent(parent)
+{
+    Q_ASSERT(view());
+    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);
+    });
+}
+
+KItemListViewAccessible::~KItemListViewAccessible()
+{
+    for (AccessibleIdWrapper idWrapper : std::as_const(m_accessibleDelegates)) {
+        if (idWrapper.isValid) {
+            QAccessible::deleteAccessibleInterface(idWrapper.id);
+        }
+    }
+}
+
+void *KItemListViewAccessible::interface_cast(QAccessible::InterfaceType type)
+{
+    switch (type) {
+    case QAccessible::SelectionInterface:
+        return static_cast<QAccessibleSelectionInterface *>(this);
+    case QAccessible::TableInterface:
+        return static_cast<QAccessibleTableInterface *>(this);
+    case QAccessible::ActionInterface:
+        return static_cast<QAccessibleActionInterface *>(this);
+    default:
+        return nullptr;
+    }
+}
+
+void KItemListViewAccessible::modelReset()
+{
+}
+
+QAccessibleInterface *KItemListViewAccessible::accessibleDelegate(int index) const
+{
+    if (index < 0 || index >= view()->model()->count()) {
+        return nullptr;
+    }
+
+    if (m_accessibleDelegates.size() <= index) {
+        m_accessibleDelegates.resize(childCount());
+    }
+    Q_ASSERT(index < m_accessibleDelegates.size());
+
+    AccessibleIdWrapper idWrapper = m_accessibleDelegates.at(index);
+    if (!idWrapper.isValid) {
+        idWrapper.id = QAccessible::registerAccessibleInterface(new KItemListDelegateAccessible(view(), index));
+        idWrapper.isValid = true;
+        m_accessibleDelegates.insert(index, idWrapper);
+    }
+    return QAccessible::accessibleInterface(idWrapper.id);
+}
+
+QAccessibleInterface *KItemListViewAccessible::cellAt(int row, int column) const
+{
+    return accessibleDelegate(columnCount() * row + column);
+}
+
+QAccessibleInterface *KItemListViewAccessible::caption() const
+{
+    return nullptr;
+}
+
+QString KItemListViewAccessible::columnDescription(int) const
+{
+    return QString();
+}
+
+int KItemListViewAccessible::columnCount() const
+{
+    return view()->m_layouter->columnCount();
+}
+
+int KItemListViewAccessible::rowCount() const
+{
+    if (columnCount() <= 0) {
+        return 0;
+    }
+
+    int itemCount = view()->model()->count();
+    int rowCount = itemCount / columnCount();
+
+    if (rowCount <= 0) {
+        return 0;
+    }
+
+    if (itemCount % columnCount()) {
+        ++rowCount;
+    }
+    return rowCount;
+}
+
+int KItemListViewAccessible::selectedCellCount() const
+{
+    return selectionManager()->selectedItems().count();
+}
+
+int KItemListViewAccessible::selectedColumnCount() const
+{
+    return 0;
+}
+
+int KItemListViewAccessible::selectedRowCount() const
+{
+    return 0;
+}
+
+QString KItemListViewAccessible::rowDescription(int) const
+{
+    return QString();
+}
+
+QList<QAccessibleInterface *> KItemListViewAccessible::selectedCells() const
+{
+    QList<QAccessibleInterface *> cells;
+    const auto items = selectionManager()->selectedItems();
+    cells.reserve(items.count());
+    for (int index : items) {
+        cells.append(accessibleDelegate(index));
+    }
+    return cells;
+}
+
+QList<int> KItemListViewAccessible::selectedColumns() const
+{
+    return QList<int>();
+}
+
+QList<int> KItemListViewAccessible::selectedRows() const
+{
+    return QList<int>();
+}
+
+QAccessibleInterface *KItemListViewAccessible::summary() const
+{
+    return nullptr;
+}
+
+bool KItemListViewAccessible::isColumnSelected(int) const
+{
+    return false;
+}
+
+bool KItemListViewAccessible::isRowSelected(int) const
+{
+    return false;
+}
+
+bool KItemListViewAccessible::selectRow(int)
+{
+    return true;
+}
+
+bool KItemListViewAccessible::selectColumn(int)
+{
+    return true;
+}
+
+bool KItemListViewAccessible::unselectRow(int)
+{
+    return true;
+}
+
+bool KItemListViewAccessible::unselectColumn(int)
+{
+    return true;
+}
+
+void KItemListViewAccessible::modelChange(QAccessibleTableModelChangeEvent * /*event*/)
+{
+}
+
+QAccessible::Role KItemListViewAccessible::role() const
+{
+    return QAccessible::List;
+}
+
+QAccessible::State KItemListViewAccessible::state() const
+{
+    QAccessible::State s;
+    s.focusable = true;
+    s.active = true;
+    const KItemListController *controller = view()->m_controller;
+    s.multiSelectable = controller->selectionBehavior() == KItemListController::MultiSelection;
+    s.focused = !childCount() && (view()->hasFocus() || m_parent->container()->hasFocus()); // Usually the children have focus.
+    return s;
+}
+
+QAccessibleInterface *KItemListViewAccessible::childAt(int x, int y) const
+{
+    const QPointF point = QPointF(x, y);
+    const std::optional<int> itemIndex = view()->itemAt(view()->mapFromScene(point));
+    return child(itemIndex.value_or(-1));
+}
+
+QAccessibleInterface *KItemListViewAccessible::parent() const
+{
+    return m_parent;
+}
+
+int KItemListViewAccessible::childCount() const
+{
+    return view()->model()->count();
+}
+
+int KItemListViewAccessible::indexOfChild(const QAccessibleInterface *interface) const
+{
+    const KItemListDelegateAccessible *widget = static_cast<const KItemListDelegateAccessible *>(interface);
+    return widget->index();
+}
+
+QString KItemListViewAccessible::text(QAccessible::Text t) const
+{
+    const KItemListController *controller = view()->m_controller;
+    const KItemModelBase *model = controller->model();
+    const QUrl modelRootUrl = model->directory();
+    if (t == QAccessible::Name) {
+        return modelRootUrl.fileName();
+    }
+    if (t != QAccessible::Description) {
+        return QString();
+    }
+    const auto currentItem = child(controller->selectionManager()->currentItem());
+    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",
+                     m_placeholderMessage,
+                     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()};
+
+    // 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");
+        }
+    }
+
+    /**
+     * 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());
+}
+
+QRect KItemListViewAccessible::rect() const
+{
+    if (!view()->isVisible()) {
+        return QRect();
+    }
+
+    const QGraphicsScene *scene = view()->scene();
+    if (scene) {
+        const QPoint origin = scene->views().at(0)->mapToGlobal(QPoint(0, 0));
+        const QRect viewRect = view()->geometry().toRect();
+        return viewRect.translated(origin);
+    } else {
+        return QRect();
+    }
+}
+
+QAccessibleInterface *KItemListViewAccessible::child(int index) const
+{
+    if (index >= 0 && index < childCount()) {
+        return accessibleDelegate(index);
+    }
+    return nullptr;
+}
+
+KItemListViewAccessible::AccessibleIdWrapper::AccessibleIdWrapper()
+    : isValid(false)
+    , id(0)
+{
+}
+
+/* Selection interface */
+
+bool KItemListViewAccessible::clear()
+{
+    selectionManager()->clearSelection();
+    return true;
+}
+
+bool KItemListViewAccessible::isSelected(QAccessibleInterface *childItem) const
+{
+    Q_CHECK_PTR(childItem);
+    return static_cast<KItemListDelegateAccessible *>(childItem)->isSelected();
+}
+
+bool KItemListViewAccessible::select(QAccessibleInterface *childItem)
+{
+    selectionManager()->setSelected(indexOfChild(childItem));
+    return true;
+}
+
+bool KItemListViewAccessible::selectAll()
+{
+    selectionManager()->setSelected(0, childCount());
+    return true;
+}
+
+QAccessibleInterface *KItemListViewAccessible::selectedItem(int selectionIndex) const
+{
+    const auto selectedItems = selectionManager()->selectedItems();
+    int i = 0;
+    for (auto it = selectedItems.rbegin(); it != selectedItems.rend(); ++it) {
+        if (i == selectionIndex) {
+            return child(*it);
+        }
+    }
+    return nullptr;
+}
+
+int KItemListViewAccessible::selectedItemCount() const
+{
+    return selectionManager()->selectedItems().count();
+}
+
+QList<QAccessibleInterface *> KItemListViewAccessible::selectedItems() const
+{
+    const auto selectedItems = selectionManager()->selectedItems();
+    QList<QAccessibleInterface *> selectedItemsInterfaces;
+    for (auto it = selectedItems.rbegin(); it != selectedItems.rend(); ++it) {
+        selectedItemsInterfaces.append(child(*it));
+    }
+    return selectedItemsInterfaces;
+}
+
+bool KItemListViewAccessible::unselect(QAccessibleInterface *childItem)
+{
+    selectionManager()->setSelected(indexOfChild(childItem), 1, KItemListSelectionManager::Deselect);
+    return true;
+}
+
+/* Action Interface */
+
+QStringList KItemListViewAccessible::actionNames() const
+{
+    return {setFocusAction()};
+}
+
+void KItemListViewAccessible::doAction(const QString &actionName)
+{
+    if (actionName == setFocusAction()) {
+        view()->setFocus();
+    }
+}
+
+QStringList KItemListViewAccessible::keyBindingsForAction(const QString &actionName) const
+{
+    Q_UNUSED(actionName)
+    return {};
+}
+
+/* Custom non-interface methods */
+
+KItemListView *KItemListViewAccessible::view() const
+{
+    Q_CHECK_PTR(qobject_cast<KItemListView *>(object()));
+    return static_cast<KItemListView *>(object());
+}
+
+void KItemListViewAccessible::announceOverallViewState(const QString &placeholderMessage)
+{
+    m_placeholderMessage = placeholderMessage;
+
+    // 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();
+        }
+    }
+}
+
+void KItemListViewAccessible::announceDescriptionChange()
+{
+    m_announceDescriptionChangeTimer->start();
+}
diff --git a/src/kitemviews/accessibility/kitemlistviewaccessible.h b/src/kitemviews/accessibility/kitemlistviewaccessible.h
new file mode 100644 (file)
index 0000000..4c44b18
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * SPDX-FileCopyrightText: 2012 Amandeep Singh <aman.dedman@gmail.com>
+ * SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef KITEMLISTVIEWACCESSIBLE_H
+#define KITEMLISTVIEWACCESSIBLE_H
+
+#include "dolphin_export.h"
+
+#include <QAccessible>
+#include <QAccessibleObject>
+#include <QAccessibleWidget>
+#include <QPointer>
+
+class KItemListView;
+class KItemListContainer;
+class KItemListContainerAccessible;
+class KItemListSelectionManager;
+
+/**
+ * The main class for making the main view accessible.
+ *
+ * Such a class is necessary because the KItemListView is a mostly custom entity. This class provides a lot of the functionality to make it possible to
+ * interact with the view using accessibility tools. It implements various interfaces mostly to generally allow working with the view as a whole. However,
+ * actually interacting with singular items within the view is implemented in KItemListDelegateAccessible.
+ *
+ * @note For documentation of most of the methods within this class, check out the documentation of the methods which are being overriden here.
+ */
+class DOLPHIN_EXPORT KItemListViewAccessible : public QAccessibleObject,
+                                               public QAccessibleSelectionInterface,
+                                               public QAccessibleTableInterface,
+                                               public QAccessibleActionInterface
+{
+public:
+    explicit KItemListViewAccessible(KItemListView *view, KItemListContainerAccessible *parent);
+    ~KItemListViewAccessible() override;
+
+    // QAccessibleObject
+    void *interface_cast(QAccessible::InterfaceType type) override;
+
+    QAccessible::Role role() const override;
+    QAccessible::State state() const override;
+    QString text(QAccessible::Text t) const override;
+    QRect rect() const override;
+
+    QAccessibleInterface *child(int index) const override;
+    int childCount() const override;
+    int indexOfChild(const QAccessibleInterface *) const override;
+    QAccessibleInterface *childAt(int x, int y) const override;
+    QAccessibleInterface *parent() const override;
+
+    // Table interface
+    QAccessibleInterface *cellAt(int row, int column) const override;
+    QAccessibleInterface *caption() const override;
+    QAccessibleInterface *summary() const override;
+    QString columnDescription(int column) const override;
+    QString rowDescription(int row) const override;
+    int columnCount() const override;
+    int rowCount() const override;
+
+    // Selection
+    int selectedCellCount() const override;
+    int selectedColumnCount() const override;
+    int selectedRowCount() const override;
+    QList<QAccessibleInterface *> selectedCells() const override;
+    QList<int> selectedColumns() const override;
+    QList<int> selectedRows() const override;
+    bool isColumnSelected(int column) const override;
+    bool isRowSelected(int row) const override;
+    bool selectRow(int row) override;
+    bool selectColumn(int column) override;
+    bool unselectRow(int row) override;
+    bool unselectColumn(int column) override;
+    void modelChange(QAccessibleTableModelChangeEvent *) override;
+
+    // Selection interface
+    /** Clear selection */
+    bool clear() override;
+    bool isSelected(QAccessibleInterface *childItem) const override;
+    bool select(QAccessibleInterface *childItem) override;
+    bool selectAll() override;
+    QAccessibleInterface *selectedItem(int selectionIndex) const override;
+    int selectedItemCount() const override;
+    QList<QAccessibleInterface *> selectedItems() const override;
+    bool unselect(QAccessibleInterface *childItem) override;
+
+    // Action interface
+    QStringList actionNames() const override;
+    void doAction(const QString &actionName) override;
+    QStringList keyBindingsForAction(const QString &actionName) const override;
+
+    // Custom non-interface methods
+    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
+     *                              DolphinView::m_placeholderLabel in both content and purpose. @see DolphinView::updatePlaceHolderLabel().
+     */
+    void announceOverallViewState(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.
+     */
+    void announceDescriptionChange();
+
+protected:
+    virtual void modelReset();
+    /**
+     * @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.
+     * Index is 0-based.
+     */
+    inline QAccessibleInterface *accessibleDelegate(int index) const;
+
+    KItemListSelectionManager *selectionManager() const;
+
+private:
+    /** @see setPlaceholderMessage(). */
+    QString m_placeholderMessage;
+
+    QTimer *m_announceDescriptionChangeTimer;
+
+    class AccessibleIdWrapper
+    {
+    public:
+        AccessibleIdWrapper();
+        bool isValid;
+        QAccessible::Id id;
+    };
+    /**
+     * A list that maps the indices of the children of this KItemListViewAccessible to the accessible ids of the matching KItemListDelegateAccessible objects.
+     * For example: m_accessibleDelegates.at(2) would be the AccessibleIdWrapper with an id which can be used to retrieve the QAccessibleObject that represents
+     *              the third file in this view.
+     */
+    mutable QVector<AccessibleIdWrapper> m_accessibleDelegates;
+
+    KItemListContainerAccessible *m_parent;
+};
+
+#endif
index b4e0895f2a7fcb0548e8393f63d9dd3832b28fb6..3a7b37895a70879c6f450baf98eed4f1f27695c2 100644 (file)
@@ -45,7 +45,7 @@ bool KFileItemListWidgetInformant::itemIsLink(int index, const KItemListView *vi
     return item.isLink();
 }
 
-QString KFileItemListWidgetInformant::roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values) const
+QString KFileItemListWidgetInformant::roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values, ForUsageAs forUsageAs) const
 {
     QString text;
     const QVariant roleValue = values.value(role);
@@ -55,11 +55,13 @@ QString KFileItemListWidgetInformant::roleText(const QByteArray &role, const QHa
     // Implementation note: In case if more roles require a custom handling
     // use a hash + switch for a linear runtime.
 
-    auto formatDate = [formatter, local](const QDateTime &time) {
+    auto formatDate = [formatter, local, forUsageAs](const QDateTime &time) {
         if (ContentDisplaySettings::useShortRelativeDates()) {
-            return formatter.formatRelativeDateTime(time, QLocale::ShortFormat);
+            return formatter.formatRelativeDateTime(time,
+                                                    forUsageAs == KStandardItemListWidgetInformant::ForUsageAs::DisplayedText ? QLocale::ShortFormat
+                                                                                                                              : QLocale::LongFormat);
         } else {
-            return local.toString(time, QLocale::ShortFormat);
+            return local.toString(time, forUsageAs == KStandardItemListWidgetInformant::ForUsageAs::DisplayedText ? QLocale::ShortFormat : QLocale::LongFormat);
         }
     };
 
@@ -114,7 +116,7 @@ QString KFileItemListWidgetInformant::roleText(const QByteArray &role, const QHa
             break;
         }
     } else {
-        text = KStandardItemListWidgetInformant::roleText(role, values);
+        text = KStandardItemListWidgetInformant::roleText(role, values, forUsageAs);
     }
 
     return text;
index 5ce11b6da5797c65a83268f445e0cd639913ce75..e2db43178bdb53b2b4f4ce5197b50cad4652749c 100644 (file)
@@ -28,8 +28,11 @@ public:
 protected:
     QString itemText(int index, const KItemListView *view) const override;
     bool itemIsLink(int index, const KItemListView *view) const override;
-    QString roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values) const override;
+    /** @see KStandardItemListWidget::roleText(). */
+    QString roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values, ForUsageAs forUsageAs = ForUsageAs::DisplayedText) const override;
     QFont customizedFontForLinks(const QFont &baseFont) const override;
+
+    friend class KItemListDelegateAccessible;
 };
 
 /**
index 3a60834af32b222ec6f2c6aad5c3b9ca3466248d..fb5851c36c52ee07ba04cc4eaaa615c16c86d387 100644 (file)
@@ -20,6 +20,9 @@
 #include <KLocalizedString>
 #include <KUrlMimeData>
 
+#ifndef QT_NO_ACCESSIBILITY
+#include <QAccessible>
+#endif
 #include <QElapsedTimer>
 #include <QIcon>
 #include <QMimeData>
index 2f9f5d401b78de2df8b5472e9107c603672421dc..65250832b023efe5e65b258b20dcaf6a0ee89651 100644 (file)
@@ -12,6 +12,9 @@
 #include "kitemlistview.h"
 #include "private/kitemlistsmoothscroller.h"
 
+#ifndef QT_NO_ACCESSIBILITY
+#include <QAccessibleEvent>
+#endif
 #include <QApplication>
 #include <QFontMetrics>
 #include <QGraphicsScene>
@@ -195,6 +198,17 @@ void KItemListContainer::focusInEvent(QFocusEvent *event)
     KItemListView *view = m_controller->view();
     if (view) {
         QApplication::sendEvent(view, event);
+
+        // We need to set the focus to the view or accessibility software will only announce the container (which has no information available itself).
+        // For some reason actively setting the focus to the view needs to be delayed or the focus will immediately go back to this container.
+        QTimer::singleShot(0, this, [this, view]() {
+            view->setFocus();
+#ifndef QT_NO_ACCESSIBILITY
+            QAccessibleEvent accessibleFocusInEvent(this, QAccessible::Focus);
+            accessibleFocusInEvent.setChild(0);
+            QAccessible::updateAccessibility(&accessibleFocusInEvent);
+#endif
+        });
     }
 }
 
index 821e1b75fea1ff100bd96d22a81c58092a44d29e..1db665f47afa3e2279152dc1941cac5c404bf179 100644 (file)
@@ -467,6 +467,12 @@ bool KItemListController::keyPressEvent(QKeyEvent *event)
 
     case Qt::Key_Space:
         if (m_selectionBehavior == MultiSelection) {
+#ifndef QT_NO_ACCESSIBILITY
+            // Move accessible focus to the item that is acted upon, so only the state change of this item is announced and not the whole view.
+            QAccessibleEvent accessibilityEvent(view(), QAccessible::Focus);
+            accessibilityEvent.setChild(index);
+            QAccessible::updateAccessibility(&accessibilityEvent);
+#endif
             if (controlPressed) {
                 // Toggle the selection state of the current item.
                 m_selectionManager->endAnchoredSelection();
index afc392810edf51edd24585a8976dde0d927bd822..b0ea32940f2e9b80b59f409c7e78cdb8cc25a8e1 100644 (file)
@@ -8,12 +8,16 @@
 
 #include "kitemlistview.h"
 
+#ifndef QT_NO_ACCESSIBILITY
+#include "accessibility/kitemlistcontaineraccessible.h"
+#include "accessibility/kitemlistdelegateaccessible.h"
+#include "accessibility/kitemlistviewaccessible.h"
+#endif
 #include "dolphindebug.h"
 #include "kitemlistcontainer.h"
 #include "kitemlistcontroller.h"
 #include "kitemlistheader.h"
 #include "kitemlistselectionmanager.h"
-#include "kitemlistviewaccessible.h"
 #include "kstandarditemlistwidget.h"
 
 #include "private/kitemlistheaderwidget.h"
@@ -1240,6 +1244,11 @@ void KItemListView::slotItemsInserted(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::slotItemsRemoved(const KItemRangeList &itemRanges)
@@ -1358,6 +1367,11 @@ 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)
@@ -1416,10 +1430,12 @@ void KItemListView::slotItemsChanged(const KItemRangeList &itemRanges, const QSe
             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);
@@ -1483,8 +1499,6 @@ void KItemListView::slotSortRoleChanged(const QByteArray &current, const QByteAr
 
 void KItemListView::slotCurrentChanged(int current, int previous)
 {
-    Q_UNUSED(previous)
-
     // In SingleSelection mode (e.g., in the Places Panel), the current item is
     // always the selected item. It is not necessary to highlight the current item then.
     if (m_controller->selectionBehavior() != KItemListController::SingleSelection) {
@@ -1496,25 +1510,52 @@ void KItemListView::slotCurrentChanged(int current, int previous)
         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.
+            }
         }
     }
-
-    QAccessibleEvent ev(this, QAccessible::Focus);
-    ev.setChild(current);
-    QAccessible::updateAccessibility(&ev);
+#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();
+    }
+#endif
 }
 
 void KItemListView::slotSelectionChanged(const KItemSet &current, const KItemSet &previous)
 {
-    Q_UNUSED(previous)
-
     QHashIterator<int, KItemListWidget *> it(m_visibleItems);
     while (it.hasNext()) {
         it.next();
         const int index = it.key();
         KItemListWidget *widget = it.value();
-        widget->setSelected(current.contains(index));
+        const bool isSelected(current.contains(index));
+        widget->setSelected(isSelected);
+
+#ifndef QT_NO_ACCESSIBILITY
+        if (!QAccessible::isActive()) {
+            continue;
+        }
+        // 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);
+            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)
+#endif
 }
 
 void KItemListView::slotAnimationFinished(QGraphicsWidget *widget, KItemListViewAnimation::AnimationType type)
index 8812eb8cc45d3b9fdb07649daece70f4b83d79f9..7be08302cda128effdec9b9e972af0378aa51943 100644 (file)
@@ -793,7 +793,7 @@ private:
     friend class KItemListController;
     friend class KItemListControllerTest;
     friend class KItemListViewAccessible;
-    friend class KItemListAccessibleCell;
+    friend class KItemListDelegateAccessible;
 };
 
 /**
diff --git a/src/kitemviews/kitemlistviewaccessible.cpp b/src/kitemviews/kitemlistviewaccessible.cpp
deleted file mode 100644 (file)
index a8d80ab..0000000
+++ /dev/null
@@ -1,467 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2012 Amandeep Singh <aman.dedman@gmail.com>
- *
- * SPDX-License-Identifier: GPL-2.0-or-later
- */
-
-#ifndef QT_NO_ACCESSIBILITY
-#include "kitemlistviewaccessible.h"
-
-#include "kitemlistcontainer.h"
-#include "kitemlistcontroller.h"
-#include "kitemlistselectionmanager.h"
-#include "kitemlistview.h"
-#include "private/kitemlistviewlayouter.h"
-
-#include <QGraphicsScene>
-#include <QGraphicsView>
-
-KItemListView *KItemListViewAccessible::view() const
-{
-    return qobject_cast<KItemListView *>(object());
-}
-
-KItemListViewAccessible::KItemListViewAccessible(KItemListView *view_, KItemListContainerAccessible *parent)
-    : QAccessibleObject(view_)
-    , m_parent(parent)
-{
-    Q_ASSERT(view());
-    Q_CHECK_PTR(parent);
-    m_cells.resize(childCount());
-}
-
-KItemListViewAccessible::~KItemListViewAccessible()
-{
-    for (AccessibleIdWrapper idWrapper : std::as_const(m_cells)) {
-        if (idWrapper.isValid) {
-            QAccessible::deleteAccessibleInterface(idWrapper.id);
-        }
-    }
-}
-
-void *KItemListViewAccessible::interface_cast(QAccessible::InterfaceType type)
-{
-    if (type == QAccessible::TableInterface) {
-        return static_cast<QAccessibleTableInterface *>(this);
-    }
-    return nullptr;
-}
-
-void KItemListViewAccessible::modelReset()
-{
-}
-
-QAccessibleInterface *KItemListViewAccessible::cell(int index) const
-{
-    if (index < 0 || index >= view()->model()->count()) {
-        return nullptr;
-    }
-
-    if (m_cells.size() <= index) {
-        m_cells.resize(childCount());
-    }
-    Q_ASSERT(index < m_cells.size());
-
-    AccessibleIdWrapper idWrapper = m_cells.at(index);
-    if (!idWrapper.isValid) {
-        idWrapper.id = QAccessible::registerAccessibleInterface(new KItemListAccessibleCell(view(), index));
-        idWrapper.isValid = true;
-        m_cells.insert(index, idWrapper);
-    }
-    return QAccessible::accessibleInterface(idWrapper.id);
-}
-
-QAccessibleInterface *KItemListViewAccessible::cellAt(int row, int column) const
-{
-    return cell(columnCount() * row + column);
-}
-
-QAccessibleInterface *KItemListViewAccessible::caption() const
-{
-    return nullptr;
-}
-
-QString KItemListViewAccessible::columnDescription(int) const
-{
-    return QString();
-}
-
-int KItemListViewAccessible::columnCount() const
-{
-    return view()->m_layouter->columnCount();
-}
-
-int KItemListViewAccessible::rowCount() const
-{
-    if (columnCount() <= 0) {
-        return 0;
-    }
-
-    int itemCount = view()->model()->count();
-    int rowCount = itemCount / columnCount();
-
-    if (rowCount <= 0) {
-        return 0;
-    }
-
-    if (itemCount % columnCount()) {
-        ++rowCount;
-    }
-    return rowCount;
-}
-
-int KItemListViewAccessible::selectedCellCount() const
-{
-    return view()->controller()->selectionManager()->selectedItems().count();
-}
-
-int KItemListViewAccessible::selectedColumnCount() const
-{
-    return 0;
-}
-
-int KItemListViewAccessible::selectedRowCount() const
-{
-    return 0;
-}
-
-QString KItemListViewAccessible::rowDescription(int) const
-{
-    return QString();
-}
-
-QList<QAccessibleInterface *> KItemListViewAccessible::selectedCells() const
-{
-    QList<QAccessibleInterface *> cells;
-    const auto items = view()->controller()->selectionManager()->selectedItems();
-    cells.reserve(items.count());
-    for (int index : items) {
-        cells.append(cell(index));
-    }
-    return cells;
-}
-
-QList<int> KItemListViewAccessible::selectedColumns() const
-{
-    return QList<int>();
-}
-
-QList<int> KItemListViewAccessible::selectedRows() const
-{
-    return QList<int>();
-}
-
-QAccessibleInterface *KItemListViewAccessible::summary() const
-{
-    return nullptr;
-}
-
-bool KItemListViewAccessible::isColumnSelected(int) const
-{
-    return false;
-}
-
-bool KItemListViewAccessible::isRowSelected(int) const
-{
-    return false;
-}
-
-bool KItemListViewAccessible::selectRow(int)
-{
-    return true;
-}
-
-bool KItemListViewAccessible::selectColumn(int)
-{
-    return true;
-}
-
-bool KItemListViewAccessible::unselectRow(int)
-{
-    return true;
-}
-
-bool KItemListViewAccessible::unselectColumn(int)
-{
-    return true;
-}
-
-void KItemListViewAccessible::modelChange(QAccessibleTableModelChangeEvent * /*event*/)
-{
-}
-
-QAccessible::Role KItemListViewAccessible::role() const
-{
-    return QAccessible::Table;
-}
-
-QAccessible::State KItemListViewAccessible::state() const
-{
-    QAccessible::State s;
-    return s;
-}
-
-QAccessibleInterface *KItemListViewAccessible::childAt(int x, int y) const
-{
-    const QPointF point = QPointF(x, y);
-    const std::optional<int> itemIndex = view()->itemAt(view()->mapFromScene(point));
-    return child(itemIndex.value_or(-1));
-}
-
-QAccessibleInterface *KItemListViewAccessible::parent() const
-{
-    return m_parent;
-}
-
-int KItemListViewAccessible::childCount() const
-{
-    return view()->model()->count();
-}
-
-int KItemListViewAccessible::indexOfChild(const QAccessibleInterface *interface) const
-{
-    const KItemListAccessibleCell *widget = static_cast<const KItemListAccessibleCell *>(interface);
-    return widget->index();
-}
-
-QString KItemListViewAccessible::text(QAccessible::Text) const
-{
-    return QString();
-}
-
-QRect KItemListViewAccessible::rect() const
-{
-    if (!view()->isVisible()) {
-        return QRect();
-    }
-
-    const QGraphicsScene *scene = view()->scene();
-    if (scene) {
-        const QPoint origin = scene->views().at(0)->mapToGlobal(QPoint(0, 0));
-        const QRect viewRect = view()->geometry().toRect();
-        return viewRect.translated(origin);
-    } else {
-        return QRect();
-    }
-}
-
-QAccessibleInterface *KItemListViewAccessible::child(int index) const
-{
-    if (index >= 0 && index < childCount()) {
-        return cell(index);
-    }
-    return nullptr;
-}
-
-KItemListViewAccessible::AccessibleIdWrapper::AccessibleIdWrapper()
-    : isValid(false)
-    , id(0)
-{
-}
-
-// Table Cell
-
-KItemListAccessibleCell::KItemListAccessibleCell(KItemListView *view, int index)
-    : m_view(view)
-    , m_index(index)
-{
-    Q_ASSERT(index >= 0 && index < view->model()->count());
-}
-
-void *KItemListAccessibleCell::interface_cast(QAccessible::InterfaceType type)
-{
-    if (type == QAccessible::TableCellInterface) {
-        return static_cast<QAccessibleTableCellInterface *>(this);
-    }
-    return nullptr;
-}
-
-int KItemListAccessibleCell::columnExtent() const
-{
-    return 1;
-}
-
-int KItemListAccessibleCell::rowExtent() const
-{
-    return 1;
-}
-
-QList<QAccessibleInterface *> KItemListAccessibleCell::rowHeaderCells() const
-{
-    return QList<QAccessibleInterface *>();
-}
-
-QList<QAccessibleInterface *> KItemListAccessibleCell::columnHeaderCells() const
-{
-    return QList<QAccessibleInterface *>();
-}
-
-int KItemListAccessibleCell::columnIndex() const
-{
-    return m_view->m_layouter->itemColumn(m_index);
-}
-
-int KItemListAccessibleCell::rowIndex() const
-{
-    return m_view->m_layouter->itemRow(m_index);
-}
-
-bool KItemListAccessibleCell::isSelected() const
-{
-    return m_view->controller()->selectionManager()->isSelected(m_index);
-}
-
-QAccessibleInterface *KItemListAccessibleCell::table() const
-{
-    return QAccessible::queryAccessibleInterface(m_view);
-}
-
-QAccessible::Role KItemListAccessibleCell::role() const
-{
-    return QAccessible::Cell;
-}
-
-QAccessible::State KItemListAccessibleCell::state() const
-{
-    QAccessible::State state;
-
-    state.selectable = true;
-    if (isSelected()) {
-        state.selected = true;
-    }
-
-    state.focusable = true;
-    if (m_view->controller()->selectionManager()->currentItem() == m_index) {
-        state.focused = true;
-    }
-
-    if (m_view->controller()->selectionBehavior() == KItemListController::MultiSelection) {
-        state.multiSelectable = true;
-    }
-
-    if (m_view->model()->isExpandable(m_index)) {
-        if (m_view->model()->isExpanded(m_index)) {
-            state.expanded = true;
-        } else {
-            state.collapsed = true;
-        }
-    }
-
-    return state;
-}
-
-bool KItemListAccessibleCell::isExpandable() const
-{
-    return m_view->model()->isExpandable(m_index);
-}
-
-QRect KItemListAccessibleCell::rect() const
-{
-    QRect rect = m_view->itemRect(m_index).toRect();
-
-    if (rect.isNull()) {
-        return QRect();
-    }
-
-    rect.translate(m_view->mapToScene(QPointF(0.0, 0.0)).toPoint());
-    rect.translate(m_view->scene()->views()[0]->mapToGlobal(QPoint(0, 0)));
-    return rect;
-}
-
-QString KItemListAccessibleCell::text(QAccessible::Text t) const
-{
-    switch (t) {
-    case QAccessible::Name: {
-        const QHash<QByteArray, QVariant> data = m_view->model()->data(m_index);
-        return data["text"].toString();
-    }
-
-    default:
-        break;
-    }
-
-    return QString();
-}
-
-void KItemListAccessibleCell::setText(QAccessible::Text, const QString &)
-{
-}
-
-QAccessibleInterface *KItemListAccessibleCell::child(int) const
-{
-    return nullptr;
-}
-
-bool KItemListAccessibleCell::isValid() const
-{
-    return m_view && (m_index >= 0) && (m_index < m_view->model()->count());
-}
-
-QAccessibleInterface *KItemListAccessibleCell::childAt(int, int) const
-{
-    return nullptr;
-}
-
-int KItemListAccessibleCell::childCount() const
-{
-    return 0;
-}
-
-int KItemListAccessibleCell::indexOfChild(const QAccessibleInterface *child) const
-{
-    Q_UNUSED(child)
-    return -1;
-}
-
-QAccessibleInterface *KItemListAccessibleCell::parent() const
-{
-    return QAccessible::queryAccessibleInterface(m_view);
-}
-
-int KItemListAccessibleCell::index() const
-{
-    return m_index;
-}
-
-QObject *KItemListAccessibleCell::object() const
-{
-    return nullptr;
-}
-
-// Container Interface
-KItemListContainerAccessible::KItemListContainerAccessible(KItemListContainer *container)
-    : QAccessibleWidget(container)
-{
-}
-
-KItemListContainerAccessible::~KItemListContainerAccessible()
-{
-}
-
-int KItemListContainerAccessible::childCount() const
-{
-    return 1;
-}
-
-int KItemListContainerAccessible::indexOfChild(const QAccessibleInterface *child) const
-{
-    if (child->object() == container()->controller()->view()) {
-        return 0;
-    }
-    return -1;
-}
-
-QAccessibleInterface *KItemListContainerAccessible::child(int index) const
-{
-    if (index == 0) {
-        return QAccessible::queryAccessibleInterface(container()->controller()->view());
-    }
-    return nullptr;
-}
-
-const KItemListContainer *KItemListContainerAccessible::container() const
-{
-    return qobject_cast<KItemListContainer *>(object());
-}
-
-#endif // QT_NO_ACCESSIBILITY
diff --git a/src/kitemviews/kitemlistviewaccessible.h b/src/kitemviews/kitemlistviewaccessible.h
deleted file mode 100644 (file)
index 41aacf3..0000000
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2012 Amandeep Singh <aman.dedman@gmail.com>
- *
- * SPDX-License-Identifier: GPL-2.0-or-later
- */
-
-#ifndef KITEMLISTVIEWACCESSIBLE_H
-#define KITEMLISTVIEWACCESSIBLE_H
-
-#ifndef QT_NO_ACCESSIBILITY
-
-#include "dolphin_export.h"
-
-#include <QAccessible>
-#include <QAccessibleObject>
-#include <QAccessibleWidget>
-#include <QPointer>
-
-class KItemListView;
-class KItemListContainer;
-class KItemListContainerAccessible;
-
-class DOLPHIN_EXPORT KItemListViewAccessible : public QAccessibleObject, public QAccessibleTableInterface
-{
-public:
-    explicit KItemListViewAccessible(KItemListView *view, KItemListContainerAccessible *parent);
-    ~KItemListViewAccessible() override;
-
-    void *interface_cast(QAccessible::InterfaceType type) override;
-
-    QAccessible::Role role() const override;
-    QAccessible::State state() const override;
-    QString text(QAccessible::Text t) const override;
-    QRect rect() const override;
-
-    QAccessibleInterface *child(int index) const override;
-    int childCount() const override;
-    int indexOfChild(const QAccessibleInterface *) const override;
-    QAccessibleInterface *childAt(int x, int y) const override;
-    QAccessibleInterface *parent() const override;
-
-    // Table interface
-    QAccessibleInterface *cellAt(int row, int column) const override;
-    QAccessibleInterface *caption() const override;
-    QAccessibleInterface *summary() const override;
-    QString columnDescription(int column) const override;
-    QString rowDescription(int row) const override;
-    int columnCount() const override;
-    int rowCount() const override;
-
-    // Selection
-    int selectedCellCount() const override;
-    int selectedColumnCount() const override;
-    int selectedRowCount() const override;
-    QList<QAccessibleInterface *> selectedCells() const override;
-    QList<int> selectedColumns() const override;
-    QList<int> selectedRows() const override;
-    bool isColumnSelected(int column) const override;
-    bool isRowSelected(int row) const override;
-    bool selectRow(int row) override;
-    bool selectColumn(int column) override;
-    bool unselectRow(int row) override;
-    bool unselectColumn(int column) override;
-    void modelChange(QAccessibleTableModelChangeEvent *) override;
-
-    KItemListView *view() const;
-
-protected:
-    virtual void modelReset();
-    /**
-     * Create an QAccessibleTableCellInterface representing the table
-     * cell at the @index. Index is 0-based.
-     */
-    inline QAccessibleInterface *cell(int index) const;
-
-private:
-    class AccessibleIdWrapper
-    {
-    public:
-        AccessibleIdWrapper();
-        bool isValid;
-        QAccessible::Id id;
-    };
-    mutable QVector<AccessibleIdWrapper> m_cells;
-
-    KItemListContainerAccessible *m_parent;
-};
-
-class DOLPHIN_EXPORT KItemListAccessibleCell : public QAccessibleInterface, public QAccessibleTableCellInterface
-{
-public:
-    KItemListAccessibleCell(KItemListView *view, int m_index);
-
-    void *interface_cast(QAccessible::InterfaceType type) override;
-    QObject *object() const override;
-    bool isValid() const override;
-    QAccessible::Role role() const override;
-    QAccessible::State state() const override;
-    QRect rect() const override;
-    QString text(QAccessible::Text t) const override;
-    void setText(QAccessible::Text t, const QString &text) override;
-
-    QAccessibleInterface *child(int index) const override;
-    int childCount() const override;
-    QAccessibleInterface *childAt(int x, int y) const override;
-    int indexOfChild(const QAccessibleInterface *) const override;
-
-    QAccessibleInterface *parent() const override;
-    bool isExpandable() const;
-
-    // Cell Interface
-    int columnExtent() const override;
-    QList<QAccessibleInterface *> columnHeaderCells() const override;
-    int columnIndex() const override;
-    int rowExtent() const override;
-    QList<QAccessibleInterface *> rowHeaderCells() const override;
-    int rowIndex() const override;
-    bool isSelected() const override;
-    QAccessibleInterface *table() const override;
-
-    inline int index() const;
-
-private:
-    QPointer<KItemListView> m_view;
-    int m_index;
-};
-
-class DOLPHIN_EXPORT KItemListContainerAccessible : public QAccessibleWidget
-{
-public:
-    explicit KItemListContainerAccessible(KItemListContainer *container);
-    ~KItemListContainerAccessible() override;
-
-    QAccessibleInterface *child(int index) const override;
-    int childCount() const override;
-    int indexOfChild(const QAccessibleInterface *child) const override;
-
-private:
-    const KItemListContainer *container() const;
-};
-
-#endif // QT_NO_ACCESSIBILITY
-
-#endif
index fe686d4fe4d6f0b6024c088f497c76b8aab21417..a8fee62449dc28e7c379e5db885b75fde8a5caea 100644 (file)
@@ -109,11 +109,20 @@ bool KStandardItemListWidgetInformant::itemIsLink(int index, const KItemListView
     return false;
 }
 
-QString KStandardItemListWidgetInformant::roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values) const
+QString KStandardItemListWidgetInformant::roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values, ForUsageAs forUsageAs) const
 {
     if (role == "rating") {
-        // Always use an empty text, as the rating is shown by the image m_rating.
-        return QString();
+        if (forUsageAs == ForUsageAs::DisplayedText) {
+            // Always use an empty text, as the rating is shown by the image m_rating.
+            return QString();
+        } else {
+            const int rating{values.value(role).toInt()};
+            // Check if there are half stars
+            if (rating % 2) {
+                return i18ncp("@accessible rating", "%1 and a half stars", "%1 and a half stars", rating / 2);
+            }
+            return i18ncp("@accessible rating", "%1 star", "%1 stars", rating / 2);
+        }
     }
     return values.value(role).toString();
 }
index d182755fa13e8b825aa9a542b70ec8b5e66b7d48..588ec3548e89aea48e70f3aeabda509962a2553b 100644 (file)
@@ -54,12 +54,19 @@ protected:
      */
     virtual bool itemIsLink(int index, const KItemListView *view) const;
 
+    /** Configure whether the requested text should be optimized for viewing on a screen or for being read out aloud by a text-to-speech engine. */
+    enum class ForUsageAs { DisplayedText, SpokenText };
+
     /**
+     * @param role          The role the text is being requested for.
+     * @param values        The data of the item. All the data is passed because the text might depend on multiple data points.
+     * @param forUsageAs    Whether the roleText should be optimized for displaying (i.e. kept somewhat short) or optimized for speaking e.g. by screen readers
+     *                      or text-to-speech in general (i.e. by prefering announcing a month as July instead of as the number 7).
      * @return String representation of the role \a role. The representation of
      *         a role might depend on other roles, so the values of all roles
      *         are passed as parameter.
      */
-    virtual QString roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values) const;
+    virtual QString roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values, ForUsageAs forUsageAs = ForUsageAs::DisplayedText) const;
 
     /**
     * @return A font based on baseFont which is customized for symlinks.
index c8369febcf8dd7341a9e04ec3a54bf86fc2be0de..99affde6fa2821d17d161ad042389a1c572327c0 100644 (file)
@@ -53,6 +53,7 @@ DolphinStatusBar::DolphinStatusBar(QWidget *parent)
     // Initialize text label
     m_label = new KSqueezedTextLabel(m_text, contentsContainer);
     m_label->setTextFormat(Qt::PlainText);
+    m_label->setTextInteractionFlags(Qt::TextBrowserInteraction | Qt::TextSelectableByKeyboard); // for accessibility but also to allow copy-pasting this text.
 
     // Initialize zoom slider's explanatory label
     m_zoomLabel = new KSqueezedTextLabel(i18nc("Used as a noun, i.e. 'Here is the zoom level:'", "Zoom:"), contentsContainer);
index 0c5ebb1df72a2a82e0372751d8183cf40ed119ec..6da285a87a9c12d5e24c5033531f102f7178451f 100644 (file)
@@ -12,6 +12,9 @@
 #include "dolphinitemlistview.h"
 #include "dolphinnewfilemenuobserver.h"
 #include "draganddrophelper.h"
+#ifndef QT_NO_ACCESSIBILITY
+#include "kitemviews/accessibility/kitemlistviewaccessible.h"
+#endif
 #include "kitemviews/kfileitemlistview.h"
 #include "kitemviews/kfileitemmodel.h"
 #include "kitemviews/kitemlistcontainer.h"
@@ -50,6 +53,9 @@
 #include <kwidgetsaddons_version.h>
 
 #include <QAbstractItemView>
+#ifndef QT_NO_ACCESSIBILITY
+#include <QAccessible>
+#endif
 #include <QActionGroup>
 #include <QApplication>
 #include <QClipboard>
@@ -2333,6 +2339,12 @@ void DolphinView::showLoadingPlaceholder()
 {
     m_placeholderLabel->setText(i18n("Loading…"));
     m_placeholderLabel->setVisible(true);
+#ifndef QT_NO_ACCESSIBILITY
+    if (QAccessible::isActive()) {
+        auto accessibleViewInterface = static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(m_view));
+        accessibleViewInterface->announceOverallViewState(m_placeholderLabel->text());
+    }
+#endif
 }
 
 void DolphinView::updatePlaceholderLabel()
@@ -2382,6 +2394,12 @@ void DolphinView::updatePlaceholderLabel()
     }
 
     m_placeholderLabel->setVisible(true);
+#ifndef QT_NO_ACCESSIBILITY
+    if (QAccessible::isActive()) {
+        auto accessibleViewInterface = static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(m_view));
+        accessibleViewInterface->announceOverallViewState(m_placeholderLabel->text());
+    }
+#endif
 }
 
 bool DolphinView::tryShowNameToolTip(QHelpEvent *event)