┌   ┐
54
└   ┘

summaryrefslogtreecommitdiff
path: root/src/kitemviews/accessibility
diff options
context:
space:
mode:
Diffstat (limited to 'src/kitemviews/accessibility')
-rw-r--r--src/kitemviews/accessibility/kitemlistcontaineraccessible.cpp85
-rw-r--r--src/kitemviews/accessibility/kitemlistcontaineraccessible.h45
-rw-r--r--src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp240
-rw-r--r--src/kitemviews/accessibility/kitemlistdelegateaccessible.h61
-rw-r--r--src/kitemviews/accessibility/kitemlistviewaccessible.cpp479
-rw-r--r--src/kitemviews/accessibility/kitemlistviewaccessible.h146
6 files changed, 1056 insertions, 0 deletions
diff --git a/src/kitemviews/accessibility/kitemlistcontaineraccessible.cpp b/src/kitemviews/accessibility/kitemlistcontaineraccessible.cpp
new file mode 100644
index 000000000..6abf45025
--- /dev/null
+++ b/src/kitemviews/accessibility/kitemlistcontaineraccessible.cpp
@@ -0,0 +1,85 @@
+/*
+ * SPDX-FileCopyrightText: 2012 Amandeep Singh <[email protected]>
+ *
+ * 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
index 000000000..5a7147a36
--- /dev/null
+++ b/src/kitemviews/accessibility/kitemlistcontaineraccessible.h
@@ -0,0 +1,45 @@
+/*
+ * SPDX-FileCopyrightText: 2012 Amandeep Singh <[email protected]>
+ *
+ * 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
index 000000000..dcfe3af80
--- /dev/null
+++ b/src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp
@@ -0,0 +1,240 @@
+/*
+ * SPDX-FileCopyrightText: 2012 Amandeep Singh <[email protected]>
+ * SPDX-FileCopyrightText: 2024 Felix Ernst <[email protected]>
+ *
+ * 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
index 000000000..f9f6d5738
--- /dev/null
+++ b/src/kitemviews/accessibility/kitemlistdelegateaccessible.h
@@ -0,0 +1,61 @@
+/*
+ * SPDX-FileCopyrightText: 2012 Amandeep Singh <[email protected]>
+ * SPDX-FileCopyrightText: 2024 Felix Ernst <[email protected]>
+ *
+ * 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
index 000000000..2643eb302
--- /dev/null
+++ b/src/kitemviews/accessibility/kitemlistviewaccessible.cpp
@@ -0,0 +1,479 @@
+/*
+ * SPDX-FileCopyrightText: 2012 Amandeep Singh <[email protected]>
+ * SPDX-FileCopyrightText: 2024 Felix Ernst <[email protected]>
+ *
+ * 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
index 000000000..4c44b18ad
--- /dev/null
+++ b/src/kitemviews/accessibility/kitemlistviewaccessible.h
@@ -0,0 +1,146 @@
+/*
+ * SPDX-FileCopyrightText: 2012 Amandeep Singh <[email protected]>
+ * SPDX-FileCopyrightText: 2024 Felix Ernst <[email protected]>
+ *
+ * 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