diff options
Diffstat (limited to 'src/kitemviews/accessibility')
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 |
