]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/accessibility/kitemlistviewaccessible.cpp
Have special keyboard controls in selection mode
[dolphin.git] / src / kitemviews / accessibility / kitemlistviewaccessible.cpp
1 /*
2 * SPDX-FileCopyrightText: 2012 Amandeep Singh <aman.dedman@gmail.com>
3 * SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8 #include "kitemlistviewaccessible.h"
9 #include "kitemlistcontaineraccessible.h"
10 #include "kitemlistdelegateaccessible.h"
11
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"
19
20 #include <KLocalizedString>
21
22 #include <QApplication> // for figuring out if we should move focus to this view.
23 #include <QGraphicsScene>
24 #include <QGraphicsView>
25
26 KItemListSelectionManager *KItemListViewAccessible::selectionManager() const
27 {
28 return view()->controller()->selectionManager();
29 }
30
31 KItemListViewAccessible::KItemListViewAccessible(KItemListView *view_, KItemListContainerAccessible *parent)
32 : QAccessibleObject(view_)
33 , m_parent(parent)
34 {
35 Q_ASSERT(view());
36 Q_CHECK_PTR(parent);
37 m_accessibleDelegates.resize(childCount());
38
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();
44 });
45 }
46
47 KItemListViewAccessible::~KItemListViewAccessible()
48 {
49 for (AccessibleIdWrapper idWrapper : std::as_const(m_accessibleDelegates)) {
50 if (idWrapper.isValid) {
51 QAccessible::deleteAccessibleInterface(idWrapper.id);
52 }
53 }
54 }
55
56 void *KItemListViewAccessible::interface_cast(QAccessible::InterfaceType type)
57 {
58 switch (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);
65 default:
66 return nullptr;
67 }
68 }
69
70 QAccessibleInterface *KItemListViewAccessible::accessibleDelegate(int index) const
71 {
72 if (index < 0 || index >= view()->model()->count()) {
73 return nullptr;
74 }
75
76 if (m_accessibleDelegates.size() <= index) {
77 m_accessibleDelegates.resize(childCount());
78 }
79 Q_ASSERT(index < m_accessibleDelegates.size());
80
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);
86 }
87 return QAccessible::accessibleInterface(idWrapper.id);
88 }
89
90 QAccessibleInterface *KItemListViewAccessible::cellAt(int row, int column) const
91 {
92 return accessibleDelegate(columnCount() * row + column);
93 }
94
95 QAccessibleInterface *KItemListViewAccessible::caption() const
96 {
97 return nullptr;
98 }
99
100 QString KItemListViewAccessible::columnDescription(int) const
101 {
102 return QString();
103 }
104
105 int KItemListViewAccessible::columnCount() const
106 {
107 return view()->m_layouter->columnCount();
108 }
109
110 int KItemListViewAccessible::rowCount() const
111 {
112 if (columnCount() <= 0) {
113 return 0;
114 }
115
116 int itemCount = view()->model()->count();
117 int rowCount = itemCount / columnCount();
118
119 if (rowCount <= 0) {
120 return 0;
121 }
122
123 if (itemCount % columnCount()) {
124 ++rowCount;
125 }
126 return rowCount;
127 }
128
129 int KItemListViewAccessible::selectedCellCount() const
130 {
131 return selectionManager()->selectedItems().count();
132 }
133
134 int KItemListViewAccessible::selectedColumnCount() const
135 {
136 return 0;
137 }
138
139 int KItemListViewAccessible::selectedRowCount() const
140 {
141 return 0;
142 }
143
144 QString KItemListViewAccessible::rowDescription(int) const
145 {
146 return QString();
147 }
148
149 QList<QAccessibleInterface *> KItemListViewAccessible::selectedCells() const
150 {
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));
156 }
157 return cells;
158 }
159
160 QList<int> KItemListViewAccessible::selectedColumns() const
161 {
162 return QList<int>();
163 }
164
165 QList<int> KItemListViewAccessible::selectedRows() const
166 {
167 return QList<int>();
168 }
169
170 QAccessibleInterface *KItemListViewAccessible::summary() const
171 {
172 return nullptr;
173 }
174
175 bool KItemListViewAccessible::isColumnSelected(int) const
176 {
177 return false;
178 }
179
180 bool KItemListViewAccessible::isRowSelected(int) const
181 {
182 return false;
183 }
184
185 bool KItemListViewAccessible::selectRow(int)
186 {
187 return true;
188 }
189
190 bool KItemListViewAccessible::selectColumn(int)
191 {
192 return true;
193 }
194
195 bool KItemListViewAccessible::unselectRow(int)
196 {
197 return true;
198 }
199
200 bool KItemListViewAccessible::unselectColumn(int)
201 {
202 return true;
203 }
204
205 void KItemListViewAccessible::modelChange(QAccessibleTableModelChangeEvent * /*event*/)
206 {
207 }
208
209 QAccessible::Role KItemListViewAccessible::role() const
210 {
211 return QAccessible::List;
212 }
213
214 QAccessible::State KItemListViewAccessible::state() const
215 {
216 QAccessible::State s;
217 s.focusable = true;
218 s.active = true;
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.
222 return s;
223 }
224
225 QAccessibleInterface *KItemListViewAccessible::childAt(int x, int y) const
226 {
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));
230 }
231
232 QAccessibleInterface *KItemListViewAccessible::parent() const
233 {
234 return m_parent;
235 }
236
237 int KItemListViewAccessible::childCount() const
238 {
239 return view()->model()->count();
240 }
241
242 int KItemListViewAccessible::indexOfChild(const QAccessibleInterface *interface) const
243 {
244 const KItemListDelegateAccessible *widget = static_cast<const KItemListDelegateAccessible *>(interface);
245 return widget->index();
246 }
247
248 QString KItemListViewAccessible::text(QAccessible::Text t) const
249 {
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();
255 }
256 if (t != QAccessible::Description) {
257 return QString();
258 }
259
260 QAccessibleInterface *currentItem = child(controller->selectionManager()->currentItem());
261
262 /**
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.
265 */
266 if (!currentItem) {
267 return i18nc("@info 1 states that the folder is empty and sometimes why, 2 is the full filesystem path",
268 "%1 at location %2",
269 m_placeholderMessage,
270 modelRootUrl.toDisplayString());
271 }
272
273 const int numberOfSelectedItems = selectedItemCount();
274
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());
285 }
286 return i18nc("@info accessibility, 1 is path", "in a grid layout in location %1", modelRootUrl.toDisplayString());
287 }
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());
294 }
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());
300 }
301 }
302
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());
308 }
309 return i18nc("@info accessibility, 1 is path", "in location %1", modelRootUrl.toDisplayString());
310 }
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());
317 }
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());
323 }
324
325 QRect KItemListViewAccessible::rect() const
326 {
327 if (!view()->isVisible()) {
328 return QRect();
329 }
330
331 const QGraphicsScene *scene = view()->scene();
332 if (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);
336 } else {
337 return QRect();
338 }
339 }
340
341 QAccessibleInterface *KItemListViewAccessible::child(int index) const
342 {
343 if (index >= 0 && index < childCount()) {
344 return accessibleDelegate(index);
345 }
346 return nullptr;
347 }
348
349 KItemListViewAccessible::AccessibleIdWrapper::AccessibleIdWrapper()
350 : isValid(false)
351 , id(0)
352 {
353 }
354
355 /* Selection interface */
356
357 bool KItemListViewAccessible::clear()
358 {
359 selectionManager()->clearSelection();
360 return true;
361 }
362
363 bool KItemListViewAccessible::isSelected(QAccessibleInterface *childItem) const
364 {
365 Q_CHECK_PTR(childItem);
366 return static_cast<KItemListDelegateAccessible *>(childItem)->isSelected();
367 }
368
369 bool KItemListViewAccessible::select(QAccessibleInterface *childItem)
370 {
371 selectionManager()->setSelected(indexOfChild(childItem));
372 return true;
373 }
374
375 bool KItemListViewAccessible::selectAll()
376 {
377 selectionManager()->setSelected(0, childCount());
378 return true;
379 }
380
381 QAccessibleInterface *KItemListViewAccessible::selectedItem(int selectionIndex) const
382 {
383 const auto selectedItems = selectionManager()->selectedItems();
384 int i = 0;
385 for (auto it = selectedItems.rbegin(); it != selectedItems.rend(); ++it) {
386 if (i == selectionIndex) {
387 return child(*it);
388 }
389 }
390 return nullptr;
391 }
392
393 int KItemListViewAccessible::selectedItemCount() const
394 {
395 return selectionManager()->selectedItems().count();
396 }
397
398 QList<QAccessibleInterface *> KItemListViewAccessible::selectedItems() const
399 {
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));
404 }
405 return selectedItemsInterfaces;
406 }
407
408 bool KItemListViewAccessible::unselect(QAccessibleInterface *childItem)
409 {
410 selectionManager()->setSelected(indexOfChild(childItem), 1, KItemListSelectionManager::Deselect);
411 return true;
412 }
413
414 /* Action Interface */
415
416 QStringList KItemListViewAccessible::actionNames() const
417 {
418 return {setFocusAction()};
419 }
420
421 void KItemListViewAccessible::doAction(const QString &actionName)
422 {
423 if (actionName == setFocusAction()) {
424 view()->setFocus();
425 }
426 }
427
428 QStringList KItemListViewAccessible::keyBindingsForAction(const QString &actionName) const
429 {
430 Q_UNUSED(actionName)
431 return {};
432 }
433
434 /* Custom non-interface methods */
435
436 KItemListView *KItemListViewAccessible::view() const
437 {
438 Q_CHECK_PTR(qobject_cast<KItemListView *>(object()));
439 return static_cast<KItemListView *>(object());
440 }
441
442 void KItemListViewAccessible::setAccessibleFocusAndAnnounceAll()
443 {
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);
449 return;
450 }
451
452 QAccessibleEvent accessibleFocusInEvent(this, QAccessible::Focus);
453 accessibleFocusInEvent.setChild(currentItemIndex);
454 QAccessible::updateAccessibility(&accessibleFocusInEvent);
455 m_shouldAnnounceLocation = true;
456 announceCurrentItem();
457 }
458
459 void KItemListViewAccessible::announceNewlyLoadedLocation(const QString &placeholderMessage)
460 {
461 m_placeholderMessage = placeholderMessage;
462 m_shouldAnnounceLocation = true;
463
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();
466 }
467
468 void KItemListViewAccessible::announceCurrentItem()
469 {
470 m_announceCurrentItemTimer->start();
471 }
472
473 void KItemListViewAccessible::slotAnnounceCurrentItemTimerTimeout()
474 {
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.
479 return;
480 }
481
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);
493 } else {
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);
500 }
501 m_lastAnnouncedIndex = currentIndex;
502
503 /// Announce the location if we are not just moving within the same location.
504 if (m_shouldAnnounceLocation) {
505 m_shouldAnnounceLocation = false;
506
507 QAccessibleEvent announceAccessibleDescriptionEvent1(this, QAccessible::NameChanged);
508 QAccessible::updateAccessibility(&announceAccessibleDescriptionEvent1);
509 QAccessibleEvent announceAccessibleDescriptionEvent(this, QAccessible::DescriptionChanged);
510 QAccessible::updateAccessibility(&announceAccessibleDescriptionEvent);
511 }
512 }
513
514 void KItemListViewAccessible::announceSelectionModeEnabled(const bool enabled)
515 {
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);
522 #endif
523 }