]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/accessibility/kitemlistviewaccessible.cpp
Adapt to Orca 47
[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 return i18nc("@info accessibility, 1 is path", "in a grid layout in location %1", modelRootUrl.toDisplayString());
284 }
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());
290 }
291 }
292
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());
297 }
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());
303 }
304
305 QRect KItemListViewAccessible::rect() const
306 {
307 if (!view()->isVisible()) {
308 return QRect();
309 }
310
311 const QGraphicsScene *scene = view()->scene();
312 if (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);
316 } else {
317 return QRect();
318 }
319 }
320
321 QAccessibleInterface *KItemListViewAccessible::child(int index) const
322 {
323 if (index >= 0 && index < childCount()) {
324 return accessibleDelegate(index);
325 }
326 return nullptr;
327 }
328
329 KItemListViewAccessible::AccessibleIdWrapper::AccessibleIdWrapper()
330 : isValid(false)
331 , id(0)
332 {
333 }
334
335 /* Selection interface */
336
337 bool KItemListViewAccessible::clear()
338 {
339 selectionManager()->clearSelection();
340 return true;
341 }
342
343 bool KItemListViewAccessible::isSelected(QAccessibleInterface *childItem) const
344 {
345 Q_CHECK_PTR(childItem);
346 return static_cast<KItemListDelegateAccessible *>(childItem)->isSelected();
347 }
348
349 bool KItemListViewAccessible::select(QAccessibleInterface *childItem)
350 {
351 selectionManager()->setSelected(indexOfChild(childItem));
352 return true;
353 }
354
355 bool KItemListViewAccessible::selectAll()
356 {
357 selectionManager()->setSelected(0, childCount());
358 return true;
359 }
360
361 QAccessibleInterface *KItemListViewAccessible::selectedItem(int selectionIndex) const
362 {
363 const auto selectedItems = selectionManager()->selectedItems();
364 int i = 0;
365 for (auto it = selectedItems.rbegin(); it != selectedItems.rend(); ++it) {
366 if (i == selectionIndex) {
367 return child(*it);
368 }
369 }
370 return nullptr;
371 }
372
373 int KItemListViewAccessible::selectedItemCount() const
374 {
375 return selectionManager()->selectedItems().count();
376 }
377
378 QList<QAccessibleInterface *> KItemListViewAccessible::selectedItems() const
379 {
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));
384 }
385 return selectedItemsInterfaces;
386 }
387
388 bool KItemListViewAccessible::unselect(QAccessibleInterface *childItem)
389 {
390 selectionManager()->setSelected(indexOfChild(childItem), 1, KItemListSelectionManager::Deselect);
391 return true;
392 }
393
394 /* Action Interface */
395
396 QStringList KItemListViewAccessible::actionNames() const
397 {
398 return {setFocusAction()};
399 }
400
401 void KItemListViewAccessible::doAction(const QString &actionName)
402 {
403 if (actionName == setFocusAction()) {
404 view()->setFocus();
405 }
406 }
407
408 QStringList KItemListViewAccessible::keyBindingsForAction(const QString &actionName) const
409 {
410 Q_UNUSED(actionName)
411 return {};
412 }
413
414 /* Custom non-interface methods */
415
416 KItemListView *KItemListViewAccessible::view() const
417 {
418 Q_CHECK_PTR(qobject_cast<KItemListView *>(object()));
419 return static_cast<KItemListView *>(object());
420 }
421
422 void KItemListViewAccessible::setAccessibleFocusAndAnnounceAll()
423 {
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);
429 return;
430 }
431
432 QAccessibleEvent accessibleFocusInEvent(this, QAccessible::Focus);
433 accessibleFocusInEvent.setChild(currentItemIndex);
434 QAccessible::updateAccessibility(&accessibleFocusInEvent);
435 m_shouldAnnounceLocation = true;
436 announceCurrentItem();
437 }
438
439 void KItemListViewAccessible::announceNewlyLoadedLocation(const QString &placeholderMessage)
440 {
441 m_placeholderMessage = placeholderMessage;
442 m_shouldAnnounceLocation = true;
443
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();
446 }
447
448 void KItemListViewAccessible::announceCurrentItem()
449 {
450 m_announceCurrentItemTimer->start();
451 }
452
453 void KItemListViewAccessible::slotAnnounceCurrentItemTimerTimeout()
454 {
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.
459 return;
460 }
461
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);
473 } else {
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);
480 }
481 m_lastAnnouncedIndex = currentIndex;
482
483 /// Announce the location if we are not just moving within the same location.
484 if (m_shouldAnnounceLocation) {
485 m_shouldAnnounceLocation = false;
486
487 QAccessibleEvent announceAccessibleDescriptionEvent1(this, QAccessible::NameChanged);
488 QAccessible::updateAccessibility(&announceAccessibleDescriptionEvent1);
489 QAccessibleEvent announceAccessibleDescriptionEvent(this, QAccessible::DescriptionChanged);
490 QAccessible::updateAccessibility(&announceAccessibleDescriptionEvent);
491 }
492 }