diff options
22 files changed, 1203 insertions, 634 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7d1206e48..ef50cf77d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/animatedheightwidget.cpp b/src/animatedheightwidget.cpp index cee1a4922..f1631bb6f 100644 --- a/src/animatedheightwidget.cpp +++ b/src/animatedheightwidget.cpp @@ -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; } diff --git a/src/dolphintabwidget.cpp b/src/dolphintabwidget.cpp index f80b94ea7..825ff3c7f 100644 --- a/src/dolphintabwidget.cpp +++ b/src/dolphintabwidget.cpp @@ -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 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 diff --git a/src/kitemviews/kfileitemlistwidget.cpp b/src/kitemviews/kfileitemlistwidget.cpp index b4e0895f2..3a7b37895 100644 --- a/src/kitemviews/kfileitemlistwidget.cpp +++ b/src/kitemviews/kfileitemlistwidget.cpp @@ -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; diff --git a/src/kitemviews/kfileitemlistwidget.h b/src/kitemviews/kfileitemlistwidget.h index 5ce11b6da..e2db43178 100644 --- a/src/kitemviews/kfileitemlistwidget.h +++ b/src/kitemviews/kfileitemlistwidget.h @@ -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; }; /** diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index 3a60834af..fb5851c36 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -20,6 +20,9 @@ #include <KLocalizedString> #include <KUrlMimeData> +#ifndef QT_NO_ACCESSIBILITY +#include <QAccessible> +#endif #include <QElapsedTimer> #include <QIcon> #include <QMimeData> diff --git a/src/kitemviews/kitemlistcontainer.cpp b/src/kitemviews/kitemlistcontainer.cpp index 2f9f5d401..65250832b 100644 --- a/src/kitemviews/kitemlistcontainer.cpp +++ b/src/kitemviews/kitemlistcontainer.cpp @@ -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 + }); } } diff --git a/src/kitemviews/kitemlistcontroller.cpp b/src/kitemviews/kitemlistcontroller.cpp index 821e1b75f..1db665f47 100644 --- a/src/kitemviews/kitemlistcontroller.cpp +++ b/src/kitemviews/kitemlistcontroller.cpp @@ -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(); diff --git a/src/kitemviews/kitemlistview.cpp b/src/kitemviews/kitemlistview.cpp index afc392810..b0ea32940 100644 --- a/src/kitemviews/kitemlistview.cpp +++ b/src/kitemviews/kitemlistview.cpp @@ -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 ¤t, 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 ¤t, 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) diff --git a/src/kitemviews/kitemlistview.h b/src/kitemviews/kitemlistview.h index 8812eb8cc..7be08302c 100644 --- a/src/kitemviews/kitemlistview.h +++ b/src/kitemviews/kitemlistview.h @@ -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 index a8d80ab52..000000000 --- a/src/kitemviews/kitemlistviewaccessible.cpp +++ /dev/null @@ -1,467 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Amandeep Singh <[email protected]> - * - * 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 index 41aacf367..000000000 --- a/src/kitemviews/kitemlistviewaccessible.h +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Amandeep Singh <[email protected]> - * - * 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 diff --git a/src/kitemviews/kstandarditemlistwidget.cpp b/src/kitemviews/kstandarditemlistwidget.cpp index fe686d4fe..a8fee6244 100644 --- a/src/kitemviews/kstandarditemlistwidget.cpp +++ b/src/kitemviews/kstandarditemlistwidget.cpp @@ -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(); } diff --git a/src/kitemviews/kstandarditemlistwidget.h b/src/kitemviews/kstandarditemlistwidget.h index d182755fa..588ec3548 100644 --- a/src/kitemviews/kstandarditemlistwidget.h +++ b/src/kitemviews/kstandarditemlistwidget.h @@ -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. diff --git a/src/statusbar/dolphinstatusbar.cpp b/src/statusbar/dolphinstatusbar.cpp index c8369febc..99affde6f 100644 --- a/src/statusbar/dolphinstatusbar.cpp +++ b/src/statusbar/dolphinstatusbar.cpp @@ -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); diff --git a/src/views/dolphinview.cpp b/src/views/dolphinview.cpp index 0c5ebb1df..6da285a87 100644 --- a/src/views/dolphinview.cpp +++ b/src/views/dolphinview.cpp @@ -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) |
