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_announceCurrentItemTimer
= new QTimer
{view_
};
40 m_announceCurrentItemTimer
->setSingleShot(true);
41 m_announceCurrentItemTimer
->setInterval(100);
42 KItemListGroupHeader::connect(m_announceCurrentItemTimer
, &QTimer::timeout
, view_
, [this]() {
43 slotAnnounceCurrentItemTimerTimeout();
47 KItemListViewAccessible::~KItemListViewAccessible()
49 for (AccessibleIdWrapper idWrapper
: std::as_const(m_accessibleDelegates
)) {
50 if (idWrapper
.isValid
) {
51 QAccessible::deleteAccessibleInterface(idWrapper
.id
);
56 void *KItemListViewAccessible::interface_cast(QAccessible::InterfaceType type
)
59 case QAccessible::SelectionInterface
:
60 return static_cast<QAccessibleSelectionInterface
*>(this);
61 case QAccessible::TableInterface
:
62 return static_cast<QAccessibleTableInterface
*>(this);
63 case QAccessible::ActionInterface
:
64 return static_cast<QAccessibleActionInterface
*>(this);
70 QAccessibleInterface
*KItemListViewAccessible::accessibleDelegate(int index
) const
72 if (index
< 0 || index
>= view()->model()->count()) {
76 if (m_accessibleDelegates
.size() <= index
) {
77 m_accessibleDelegates
.resize(childCount());
79 Q_ASSERT(index
< m_accessibleDelegates
.size());
81 AccessibleIdWrapper idWrapper
= m_accessibleDelegates
.at(index
);
82 if (!idWrapper
.isValid
) {
83 idWrapper
.id
= QAccessible::registerAccessibleInterface(new KItemListDelegateAccessible(view(), index
));
84 idWrapper
.isValid
= true;
85 m_accessibleDelegates
.insert(index
, idWrapper
);
87 return QAccessible::accessibleInterface(idWrapper
.id
);
90 QAccessibleInterface
*KItemListViewAccessible::cellAt(int row
, int column
) const
92 return accessibleDelegate(columnCount() * row
+ column
);
95 QAccessibleInterface
*KItemListViewAccessible::caption() const
100 QString
KItemListViewAccessible::columnDescription(int) const
105 int KItemListViewAccessible::columnCount() const
107 return view()->m_layouter
->columnCount();
110 int KItemListViewAccessible::rowCount() const
112 if (columnCount() <= 0) {
116 int itemCount
= view()->model()->count();
117 int rowCount
= itemCount
/ columnCount();
123 if (itemCount
% columnCount()) {
129 int KItemListViewAccessible::selectedCellCount() const
131 return selectionManager()->selectedItems().count();
134 int KItemListViewAccessible::selectedColumnCount() const
139 int KItemListViewAccessible::selectedRowCount() const
144 QString
KItemListViewAccessible::rowDescription(int) const
149 QList
<QAccessibleInterface
*> KItemListViewAccessible::selectedCells() const
151 QList
<QAccessibleInterface
*> cells
;
152 const auto items
= selectionManager()->selectedItems();
153 cells
.reserve(items
.count());
154 for (int index
: items
) {
155 cells
.append(accessibleDelegate(index
));
160 QList
<int> KItemListViewAccessible::selectedColumns() const
165 QList
<int> KItemListViewAccessible::selectedRows() const
170 QAccessibleInterface
*KItemListViewAccessible::summary() const
175 bool KItemListViewAccessible::isColumnSelected(int) const
180 bool KItemListViewAccessible::isRowSelected(int) const
185 bool KItemListViewAccessible::selectRow(int)
190 bool KItemListViewAccessible::selectColumn(int)
195 bool KItemListViewAccessible::unselectRow(int)
200 bool KItemListViewAccessible::unselectColumn(int)
205 void KItemListViewAccessible::modelChange(QAccessibleTableModelChangeEvent
* /*event*/)
209 QAccessible::Role
KItemListViewAccessible::role() const
211 return QAccessible::List
;
214 QAccessible::State
KItemListViewAccessible::state() const
216 QAccessible::State s
;
219 const KItemListController
*controller
= view()->m_controller
;
220 s
.multiSelectable
= controller
->selectionBehavior() == KItemListController::MultiSelection
;
221 s
.focused
= !childCount() && (view()->hasFocus() || m_parent
->container()->hasFocus()); // Usually the children have focus.
225 QAccessibleInterface
*KItemListViewAccessible::childAt(int x
, int y
) const
227 const QPointF point
= QPointF(x
, y
);
228 const std::optional
<int> itemIndex
= view()->itemAt(view()->mapFromScene(point
));
229 return child(itemIndex
.value_or(-1));
232 QAccessibleInterface
*KItemListViewAccessible::parent() const
237 int KItemListViewAccessible::childCount() const
239 return view()->model()->count();
242 int KItemListViewAccessible::indexOfChild(const QAccessibleInterface
*interface
) const
244 const KItemListDelegateAccessible
*widget
= static_cast<const KItemListDelegateAccessible
*>(interface
);
245 return widget
->index();
248 QString
KItemListViewAccessible::text(QAccessible::Text t
) const
250 const KItemListController
*controller
= view()->m_controller
;
251 const KItemModelBase
*model
= controller
->model();
252 const QUrl modelRootUrl
= model
->directory();
253 if (t
== QAccessible::Name
) {
254 return modelRootUrl
.fileName();
256 if (t
!= QAccessible::Description
) {
260 QAccessibleInterface
*currentItem
= child(controller
->selectionManager()->currentItem());
263 * Always announce the path last because it might be very long.
264 * We do not need to announce the total count of items here because accessibility software like Orca alrady announces this automatically for lists.
267 return i18nc("@info 1 states that the folder is empty and sometimes why, 2 is the full filesystem path",
269 m_placeholderMessage
,
270 modelRootUrl
.toDisplayString());
273 const int numberOfSelectedItems
= selectedItemCount();
275 // 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
276 // 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.
277 // Therefore we need to announce this layout when in icon view mode.
278 if (auto standardView
= qobject_cast
<const KStandardItemListView
*>(view())) {
279 if (standardView
->itemLayout() == KStandardItemListView::ItemLayout::IconsLayout
) {
280 if (numberOfSelectedItems
< 1 || (numberOfSelectedItems
== 1 && isSelected(currentItem
))) {
281 // We do not announce the number of selected items if the only selected item is the current item
282 // because the selection state of the current item is already announced elsewhere.
283 return i18nc("@info accessibility, 1 is path", "in a grid layout in location %1", modelRootUrl
.toDisplayString());
285 return i18ncp("@info accessibility, 2 is path",
286 "%1 selected item in a grid layout in location %2",
287 "%1 selected items in a grid layout in location %2",
288 numberOfSelectedItems
,
289 modelRootUrl
.toDisplayString());
293 if (numberOfSelectedItems
< 1 || (numberOfSelectedItems
== 1 && isSelected(currentItem
))) {
294 // We do not announce the number of selected items if the only selected item is the current item
295 // because the selection state of the current item is already announced elsewhere.
296 return i18nc("@info accessibility, 1 is path", "in location %1", modelRootUrl
.toDisplayString());
298 return i18ncp("@info accessibility, 2 is path",
299 "%1 selected item in location %2",
300 "%1 selected items in location %2",
301 numberOfSelectedItems
,
302 modelRootUrl
.toDisplayString());
305 QRect
KItemListViewAccessible::rect() const
307 if (!view()->isVisible()) {
311 const QGraphicsScene
*scene
= view()->scene();
313 const QPoint origin
= scene
->views().at(0)->mapToGlobal(QPoint(0, 0));
314 const QRect viewRect
= view()->geometry().toRect();
315 return viewRect
.translated(origin
);
321 QAccessibleInterface
*KItemListViewAccessible::child(int index
) const
323 if (index
>= 0 && index
< childCount()) {
324 return accessibleDelegate(index
);
329 KItemListViewAccessible::AccessibleIdWrapper::AccessibleIdWrapper()
335 /* Selection interface */
337 bool KItemListViewAccessible::clear()
339 selectionManager()->clearSelection();
343 bool KItemListViewAccessible::isSelected(QAccessibleInterface
*childItem
) const
345 Q_CHECK_PTR(childItem
);
346 return static_cast<KItemListDelegateAccessible
*>(childItem
)->isSelected();
349 bool KItemListViewAccessible::select(QAccessibleInterface
*childItem
)
351 selectionManager()->setSelected(indexOfChild(childItem
));
355 bool KItemListViewAccessible::selectAll()
357 selectionManager()->setSelected(0, childCount());
361 QAccessibleInterface
*KItemListViewAccessible::selectedItem(int selectionIndex
) const
363 const auto selectedItems
= selectionManager()->selectedItems();
365 for (auto it
= selectedItems
.rbegin(); it
!= selectedItems
.rend(); ++it
) {
366 if (i
== selectionIndex
) {
373 int KItemListViewAccessible::selectedItemCount() const
375 return selectionManager()->selectedItems().count();
378 QList
<QAccessibleInterface
*> KItemListViewAccessible::selectedItems() const
380 const auto selectedItems
= selectionManager()->selectedItems();
381 QList
<QAccessibleInterface
*> selectedItemsInterfaces
;
382 for (auto it
= selectedItems
.rbegin(); it
!= selectedItems
.rend(); ++it
) {
383 selectedItemsInterfaces
.append(child(*it
));
385 return selectedItemsInterfaces
;
388 bool KItemListViewAccessible::unselect(QAccessibleInterface
*childItem
)
390 selectionManager()->setSelected(indexOfChild(childItem
), 1, KItemListSelectionManager::Deselect
);
394 /* Action Interface */
396 QStringList
KItemListViewAccessible::actionNames() const
398 return {setFocusAction()};
401 void KItemListViewAccessible::doAction(const QString
&actionName
)
403 if (actionName
== setFocusAction()) {
408 QStringList
KItemListViewAccessible::keyBindingsForAction(const QString
&actionName
) const
414 /* Custom non-interface methods */
416 KItemListView
*KItemListViewAccessible::view() const
418 Q_CHECK_PTR(qobject_cast
<KItemListView
*>(object()));
419 return static_cast<KItemListView
*>(object());
422 void KItemListViewAccessible::setAccessibleFocusAndAnnounceAll()
424 const int currentItemIndex
= view()->m_controller
->selectionManager()->currentItem();
425 if (currentItemIndex
< 0) {
426 // The current item is invalid (perhaps because the folder is empty), so we set the focus to the view itself instead.
427 QAccessibleEvent
accessibleFocusInEvent(this, QAccessible::Focus
);
428 QAccessible::updateAccessibility(&accessibleFocusInEvent
);
432 QAccessibleEvent
accessibleFocusInEvent(this, QAccessible::Focus
);
433 accessibleFocusInEvent
.setChild(currentItemIndex
);
434 QAccessible::updateAccessibility(&accessibleFocusInEvent
);
435 m_shouldAnnounceLocation
= true;
436 announceCurrentItem();
439 void KItemListViewAccessible::announceNewlyLoadedLocation(const QString
&placeholderMessage
)
441 m_placeholderMessage
= placeholderMessage
;
442 m_shouldAnnounceLocation
= true;
444 // Changes might still be happening in the view. We (re)start the timer to make it less likely that it announces a state that is still in flux.
445 m_announceCurrentItemTimer
->start();
448 void KItemListViewAccessible::announceCurrentItem()
450 m_announceCurrentItemTimer
->start();
453 void KItemListViewAccessible::slotAnnounceCurrentItemTimerTimeout()
455 if (!view()->hasFocus() && QApplication::focusWidget() && QApplication::focusWidget()->isVisible()
456 && !static_cast<QWidget
*>(m_parent
->object())->isAncestorOf(QApplication::focusWidget())) {
457 // Something else than this view has focus, so we do not announce anything.
458 m_lastAnnouncedIndex
= -1; // Reset this to -1 so we properly move focus to the current item the next time this method is called.
462 /// Announce the current item (or the view if there is no current item).
463 const int currentIndex
= view()->m_controller
->selectionManager()->currentItem();
464 if (currentIndex
< 0) {
465 // The current index is invalid! There might be no items in the list. Instead the list itself is announced.
466 m_shouldAnnounceLocation
= true;
467 QAccessibleEvent
announceEmptyViewPlaceholderMessageEvent(this, QAccessible::Focus
);
468 QAccessible::updateAccessibility(&announceEmptyViewPlaceholderMessageEvent
);
469 } else if (currentIndex
!= m_lastAnnouncedIndex
) {
470 QAccessibleEvent
announceNewlyFocusedItemEvent(this, QAccessible::Focus
);
471 announceNewlyFocusedItemEvent
.setChild(currentIndex
);
472 QAccessible::updateAccessibility(&announceNewlyFocusedItemEvent
);
474 QAccessibleEvent
announceCurrentItemNameChangeEvent(this, QAccessible::NameChanged
);
475 announceCurrentItemNameChangeEvent
.setChild(currentIndex
);
476 QAccessible::updateAccessibility(&announceCurrentItemNameChangeEvent
);
477 QAccessibleEvent
announceCurrentItemDescriptionChangeEvent(this, QAccessible::DescriptionChanged
);
478 announceCurrentItemDescriptionChangeEvent
.setChild(currentIndex
);
479 QAccessible::updateAccessibility(&announceCurrentItemDescriptionChangeEvent
);
481 m_lastAnnouncedIndex
= currentIndex
;
483 /// Announce the location if we are not just moving within the same location.
484 if (m_shouldAnnounceLocation
) {
485 m_shouldAnnounceLocation
= false;
487 QAccessibleEvent
announceAccessibleDescriptionEvent1(this, QAccessible::NameChanged
);
488 QAccessible::updateAccessibility(&announceAccessibleDescriptionEvent1
);
489 QAccessibleEvent
announceAccessibleDescriptionEvent(this, QAccessible::DescriptionChanged
);
490 QAccessible::updateAccessibility(&announceAccessibleDescriptionEvent
);