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 if (m_selectionMode
) {
284 return i18nc("@info accessibility, 1 is path", "in a grid layout in selection mode in location %1", modelRootUrl
.toDisplayString());
286 return i18nc("@info accessibility, 1 is path", "in a grid layout in location %1", modelRootUrl
.toDisplayString());
288 if (m_selectionMode
) {
289 return i18ncp("@info accessibility, 2 is path",
290 "%1 selected item in a grid layout in selection mode in location %2",
291 "%1 selected items in a grid layout in selection mode in location %2",
292 numberOfSelectedItems
,
293 modelRootUrl
.toDisplayString());
295 return i18ncp("@info accessibility, 2 is path",
296 "%1 selected item in a grid layout in location %2",
297 "%1 selected items in a grid layout in location %2",
298 numberOfSelectedItems
,
299 modelRootUrl
.toDisplayString());
303 if (numberOfSelectedItems
< 1 || (numberOfSelectedItems
== 1 && isSelected(currentItem
))) {
304 // We do not announce the number of selected items if the only selected item is the current item
305 // because the selection state of the current item is already announced elsewhere.
306 if (m_selectionMode
) {
307 return i18nc("@info accessibility, 1 is path", "in selection mode in location %1", modelRootUrl
.toDisplayString());
309 return i18nc("@info accessibility, 1 is path", "in location %1", modelRootUrl
.toDisplayString());
311 if (m_selectionMode
) {
312 return i18ncp("@info accessibility, 2 is path",
313 "%1 selected item in selection mode in location %2",
314 "%1 selected items in selection mode in location %2",
315 numberOfSelectedItems
,
316 modelRootUrl
.toDisplayString());
318 return i18ncp("@info accessibility, 2 is path",
319 "%1 selected item in location %2",
320 "%1 selected items in location %2",
321 numberOfSelectedItems
,
322 modelRootUrl
.toDisplayString());
325 QRect
KItemListViewAccessible::rect() const
327 if (!view()->isVisible()) {
331 const QGraphicsScene
*scene
= view()->scene();
333 const QPoint origin
= scene
->views().at(0)->mapToGlobal(QPoint(0, 0));
334 const QRect viewRect
= view()->geometry().toRect();
335 return viewRect
.translated(origin
);
341 QAccessibleInterface
*KItemListViewAccessible::child(int index
) const
343 if (index
>= 0 && index
< childCount()) {
344 return accessibleDelegate(index
);
349 KItemListViewAccessible::AccessibleIdWrapper::AccessibleIdWrapper()
355 /* Selection interface */
357 bool KItemListViewAccessible::clear()
359 selectionManager()->clearSelection();
363 bool KItemListViewAccessible::isSelected(QAccessibleInterface
*childItem
) const
365 Q_CHECK_PTR(childItem
);
366 return static_cast<KItemListDelegateAccessible
*>(childItem
)->isSelected();
369 bool KItemListViewAccessible::select(QAccessibleInterface
*childItem
)
371 selectionManager()->setSelected(indexOfChild(childItem
));
375 bool KItemListViewAccessible::selectAll()
377 selectionManager()->setSelected(0, childCount());
381 QAccessibleInterface
*KItemListViewAccessible::selectedItem(int selectionIndex
) const
383 const auto selectedItems
= selectionManager()->selectedItems();
385 for (auto it
= selectedItems
.rbegin(); it
!= selectedItems
.rend(); ++it
) {
386 if (i
== selectionIndex
) {
393 int KItemListViewAccessible::selectedItemCount() const
395 return selectionManager()->selectedItems().count();
398 QList
<QAccessibleInterface
*> KItemListViewAccessible::selectedItems() const
400 const auto selectedItems
= selectionManager()->selectedItems();
401 QList
<QAccessibleInterface
*> selectedItemsInterfaces
;
402 for (auto it
= selectedItems
.rbegin(); it
!= selectedItems
.rend(); ++it
) {
403 selectedItemsInterfaces
.append(child(*it
));
405 return selectedItemsInterfaces
;
408 bool KItemListViewAccessible::unselect(QAccessibleInterface
*childItem
)
410 selectionManager()->setSelected(indexOfChild(childItem
), 1, KItemListSelectionManager::Deselect
);
414 /* Action Interface */
416 QStringList
KItemListViewAccessible::actionNames() const
418 return {setFocusAction()};
421 void KItemListViewAccessible::doAction(const QString
&actionName
)
423 if (actionName
== setFocusAction()) {
428 QStringList
KItemListViewAccessible::keyBindingsForAction(const QString
&actionName
) const
434 /* Custom non-interface methods */
436 KItemListView
*KItemListViewAccessible::view() const
438 Q_CHECK_PTR(qobject_cast
<KItemListView
*>(object()));
439 return static_cast<KItemListView
*>(object());
442 void KItemListViewAccessible::setAccessibleFocusAndAnnounceAll()
444 const int currentItemIndex
= view()->m_controller
->selectionManager()->currentItem();
445 if (currentItemIndex
< 0) {
446 // The current item is invalid (perhaps because the folder is empty), so we set the focus to the view itself instead.
447 QAccessibleEvent
accessibleFocusInEvent(this, QAccessible::Focus
);
448 QAccessible::updateAccessibility(&accessibleFocusInEvent
);
452 QAccessibleEvent
accessibleFocusInEvent(this, QAccessible::Focus
);
453 accessibleFocusInEvent
.setChild(currentItemIndex
);
454 QAccessible::updateAccessibility(&accessibleFocusInEvent
);
455 m_shouldAnnounceLocation
= true;
456 announceCurrentItem();
459 void KItemListViewAccessible::announceNewlyLoadedLocation(const QString
&placeholderMessage
)
461 m_placeholderMessage
= placeholderMessage
;
462 m_shouldAnnounceLocation
= true;
464 // 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.
465 m_announceCurrentItemTimer
->start();
468 void KItemListViewAccessible::announceCurrentItem()
470 m_announceCurrentItemTimer
->start();
473 void KItemListViewAccessible::slotAnnounceCurrentItemTimerTimeout()
475 if (!view()->hasFocus() && QApplication::focusWidget() && QApplication::focusWidget()->isVisible()
476 && !static_cast<QWidget
*>(m_parent
->object())->isAncestorOf(QApplication::focusWidget())) {
477 // Something else than this view has focus, so we do not announce anything.
478 m_lastAnnouncedIndex
= -1; // Reset this to -1 so we properly move focus to the current item the next time this method is called.
482 /// Announce the current item (or the view if there is no current item).
483 const int currentIndex
= view()->m_controller
->selectionManager()->currentItem();
484 if (currentIndex
< 0) {
485 // The current index is invalid! There might be no items in the list. Instead the list itself is announced.
486 m_shouldAnnounceLocation
= true;
487 QAccessibleEvent
announceEmptyViewPlaceholderMessageEvent(this, QAccessible::Focus
);
488 QAccessible::updateAccessibility(&announceEmptyViewPlaceholderMessageEvent
);
489 } else if (currentIndex
!= m_lastAnnouncedIndex
) {
490 QAccessibleEvent
announceNewlyFocusedItemEvent(this, QAccessible::Focus
);
491 announceNewlyFocusedItemEvent
.setChild(currentIndex
);
492 QAccessible::updateAccessibility(&announceNewlyFocusedItemEvent
);
494 QAccessibleEvent
announceCurrentItemNameChangeEvent(this, QAccessible::NameChanged
);
495 announceCurrentItemNameChangeEvent
.setChild(currentIndex
);
496 QAccessible::updateAccessibility(&announceCurrentItemNameChangeEvent
);
497 QAccessibleEvent
announceCurrentItemDescriptionChangeEvent(this, QAccessible::DescriptionChanged
);
498 announceCurrentItemDescriptionChangeEvent
.setChild(currentIndex
);
499 QAccessible::updateAccessibility(&announceCurrentItemDescriptionChangeEvent
);
501 m_lastAnnouncedIndex
= currentIndex
;
503 /// Announce the location if we are not just moving within the same location.
504 if (m_shouldAnnounceLocation
) {
505 m_shouldAnnounceLocation
= false;
507 QAccessibleEvent
announceAccessibleDescriptionEvent1(this, QAccessible::NameChanged
);
508 QAccessible::updateAccessibility(&announceAccessibleDescriptionEvent1
);
509 QAccessibleEvent
announceAccessibleDescriptionEvent(this, QAccessible::DescriptionChanged
);
510 QAccessible::updateAccessibility(&announceAccessibleDescriptionEvent
);
514 void KItemListViewAccessible::announceSelectionModeEnabled(const bool enabled
)
516 m_selectionMode
= enabled
;
517 #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) // QAccessibleAnnouncementEvent is only available since 6.8
518 QAccessibleAnnouncementEvent
announceChangedControlsEvent(view(),
519 enabled
? i18nc("accessibility announcement", "Selection mode enabled")
520 : i18nc("accessibility announcement", "Selection mode disabled"));
521 QAccessible::updateAccessibility(&announceChangedControlsEvent
);