]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/accessibility/kitemlistviewaccessible.cpp
KItemListSmoothScroller: stop animation on property or targetobject change
[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_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);
47 });
48 }
49
50 KItemListViewAccessible::~KItemListViewAccessible()
51 {
52 for (AccessibleIdWrapper idWrapper : std::as_const(m_accessibleDelegates)) {
53 if (idWrapper.isValid) {
54 QAccessible::deleteAccessibleInterface(idWrapper.id);
55 }
56 }
57 }
58
59 void *KItemListViewAccessible::interface_cast(QAccessible::InterfaceType type)
60 {
61 switch (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);
68 default:
69 return nullptr;
70 }
71 }
72
73 void KItemListViewAccessible::modelReset()
74 {
75 }
76
77 QAccessibleInterface *KItemListViewAccessible::accessibleDelegate(int index) const
78 {
79 if (index < 0 || index >= view()->model()->count()) {
80 return nullptr;
81 }
82
83 if (m_accessibleDelegates.size() <= index) {
84 m_accessibleDelegates.resize(childCount());
85 }
86 Q_ASSERT(index < m_accessibleDelegates.size());
87
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);
93 }
94 return QAccessible::accessibleInterface(idWrapper.id);
95 }
96
97 QAccessibleInterface *KItemListViewAccessible::cellAt(int row, int column) const
98 {
99 return accessibleDelegate(columnCount() * row + column);
100 }
101
102 QAccessibleInterface *KItemListViewAccessible::caption() const
103 {
104 return nullptr;
105 }
106
107 QString KItemListViewAccessible::columnDescription(int) const
108 {
109 return QString();
110 }
111
112 int KItemListViewAccessible::columnCount() const
113 {
114 return view()->m_layouter->columnCount();
115 }
116
117 int KItemListViewAccessible::rowCount() const
118 {
119 if (columnCount() <= 0) {
120 return 0;
121 }
122
123 int itemCount = view()->model()->count();
124 int rowCount = itemCount / columnCount();
125
126 if (rowCount <= 0) {
127 return 0;
128 }
129
130 if (itemCount % columnCount()) {
131 ++rowCount;
132 }
133 return rowCount;
134 }
135
136 int KItemListViewAccessible::selectedCellCount() const
137 {
138 return selectionManager()->selectedItems().count();
139 }
140
141 int KItemListViewAccessible::selectedColumnCount() const
142 {
143 return 0;
144 }
145
146 int KItemListViewAccessible::selectedRowCount() const
147 {
148 return 0;
149 }
150
151 QString KItemListViewAccessible::rowDescription(int) const
152 {
153 return QString();
154 }
155
156 QList<QAccessibleInterface *> KItemListViewAccessible::selectedCells() const
157 {
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));
163 }
164 return cells;
165 }
166
167 QList<int> KItemListViewAccessible::selectedColumns() const
168 {
169 return QList<int>();
170 }
171
172 QList<int> KItemListViewAccessible::selectedRows() const
173 {
174 return QList<int>();
175 }
176
177 QAccessibleInterface *KItemListViewAccessible::summary() const
178 {
179 return nullptr;
180 }
181
182 bool KItemListViewAccessible::isColumnSelected(int) const
183 {
184 return false;
185 }
186
187 bool KItemListViewAccessible::isRowSelected(int) const
188 {
189 return false;
190 }
191
192 bool KItemListViewAccessible::selectRow(int)
193 {
194 return true;
195 }
196
197 bool KItemListViewAccessible::selectColumn(int)
198 {
199 return true;
200 }
201
202 bool KItemListViewAccessible::unselectRow(int)
203 {
204 return true;
205 }
206
207 bool KItemListViewAccessible::unselectColumn(int)
208 {
209 return true;
210 }
211
212 void KItemListViewAccessible::modelChange(QAccessibleTableModelChangeEvent * /*event*/)
213 {
214 }
215
216 QAccessible::Role KItemListViewAccessible::role() const
217 {
218 return QAccessible::List;
219 }
220
221 QAccessible::State KItemListViewAccessible::state() const
222 {
223 QAccessible::State s;
224 s.focusable = true;
225 s.active = true;
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.
229 return s;
230 }
231
232 QAccessibleInterface *KItemListViewAccessible::childAt(int x, int y) const
233 {
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));
237 }
238
239 QAccessibleInterface *KItemListViewAccessible::parent() const
240 {
241 return m_parent;
242 }
243
244 int KItemListViewAccessible::childCount() const
245 {
246 return view()->model()->count();
247 }
248
249 int KItemListViewAccessible::indexOfChild(const QAccessibleInterface *interface) const
250 {
251 const KItemListDelegateAccessible *widget = static_cast<const KItemListDelegateAccessible *>(interface);
252 return widget->index();
253 }
254
255 QString KItemListViewAccessible::text(QAccessible::Text t) const
256 {
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();
262 }
263 if (t != QAccessible::Description) {
264 return QString();
265 }
266 const auto currentItem = child(controller->selectionManager()->currentItem());
267 if (!currentItem) {
268 return i18nc("@info 1 states that the folder is empty and sometimes why, 2 is the full filesystem path",
269 "%1 at location %2",
270 m_placeholderMessage,
271 modelRootUrl.toDisplayString());
272 }
273
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
277 // readers.
278 : i18n("not selected,")};
279
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,");
286 } else {
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,");
290 }
291 }
292
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())
297 : QString()};
298
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");
306 }
307 }
308
309 /**
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.
315 */
316 return i18nc(
317 "@info 1 is currentlyFocussedItemName, 2 is empty or \"not selected, \", 3 is currentlyFocussedItemDescription, 3 is currentFolderName, 4 is "
318 "currentFolderPath",
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());
327 }
328
329 QRect KItemListViewAccessible::rect() const
330 {
331 if (!view()->isVisible()) {
332 return QRect();
333 }
334
335 const QGraphicsScene *scene = view()->scene();
336 if (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);
340 } else {
341 return QRect();
342 }
343 }
344
345 QAccessibleInterface *KItemListViewAccessible::child(int index) const
346 {
347 if (index >= 0 && index < childCount()) {
348 return accessibleDelegate(index);
349 }
350 return nullptr;
351 }
352
353 KItemListViewAccessible::AccessibleIdWrapper::AccessibleIdWrapper()
354 : isValid(false)
355 , id(0)
356 {
357 }
358
359 /* Selection interface */
360
361 bool KItemListViewAccessible::clear()
362 {
363 selectionManager()->clearSelection();
364 return true;
365 }
366
367 bool KItemListViewAccessible::isSelected(QAccessibleInterface *childItem) const
368 {
369 Q_CHECK_PTR(childItem);
370 return static_cast<KItemListDelegateAccessible *>(childItem)->isSelected();
371 }
372
373 bool KItemListViewAccessible::select(QAccessibleInterface *childItem)
374 {
375 selectionManager()->setSelected(indexOfChild(childItem));
376 return true;
377 }
378
379 bool KItemListViewAccessible::selectAll()
380 {
381 selectionManager()->setSelected(0, childCount());
382 return true;
383 }
384
385 QAccessibleInterface *KItemListViewAccessible::selectedItem(int selectionIndex) const
386 {
387 const auto selectedItems = selectionManager()->selectedItems();
388 int i = 0;
389 for (auto it = selectedItems.rbegin(); it != selectedItems.rend(); ++it) {
390 if (i == selectionIndex) {
391 return child(*it);
392 }
393 }
394 return nullptr;
395 }
396
397 int KItemListViewAccessible::selectedItemCount() const
398 {
399 return selectionManager()->selectedItems().count();
400 }
401
402 QList<QAccessibleInterface *> KItemListViewAccessible::selectedItems() const
403 {
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));
408 }
409 return selectedItemsInterfaces;
410 }
411
412 bool KItemListViewAccessible::unselect(QAccessibleInterface *childItem)
413 {
414 selectionManager()->setSelected(indexOfChild(childItem), 1, KItemListSelectionManager::Deselect);
415 return true;
416 }
417
418 /* Action Interface */
419
420 QStringList KItemListViewAccessible::actionNames() const
421 {
422 return {setFocusAction()};
423 }
424
425 void KItemListViewAccessible::doAction(const QString &actionName)
426 {
427 if (actionName == setFocusAction()) {
428 view()->setFocus();
429 }
430 }
431
432 QStringList KItemListViewAccessible::keyBindingsForAction(const QString &actionName) const
433 {
434 Q_UNUSED(actionName)
435 return {};
436 }
437
438 /* Custom non-interface methods */
439
440 KItemListView *KItemListViewAccessible::view() const
441 {
442 Q_CHECK_PTR(qobject_cast<KItemListView *>(object()));
443 return static_cast<KItemListView *>(object());
444 }
445
446 void KItemListViewAccessible::announceOverallViewState(const QString &placeholderMessage)
447 {
448 m_placeholderMessage = placeholderMessage;
449
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())) {
453 view()->setFocus();
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,
458 &QTimer::timeout,
459 view(),
460 [this]() {
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.
467 }
468 },
469 Qt::SingleShotConnection);
470 if (!m_announceDescriptionChangeTimer->isActive()) {
471 m_announceDescriptionChangeTimer->start();
472 }
473 }
474 }
475
476 void KItemListViewAccessible::announceDescriptionChange()
477 {
478 m_announceDescriptionChangeTimer->start();
479 }