┌   ┐
54
└   ┘

summaryrefslogtreecommitdiff
path: root/src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp
diff options
context:
space:
mode:
authorFelix Ernst <[email protected]>2024-10-28 13:25:10 +0000
committerFelix Ernst <[email protected]>2024-10-28 13:25:10 +0000
commitf208acd5f68c8516b9f6a920cc229803637e23e9 (patch)
treef748d6386ed579c494497998097bdc92a432e704 /src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp
parent4d5cab6a5fcaa8edeb18cbacd2061cc098054882 (diff)
Overhaul main view accessibility
This commit brings the main view of Dolphin into a usable state accessibility-wise. Users of screen readers should have a way better experience while browsing files and folders and navigating along the file system hierarchy. This commit fixes most of the remaining already-identified accessibility issues listed in https://invent.kde.org/teams/accessibility/collaboration/-/issues/28, but not all. Namely, these should now be fixed: 1. Orca should read the element type in dolphin (file, folder, device, link to folder, link to file) 2. Orca should read complete label in icon and compact view mode, currently it only speaks the name, but there could be additional information like the number of elements or the file size. 3. Orca is not able to announce Selecting / Unselecting files in Dolphin. It also never announces how many items are selected in total. (Announcing the total selection can be done by reading out the view element or by pressing the Tab key to get to the status bar with the relevant information.) 4. Dolphin opens on the home directory, but Orca doesn't tell you so. Consider enclosing the area in a frame/panel which updates its accessible name each time you modify the current path by entering or leaving a directory. 5. I don't know what the folder presentation widget is, but it should be presented as a grid view. Currently, we have a terrible experience because the entire row of folders is read at once, with no indication that we can move left and right with the arrows to go between the elements of a row. When I found that out, however, I discovered that when you're on the last icon of the first row and press right arrow, you get to the first icon of the next row, but that's not announced, instead, the whole row is announced at once 6. Orca should announce the current elements instead of "layered pane" when the Folder / File view gets the focus in dolphin 7. Orca reads only name in Table View only of Dolphin 8. Items are sometimes confusingly announced as "collapsed" in contexts in which there is no concept of collapsing/expanding e.g. in icon view mode. A lot of code was moved around and renamed. The three accessibility classes, which all used to be in the same file, are moved into separate files. *Acknowledgement* Thanks to Christian Hempfling and bgt lover for testing as well as originally identifying a lot of the pain points being addressed here. This work is part of a my project funded through the NGI0 Entrust Fund, a fund established by NLnet with financial support from the European Commission's Next Generation Internet programme, under the aegis of DG Communications Networks, Content and Technology. https://kde.org/announcements/2024_ngi_openletter/
Diffstat (limited to 'src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp')
-rw-r--r--src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp240
1 files changed, 240 insertions, 0 deletions
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;
+}