2 * SPDX-FileCopyrightText: 2012 Amandeep Singh <aman.dedman@gmail.com>
3 * SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
5 * SPDX-License-Identifier: GPL-2.0-or-later
8 #include "kitemlistviewaccessible.h"
9 #include "kitemlistcontaineraccessible.h"
10 #include "kitemlistdelegateaccessible.h"
12 #include "kitemviews/kitemlistcontainer.h"
13 #include "kitemviews/kitemlistcontroller.h"
14 #include "kitemviews/kitemlistselectionmanager.h"
15 #include "kitemviews/kitemlistview.h"
16 #include "kitemviews/kitemmodelbase.h"
17 #include "kitemviews/kstandarditemlistview.h"
18 #include "kitemviews/private/kitemlistviewlayouter.h"
20 #include <KLocalizedString>
22 #include <QApplication> // for figuring out if we should move focus to this view.
23 #include <QGraphicsScene>
24 #include <QGraphicsView>
26 KItemListSelectionManager
*KItemListViewAccessible::selectionManager() const
28 return view()->controller()->selectionManager();
31 KItemListViewAccessible::KItemListViewAccessible(KItemListView
*view_
, KItemListContainerAccessible
*parent
)
32 : QAccessibleObject(view_
)
37 m_accessibleDelegates
.resize(childCount());
39 m_announceDescriptionChangeTimer
= new QTimer
{view_
};
40 m_announceDescriptionChangeTimer
->setSingleShot(true);
41 m_announceDescriptionChangeTimer
->setInterval(100);
42 KItemListGroupHeader::connect(m_announceDescriptionChangeTimer
, &QTimer::timeout
, view_
, [this]() {
43 // 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
44 // here in case the view itself has focus e.g. after tabbing there or after opening a new location.
45 QAccessibleEvent
announceAccessibleDescriptionEvent(this, QAccessible::DescriptionChanged
);
46 QAccessible::updateAccessibility(&announceAccessibleDescriptionEvent
);
50 KItemListViewAccessible::~KItemListViewAccessible()
52 for (AccessibleIdWrapper idWrapper
: std::as_const(m_accessibleDelegates
)) {
53 if (idWrapper
.isValid
) {
54 QAccessible::deleteAccessibleInterface(idWrapper
.id
);
59 void *KItemListViewAccessible::interface_cast(QAccessible::InterfaceType type
)
62 case QAccessible::SelectionInterface
:
63 return static_cast<QAccessibleSelectionInterface
*>(this);
64 case QAccessible::TableInterface
:
65 return static_cast<QAccessibleTableInterface
*>(this);
66 case QAccessible::ActionInterface
:
67 return static_cast<QAccessibleActionInterface
*>(this);
73 void KItemListViewAccessible::modelReset()
77 QAccessibleInterface
*KItemListViewAccessible::accessibleDelegate(int index
) const
79 if (index
< 0 || index
>= view()->model()->count()) {
83 if (m_accessibleDelegates
.size() <= index
) {
84 m_accessibleDelegates
.resize(childCount());
86 Q_ASSERT(index
< m_accessibleDelegates
.size());
88 AccessibleIdWrapper idWrapper
= m_accessibleDelegates
.at(index
);
89 if (!idWrapper
.isValid
) {
90 idWrapper
.id
= QAccessible::registerAccessibleInterface(new KItemListDelegateAccessible(view(), index
));
91 idWrapper
.isValid
= true;
92 m_accessibleDelegates
.insert(index
, idWrapper
);
94 return QAccessible::accessibleInterface(idWrapper
.id
);
97 QAccessibleInterface
*KItemListViewAccessible::cellAt(int row
, int column
) const
99 return accessibleDelegate(columnCount() * row
+ column
);
102 QAccessibleInterface
*KItemListViewAccessible::caption() const
107 QString
KItemListViewAccessible::columnDescription(int) const
112 int KItemListViewAccessible::columnCount() const
114 return view()->m_layouter
->columnCount();
117 int KItemListViewAccessible::rowCount() const
119 if (columnCount() <= 0) {
123 int itemCount
= view()->model()->count();
124 int rowCount
= itemCount
/ columnCount();
130 if (itemCount
% columnCount()) {
136 int KItemListViewAccessible::selectedCellCount() const
138 return selectionManager()->selectedItems().count();
141 int KItemListViewAccessible::selectedColumnCount() const
146 int KItemListViewAccessible::selectedRowCount() const
151 QString
KItemListViewAccessible::rowDescription(int) const
156 QList
<QAccessibleInterface
*> KItemListViewAccessible::selectedCells() const
158 QList
<QAccessibleInterface
*> cells
;
159 const auto items
= selectionManager()->selectedItems();
160 cells
.reserve(items
.count());
161 for (int index
: items
) {
162 cells
.append(accessibleDelegate(index
));
167 QList
<int> KItemListViewAccessible::selectedColumns() const
172 QList
<int> KItemListViewAccessible::selectedRows() const
177 QAccessibleInterface
*KItemListViewAccessible::summary() const
182 bool KItemListViewAccessible::isColumnSelected(int) const
187 bool KItemListViewAccessible::isRowSelected(int) const
192 bool KItemListViewAccessible::selectRow(int)
197 bool KItemListViewAccessible::selectColumn(int)
202 bool KItemListViewAccessible::unselectRow(int)
207 bool KItemListViewAccessible::unselectColumn(int)
212 void KItemListViewAccessible::modelChange(QAccessibleTableModelChangeEvent
* /*event*/)
216 QAccessible::Role
KItemListViewAccessible::role() const
218 return QAccessible::List
;
221 QAccessible::State
KItemListViewAccessible::state() const
223 QAccessible::State s
;
226 const KItemListController
*controller
= view()->m_controller
;
227 s
.multiSelectable
= controller
->selectionBehavior() == KItemListController::MultiSelection
;
228 s
.focused
= !childCount() && (view()->hasFocus() || m_parent
->container()->hasFocus()); // Usually the children have focus.
232 QAccessibleInterface
*KItemListViewAccessible::childAt(int x
, int y
) const
234 const QPointF point
= QPointF(x
, y
);
235 const std::optional
<int> itemIndex
= view()->itemAt(view()->mapFromScene(point
));
236 return child(itemIndex
.value_or(-1));
239 QAccessibleInterface
*KItemListViewAccessible::parent() const
244 int KItemListViewAccessible::childCount() const
246 return view()->model()->count();
249 int KItemListViewAccessible::indexOfChild(const QAccessibleInterface
*interface
) const
251 const KItemListDelegateAccessible
*widget
= static_cast<const KItemListDelegateAccessible
*>(interface
);
252 return widget
->index();
255 QString
KItemListViewAccessible::text(QAccessible::Text t
) const
257 const KItemListController
*controller
= view()->m_controller
;
258 const KItemModelBase
*model
= controller
->model();
259 const QUrl modelRootUrl
= model
->directory();
260 if (t
== QAccessible::Name
) {
261 return modelRootUrl
.fileName();
263 if (t
!= QAccessible::Description
) {
266 const auto currentItem
= child(controller
->selectionManager()->currentItem());
268 return i18nc("@info 1 states that the folder is empty and sometimes why, 2 is the full filesystem path",
270 m_placeholderMessage
,
271 modelRootUrl
.toDisplayString());
274 const QString selectionStateString
{isSelected(currentItem
) ? QString()
275 // i18n: There is a comma at the end because this is one property in an enumeration of
276 // properties that a file or folder has. Accessible text for accessibility software like screen
278 : i18n("not selected,")};
280 QString expandableStateString
;
281 if (currentItem
->state().expandable
) {
282 if (currentItem
->state().collapsed
) {
283 // 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.
284 // Accessible text for accessibility software like screen readers.
285 expandableStateString
= i18n("collapsed,");
287 // 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.
288 // Accessible text for accessibility software like screen readers.
289 expandableStateString
= i18n("expanded,");
293 const QString selectedItemCountString
{selectedItemCount() > 1
294 // i18n: There is a "—" at the beginning because this is a followup sentence to a text that did not properly end
295 // with a period. Accessible text for accessibility software like screen readers.
296 ? i18np("— %1 selected item", "— %1 selected items", selectedItemCount())
299 // 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
300 // 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.
301 // Therefore we need to announce this layout when in icon view mode.
302 QString layoutAnnouncementString
;
303 if (auto standardView
= qobject_cast
<const KStandardItemListView
*>(view())) {
304 if (standardView
->itemLayout() == KStandardItemListView::ItemLayout::IconsLayout
) {
305 layoutAnnouncementString
= i18nc("@info refering to a file or folder", "in a grid layout");
310 * Announce it in this order so the most important information is at the beginning and the potentially very long path at the end:
311 * "$currentlyFocussedItemName, $currentlyFocussedItemDescription, $currentFolderPath".
312 * We do not need to announce the total count of items here because accessibility software like Orca alrady announces this automatically for lists.
313 * Normally for list items the selection and expandadable state are also automatically announced by Orca, however we are building the accessible
314 * description of the view here, so we need to manually add all infomation about the current item we also want to announce.
317 "@info 1 is currentlyFocussedItemName, 2 is empty or \"not selected, \", 3 is currentlyFocussedItemDescription, 3 is currentFolderName, 4 is "
319 "%1, %2 %3 %4 %5 %6 in location %7",
320 currentItem
->text(QAccessible::Name
),
321 selectionStateString
,
322 expandableStateString
,
323 currentItem
->text(QAccessible::Description
),
324 selectedItemCountString
,
325 layoutAnnouncementString
,
326 modelRootUrl
.toDisplayString());
329 QRect
KItemListViewAccessible::rect() const
331 if (!view()->isVisible()) {
335 const QGraphicsScene
*scene
= view()->scene();
337 const QPoint origin
= scene
->views().at(0)->mapToGlobal(QPoint(0, 0));
338 const QRect viewRect
= view()->geometry().toRect();
339 return viewRect
.translated(origin
);
345 QAccessibleInterface
*KItemListViewAccessible::child(int index
) const
347 if (index
>= 0 && index
< childCount()) {
348 return accessibleDelegate(index
);
353 KItemListViewAccessible::AccessibleIdWrapper::AccessibleIdWrapper()
359 /* Selection interface */
361 bool KItemListViewAccessible::clear()
363 selectionManager()->clearSelection();
367 bool KItemListViewAccessible::isSelected(QAccessibleInterface
*childItem
) const
369 Q_CHECK_PTR(childItem
);
370 return static_cast<KItemListDelegateAccessible
*>(childItem
)->isSelected();
373 bool KItemListViewAccessible::select(QAccessibleInterface
*childItem
)
375 selectionManager()->setSelected(indexOfChild(childItem
));
379 bool KItemListViewAccessible::selectAll()
381 selectionManager()->setSelected(0, childCount());
385 QAccessibleInterface
*KItemListViewAccessible::selectedItem(int selectionIndex
) const
387 const auto selectedItems
= selectionManager()->selectedItems();
389 for (auto it
= selectedItems
.rbegin(); it
!= selectedItems
.rend(); ++it
) {
390 if (i
== selectionIndex
) {
397 int KItemListViewAccessible::selectedItemCount() const
399 return selectionManager()->selectedItems().count();
402 QList
<QAccessibleInterface
*> KItemListViewAccessible::selectedItems() const
404 const auto selectedItems
= selectionManager()->selectedItems();
405 QList
<QAccessibleInterface
*> selectedItemsInterfaces
;
406 for (auto it
= selectedItems
.rbegin(); it
!= selectedItems
.rend(); ++it
) {
407 selectedItemsInterfaces
.append(child(*it
));
409 return selectedItemsInterfaces
;
412 bool KItemListViewAccessible::unselect(QAccessibleInterface
*childItem
)
414 selectionManager()->setSelected(indexOfChild(childItem
), 1, KItemListSelectionManager::Deselect
);
418 /* Action Interface */
420 QStringList
KItemListViewAccessible::actionNames() const
422 return {setFocusAction()};
425 void KItemListViewAccessible::doAction(const QString
&actionName
)
427 if (actionName
== setFocusAction()) {
432 QStringList
KItemListViewAccessible::keyBindingsForAction(const QString
&actionName
) const
438 /* Custom non-interface methods */
440 KItemListView
*KItemListViewAccessible::view() const
442 Q_CHECK_PTR(qobject_cast
<KItemListView
*>(object()));
443 return static_cast<KItemListView
*>(object());
446 void KItemListViewAccessible::announceOverallViewState(const QString
&placeholderMessage
)
448 m_placeholderMessage
= placeholderMessage
;
450 // Make sure we announce this placeholderMessage. However, do not announce it when the focus is on an unrelated object currently.
451 // 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.
452 if (view()->hasFocus() || !QApplication::focusWidget() || static_cast<QWidget
*>(m_parent
->object())->isAncestorOf(QApplication::focusWidget())) {
454 // If we move focus to an item and right after that the description of the item is changed, the item will be announced twice.
455 // We want to avoid that so we wait until after the description change was announced to move focus.
456 KItemListGroupHeader::connect(
457 m_announceDescriptionChangeTimer
,
461 if (view()->hasFocus() || !QApplication::focusWidget()
462 || static_cast<QWidget
*>(m_parent
->object())->isAncestorOf(QApplication::focusWidget())) {
463 QAccessibleEvent
accessibleFocusEvent(this, QAccessible::Focus
);
464 QAccessible::updateAccessibility(&accessibleFocusEvent
); // This accessibility update is perhaps even too important: It is generally
465 // the last triggered update after changing the currently viewed folder. This call makes sure that we announce the new directory in
466 // full. Furthermore it also serves its original purpose of making sure we announce the placeholderMessage in empty folders.
469 Qt::SingleShotConnection
);
470 if (!m_announceDescriptionChangeTimer
->isActive()) {
471 m_announceDescriptionChangeTimer
->start();
476 void KItemListViewAccessible::announceDescriptionChange()
478 m_announceDescriptionChangeTimer
->start();