]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kitemlistcontroller.cpp
Overhaul main view accessibility
[dolphin.git] / src / kitemviews / kitemlistcontroller.cpp
1 /*
2 * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
3 * SPDX-FileCopyrightText: 2012 Frank Reininghaus <frank78ac@googlemail.com>
4 *
5 * Based on the Itemviews NG project from Trolltech Labs
6 *
7 * SPDX-License-Identifier: GPL-2.0-or-later
8 */
9
10 #include "kitemlistcontroller.h"
11
12 #include "kitemlistselectionmanager.h"
13 #include "kitemlistview.h"
14 #include "private/kitemlistkeyboardsearchmanager.h"
15 #include "private/kitemlistrubberband.h"
16 #include "views/draganddrophelper.h"
17
18 #include <KTwoFingerSwipe>
19 #include <KTwoFingerTap>
20 #include <KUrlMimeData>
21
22 #include <QAccessible>
23 #include <QApplication>
24 #include <QDrag>
25 #include <QGesture>
26 #include <QGraphicsScene>
27 #include <QGraphicsSceneEvent>
28 #include <QGraphicsView>
29 #include <QMimeData>
30 #include <QTimer>
31 #include <QTouchEvent>
32
33 KItemListController::KItemListController(KItemModelBase *model, KItemListView *view, QObject *parent)
34 : QObject(parent)
35 , m_singleClickActivationEnforced(false)
36 , m_selectionMode(false)
37 , m_selectionTogglePressed(false)
38 , m_clearSelectionIfItemsAreNotDragged(false)
39 , m_isSwipeGesture(false)
40 , m_dragActionOrRightClick(false)
41 , m_scrollerIsScrolling(false)
42 , m_pinchGestureInProgress(false)
43 , m_mousePress(false)
44 , m_isTouchEvent(false)
45 , m_selectionBehavior(NoSelection)
46 , m_autoActivationBehavior(ActivationAndExpansion)
47 , m_mouseDoubleClickAction(ActivateItemOnly)
48 , m_model(nullptr)
49 , m_view(nullptr)
50 , m_selectionManager(new KItemListSelectionManager(this))
51 , m_keyboardManager(new KItemListKeyboardSearchManager(this))
52 , m_pressedIndex(std::nullopt)
53 , m_pressedMouseGlobalPos()
54 , m_autoActivationTimer(nullptr)
55 , m_swipeGesture(Qt::CustomGesture)
56 , m_twoFingerTapGesture(Qt::CustomGesture)
57 , m_oldSelection()
58 , m_keyboardAnchorIndex(-1)
59 , m_keyboardAnchorPos(0)
60 {
61 connect(m_keyboardManager, &KItemListKeyboardSearchManager::changeCurrentItem, this, &KItemListController::slotChangeCurrentItem);
62 connect(m_selectionManager, &KItemListSelectionManager::currentChanged, m_keyboardManager, &KItemListKeyboardSearchManager::slotCurrentChanged);
63 connect(m_selectionManager, &KItemListSelectionManager::selectionChanged, m_keyboardManager, &KItemListKeyboardSearchManager::slotSelectionChanged);
64
65 m_autoActivationTimer = new QTimer(this);
66 m_autoActivationTimer->setSingleShot(true);
67 m_autoActivationTimer->setInterval(-1);
68 connect(m_autoActivationTimer, &QTimer::timeout, this, &KItemListController::slotAutoActivationTimeout);
69
70 setModel(model);
71 setView(view);
72
73 m_swipeGesture = QGestureRecognizer::registerRecognizer(new KTwoFingerSwipeRecognizer());
74 m_twoFingerTapGesture = QGestureRecognizer::registerRecognizer(new KTwoFingerTapRecognizer());
75 view->grabGesture(m_swipeGesture);
76 view->grabGesture(m_twoFingerTapGesture);
77 view->grabGesture(Qt::TapGesture);
78 view->grabGesture(Qt::TapAndHoldGesture);
79 view->grabGesture(Qt::PinchGesture);
80 }
81
82 KItemListController::~KItemListController()
83 {
84 setView(nullptr);
85 Q_ASSERT(!m_view);
86
87 setModel(nullptr);
88 Q_ASSERT(!m_model);
89 }
90
91 void KItemListController::setModel(KItemModelBase *model)
92 {
93 if (m_model == model) {
94 return;
95 }
96
97 KItemModelBase *oldModel = m_model;
98 if (oldModel) {
99 oldModel->deleteLater();
100 }
101
102 m_model = model;
103 if (m_model) {
104 m_model->setParent(this);
105 }
106
107 if (m_view) {
108 m_view->setModel(m_model);
109 }
110
111 m_selectionManager->setModel(m_model);
112
113 Q_EMIT modelChanged(m_model, oldModel);
114 }
115
116 KItemModelBase *KItemListController::model() const
117 {
118 return m_model;
119 }
120
121 KItemListSelectionManager *KItemListController::selectionManager() const
122 {
123 return m_selectionManager;
124 }
125
126 void KItemListController::setView(KItemListView *view)
127 {
128 if (m_view == view) {
129 return;
130 }
131
132 KItemListView *oldView = m_view;
133 if (oldView) {
134 disconnect(oldView, &KItemListView::scrollOffsetChanged, this, &KItemListController::slotViewScrollOffsetChanged);
135 oldView->deleteLater();
136 }
137
138 m_view = view;
139
140 if (m_view) {
141 m_view->setParent(this);
142 m_view->setController(this);
143 m_view->setModel(m_model);
144 connect(m_view, &KItemListView::scrollOffsetChanged, this, &KItemListController::slotViewScrollOffsetChanged);
145 updateExtendedSelectionRegion();
146 }
147
148 Q_EMIT viewChanged(m_view, oldView);
149 }
150
151 KItemListView *KItemListController::view() const
152 {
153 return m_view;
154 }
155
156 void KItemListController::setSelectionBehavior(SelectionBehavior behavior)
157 {
158 m_selectionBehavior = behavior;
159 updateExtendedSelectionRegion();
160 }
161
162 KItemListController::SelectionBehavior KItemListController::selectionBehavior() const
163 {
164 return m_selectionBehavior;
165 }
166
167 void KItemListController::setAutoActivationBehavior(AutoActivationBehavior behavior)
168 {
169 m_autoActivationBehavior = behavior;
170 }
171
172 KItemListController::AutoActivationBehavior KItemListController::autoActivationBehavior() const
173 {
174 return m_autoActivationBehavior;
175 }
176
177 void KItemListController::setMouseDoubleClickAction(MouseDoubleClickAction action)
178 {
179 m_mouseDoubleClickAction = action;
180 }
181
182 KItemListController::MouseDoubleClickAction KItemListController::mouseDoubleClickAction() const
183 {
184 return m_mouseDoubleClickAction;
185 }
186
187 int KItemListController::indexCloseToMousePressedPosition() const
188 {
189 const QPointF pressedMousePos = m_view->transform().map(m_view->scene()->views().first()->mapFromGlobal(m_pressedMouseGlobalPos.toPoint()));
190
191 QHashIterator<KItemListWidget *, KItemListGroupHeader *> it(m_view->m_visibleGroups);
192 while (it.hasNext()) {
193 it.next();
194 KItemListGroupHeader *groupHeader = it.value();
195 const QPointF mappedToGroup = groupHeader->mapFromItem(nullptr, pressedMousePos);
196 if (groupHeader->contains(mappedToGroup)) {
197 return it.key()->index();
198 }
199 }
200 return -1;
201 }
202
203 void KItemListController::setAutoActivationDelay(int delay)
204 {
205 m_autoActivationTimer->setInterval(delay);
206 }
207
208 int KItemListController::autoActivationDelay() const
209 {
210 return m_autoActivationTimer->interval();
211 }
212
213 void KItemListController::setSingleClickActivationEnforced(bool singleClick)
214 {
215 m_singleClickActivationEnforced = singleClick;
216 }
217
218 bool KItemListController::singleClickActivationEnforced() const
219 {
220 return m_singleClickActivationEnforced;
221 }
222
223 void KItemListController::setSelectionModeEnabled(bool enabled)
224 {
225 m_selectionMode = enabled;
226 }
227
228 bool KItemListController::selectionMode() const
229 {
230 return m_selectionMode;
231 }
232
233 bool KItemListController::isSearchAsYouTypeActive() const
234 {
235 return m_keyboardManager->isSearchAsYouTypeActive();
236 }
237
238 bool KItemListController::keyPressEvent(QKeyEvent *event)
239 {
240 int index = m_selectionManager->currentItem();
241 int key = event->key();
242 const bool shiftPressed = event->modifiers() & Qt::ShiftModifier;
243
244 const bool horizontalScrolling = m_view->scrollOrientation() == Qt::Horizontal;
245
246 // Handle the expanding/collapsing of items
247 // expand / collapse all selected directories
248 if (m_view->supportsItemExpanding() && m_model->isExpandable(index) && (key == Qt::Key_Right || key == Qt::Key_Left)) {
249 const bool expandOrCollapse = key == Qt::Key_Right ? true : false;
250 bool shouldReturn = m_model->setExpanded(index, expandOrCollapse);
251
252 // edit in reverse to preserve index of the first handled items
253 const auto selectedItems = m_selectionManager->selectedItems();
254 for (auto it = selectedItems.rbegin(); it != selectedItems.rend(); ++it) {
255 shouldReturn |= m_model->setExpanded(*it, expandOrCollapse);
256 if (!shiftPressed) {
257 m_selectionManager->setSelected(*it);
258 }
259 }
260 if (shouldReturn) {
261 // update keyboard anchors
262 if (shiftPressed) {
263 m_keyboardAnchorIndex = selectedItems.count() > 0 ? qMin(index, selectedItems.last()) : index;
264 m_keyboardAnchorPos = keyboardAnchorPos(m_keyboardAnchorIndex);
265 }
266
267 event->ignore();
268 return true;
269 }
270 }
271
272 const bool controlPressed = event->modifiers() & Qt::ControlModifier;
273 const bool shiftOrControlPressed = shiftPressed || controlPressed;
274 const bool navigationPressed = key == Qt::Key_Home || key == Qt::Key_End || key == Qt::Key_PageUp || key == Qt::Key_PageDown || key == Qt::Key_Up
275 || key == Qt::Key_Down || key == Qt::Key_Left || key == Qt::Key_Right;
276
277 const int itemCount = m_model->count();
278
279 // For horizontal scroll orientation, transform
280 // the arrow keys to simplify the event handling.
281 if (horizontalScrolling) {
282 switch (key) {
283 case Qt::Key_Up:
284 key = Qt::Key_Left;
285 break;
286 case Qt::Key_Down:
287 key = Qt::Key_Right;
288 break;
289 case Qt::Key_Left:
290 key = Qt::Key_Up;
291 break;
292 case Qt::Key_Right:
293 key = Qt::Key_Down;
294 break;
295 default:
296 break;
297 }
298 }
299
300 if (m_view->layoutDirection() == Qt::RightToLeft) {
301 if (horizontalScrolling) {
302 // swap up and down arrow keys
303 switch (key) {
304 case Qt::Key_Up:
305 key = Qt::Key_Down;
306 break;
307 case Qt::Key_Down:
308 key = Qt::Key_Up;
309 break;
310 default:
311 break;
312 }
313 } else if (!m_view->supportsItemExpanding()) {
314 // swap left and right arrow keys
315 switch (key) {
316 case Qt::Key_Left:
317 key = Qt::Key_Right;
318 break;
319 case Qt::Key_Right:
320 key = Qt::Key_Left;
321 break;
322 default:
323 break;
324 }
325 }
326 }
327
328 const bool selectSingleItem = m_selectionBehavior != NoSelection && itemCount == 1 && navigationPressed;
329
330 if (selectSingleItem) {
331 const int current = m_selectionManager->currentItem();
332 m_selectionManager->setSelected(current);
333 return true;
334 }
335
336 switch (key) {
337 case Qt::Key_Home:
338 index = 0;
339 m_keyboardAnchorIndex = index;
340 m_keyboardAnchorPos = keyboardAnchorPos(index);
341 break;
342
343 case Qt::Key_End:
344 index = itemCount - 1;
345 m_keyboardAnchorIndex = index;
346 m_keyboardAnchorPos = keyboardAnchorPos(index);
347 break;
348
349 case Qt::Key_Left:
350 if (index > 0) {
351 const int expandedParentsCount = m_model->expandedParentsCount(index);
352 if (expandedParentsCount == 0) {
353 --index;
354 } else {
355 // Go to the parent of the current item.
356 do {
357 --index;
358 } while (index > 0 && m_model->expandedParentsCount(index) == expandedParentsCount);
359 }
360 m_keyboardAnchorIndex = index;
361 m_keyboardAnchorPos = keyboardAnchorPos(index);
362 }
363 break;
364
365 case Qt::Key_Right:
366 if (index < itemCount - 1) {
367 ++index;
368 m_keyboardAnchorIndex = index;
369 m_keyboardAnchorPos = keyboardAnchorPos(index);
370 }
371 break;
372
373 case Qt::Key_Up:
374 updateKeyboardAnchor();
375 if (shiftPressed && !m_selectionManager->isAnchoredSelectionActive() && m_selectionManager->isSelected(index)) {
376 m_selectionManager->beginAnchoredSelection(index);
377 }
378 index = previousRowIndex(index);
379 break;
380
381 case Qt::Key_Down:
382 updateKeyboardAnchor();
383 if (shiftPressed && !m_selectionManager->isAnchoredSelectionActive() && m_selectionManager->isSelected(index)) {
384 m_selectionManager->beginAnchoredSelection(index);
385 }
386 index = nextRowIndex(index);
387 break;
388
389 case Qt::Key_PageUp:
390 if (horizontalScrolling) {
391 // The new current index should correspond to the first item in the current column.
392 int newIndex = qMax(index - 1, 0);
393 while (newIndex != index && m_view->itemRect(newIndex).topLeft().y() < m_view->itemRect(index).topLeft().y()) {
394 index = newIndex;
395 newIndex = qMax(index - 1, 0);
396 }
397 m_keyboardAnchorIndex = index;
398 m_keyboardAnchorPos = keyboardAnchorPos(index);
399 } else {
400 const qreal currentItemBottom = m_view->itemRect(index).bottomLeft().y();
401 const qreal height = m_view->geometry().height();
402
403 // The new current item should be the first item in the current
404 // column whose itemRect's top coordinate is larger than targetY.
405 const qreal targetY = currentItemBottom - height;
406
407 updateKeyboardAnchor();
408 int newIndex = previousRowIndex(index);
409 do {
410 index = newIndex;
411 updateKeyboardAnchor();
412 newIndex = previousRowIndex(index);
413 } while (m_view->itemRect(newIndex).topLeft().y() > targetY && newIndex != index);
414 }
415 break;
416
417 case Qt::Key_PageDown:
418 if (horizontalScrolling) {
419 // The new current index should correspond to the last item in the current column.
420 int newIndex = qMin(index + 1, m_model->count() - 1);
421 while (newIndex != index && m_view->itemRect(newIndex).topLeft().y() > m_view->itemRect(index).topLeft().y()) {
422 index = newIndex;
423 newIndex = qMin(index + 1, m_model->count() - 1);
424 }
425 m_keyboardAnchorIndex = index;
426 m_keyboardAnchorPos = keyboardAnchorPos(index);
427 } else {
428 const qreal currentItemTop = m_view->itemRect(index).topLeft().y();
429 const qreal height = m_view->geometry().height();
430
431 // The new current item should be the last item in the current
432 // column whose itemRect's bottom coordinate is smaller than targetY.
433 const qreal targetY = currentItemTop + height;
434
435 updateKeyboardAnchor();
436 int newIndex = nextRowIndex(index);
437 do {
438 index = newIndex;
439 updateKeyboardAnchor();
440 newIndex = nextRowIndex(index);
441 } while (m_view->itemRect(newIndex).bottomLeft().y() < targetY && newIndex != index);
442 }
443 break;
444
445 case Qt::Key_Enter:
446 case Qt::Key_Return: {
447 const KItemSet selectedItems = m_selectionManager->selectedItems();
448 if (selectedItems.count() >= 2) {
449 Q_EMIT itemsActivated(selectedItems);
450 } else if (selectedItems.count() == 1) {
451 Q_EMIT itemActivated(selectedItems.first());
452 } else {
453 Q_EMIT itemActivated(index);
454 }
455 break;
456 }
457
458 case Qt::Key_Escape:
459 if (m_selectionMode) {
460 Q_EMIT selectionModeChangeRequested(false);
461 } else if (m_selectionBehavior != SingleSelection) {
462 m_selectionManager->clearSelection();
463 }
464 m_keyboardManager->cancelSearch();
465 Q_EMIT escapePressed();
466 break;
467
468 case Qt::Key_Space:
469 if (m_selectionBehavior == MultiSelection) {
470 #ifndef QT_NO_ACCESSIBILITY
471 // Move accessible focus to the item that is acted upon, so only the state change of this item is announced and not the whole view.
472 QAccessibleEvent accessibilityEvent(view(), QAccessible::Focus);
473 accessibilityEvent.setChild(index);
474 QAccessible::updateAccessibility(&accessibilityEvent);
475 #endif
476 if (controlPressed) {
477 // Toggle the selection state of the current item.
478 m_selectionManager->endAnchoredSelection();
479 m_selectionManager->setSelected(index, 1, KItemListSelectionManager::Toggle);
480 m_selectionManager->beginAnchoredSelection(index);
481 break;
482 } else {
483 // Select the current item if it is not selected yet.
484 const int current = m_selectionManager->currentItem();
485 if (!m_selectionManager->isSelected(current)) {
486 m_selectionManager->setSelected(current);
487 break;
488 }
489 }
490 }
491 Q_FALLTHROUGH(); // fall through to the default case and add the Space to the current search string.
492 default:
493 m_keyboardManager->addKeys(event->text());
494 // Make sure unconsumed events get propagated up the chain. #302329
495 event->ignore();
496 return false;
497 }
498
499 if (m_selectionManager->currentItem() != index) {
500 switch (m_selectionBehavior) {
501 case NoSelection:
502 m_selectionManager->setCurrentItem(index);
503 break;
504
505 case SingleSelection:
506 m_selectionManager->setCurrentItem(index);
507 m_selectionManager->clearSelection();
508 m_selectionManager->setSelected(index, 1);
509 break;
510
511 case MultiSelection:
512 if (controlPressed) {
513 m_selectionManager->endAnchoredSelection();
514 }
515
516 m_selectionManager->setCurrentItem(index);
517
518 if (!shiftOrControlPressed) {
519 m_selectionManager->clearSelection();
520 m_selectionManager->setSelected(index, 1);
521 }
522
523 if (!shiftPressed) {
524 m_selectionManager->beginAnchoredSelection(index);
525 }
526 break;
527 }
528 }
529
530 if (navigationPressed) {
531 m_view->scrollToItem(index);
532 }
533 return true;
534 }
535
536 void KItemListController::slotChangeCurrentItem(const QString &text, bool searchFromNextItem)
537 {
538 if (!m_model || m_model->count() == 0) {
539 return;
540 }
541 int index;
542 if (searchFromNextItem) {
543 const int currentIndex = m_selectionManager->currentItem();
544 index = m_model->indexForKeyboardSearch(text, (currentIndex + 1) % m_model->count());
545 } else {
546 index = m_model->indexForKeyboardSearch(text, 0);
547 }
548 if (index >= 0) {
549 m_selectionManager->setCurrentItem(index);
550
551 if (m_selectionBehavior != NoSelection) {
552 m_selectionManager->replaceSelection(index);
553 m_selectionManager->beginAnchoredSelection(index);
554 }
555
556 m_view->scrollToItem(index, KItemListView::ViewItemPosition::Beginning);
557 }
558 }
559
560 void KItemListController::slotAutoActivationTimeout()
561 {
562 if (!m_model || !m_view) {
563 return;
564 }
565
566 const int index = m_autoActivationTimer->property("index").toInt();
567 if (index < 0 || index >= m_model->count()) {
568 return;
569 }
570
571 /* m_view->isUnderMouse() fixes a bug in the Folder-View-Panel and in the
572 * Places-Panel.
573 *
574 * Bug: When you drag a file onto a Folder-View-Item or a Places-Item and
575 * then move away before the auto-activation timeout triggers, than the
576 * item still becomes activated/expanded.
577 *
578 * See Bug 293200 and 305783
579 */
580 if (m_view->isUnderMouse()) {
581 if (m_view->supportsItemExpanding() && m_model->isExpandable(index)) {
582 const bool expanded = m_model->isExpanded(index);
583 m_model->setExpanded(index, !expanded);
584 } else if (m_autoActivationBehavior != ExpansionOnly) {
585 Q_EMIT itemActivated(index);
586 }
587 }
588 }
589
590 bool KItemListController::inputMethodEvent(QInputMethodEvent *event)
591 {
592 Q_UNUSED(event)
593 return false;
594 }
595
596 bool KItemListController::mousePressEvent(QGraphicsSceneMouseEvent *event, const QTransform &transform)
597 {
598 m_mousePress = true;
599 m_pressedMouseGlobalPos = event->screenPos();
600
601 if (event->source() == Qt::MouseEventSynthesizedByQt && m_isTouchEvent) {
602 return false;
603 }
604
605 if (!m_view) {
606 return false;
607 }
608
609 const QPointF pressedMousePos = transform.map(event->pos());
610 m_pressedIndex = m_view->itemAt(pressedMousePos);
611
612 const Qt::MouseButtons buttons = event->buttons();
613
614 if (!onPress(event->pos(), event->modifiers(), buttons)) {
615 startRubberBand();
616 return false;
617 }
618
619 return true;
620 }
621
622 bool KItemListController::mouseMoveEvent(QGraphicsSceneMouseEvent *event, const QTransform &transform)
623 {
624 if (!m_view) {
625 return false;
626 }
627
628 if (m_view->m_tapAndHoldIndicator->isActive()) {
629 m_view->m_tapAndHoldIndicator->setActive(false);
630 }
631
632 if (event->source() == Qt::MouseEventSynthesizedByQt && !m_dragActionOrRightClick && m_isTouchEvent) {
633 return false;
634 }
635
636 if (m_pressedIndex.has_value() && !m_view->rubberBand()->isActive()) {
637 // Check whether a dragging should be started
638 if (event->buttons() & Qt::LeftButton) {
639 const auto distance = (event->screenPos() - m_pressedMouseGlobalPos).manhattanLength();
640 if (distance >= QApplication::startDragDistance()) {
641 if (!m_selectionManager->isSelected(m_pressedIndex.value())) {
642 // Always assure that the dragged item gets selected. Usually this is already
643 // done on the mouse-press event, but when using the selection-toggle on a
644 // selected item the dragged item is not selected yet.
645 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
646 } else {
647 // A selected item has been clicked to drag all selected items
648 // -> the selection should not be cleared when the mouse button is released.
649 m_clearSelectionIfItemsAreNotDragged = false;
650 }
651 startDragging();
652 m_mousePress = false;
653 }
654 }
655 } else {
656 KItemListRubberBand *rubberBand = m_view->rubberBand();
657 if (rubberBand->isActive()) {
658 QPointF endPos = transform.map(event->pos());
659
660 // Update the current item.
661 const std::optional<int> newCurrent = m_view->itemAt(endPos);
662 if (newCurrent.has_value()) {
663 // It's expected that the new current index is also the new anchor (bug 163451).
664 m_selectionManager->endAnchoredSelection();
665 m_selectionManager->setCurrentItem(newCurrent.value());
666 m_selectionManager->beginAnchoredSelection(newCurrent.value());
667 }
668
669 if (m_view->scrollOrientation() == Qt::Vertical) {
670 endPos.ry() += m_view->scrollOffset();
671 } else {
672 endPos.rx() += m_view->scrollOffset();
673 }
674 rubberBand->setEndPosition(endPos);
675 }
676 }
677
678 return false;
679 }
680
681 bool KItemListController::mouseReleaseEvent(QGraphicsSceneMouseEvent *event, const QTransform &transform)
682 {
683 m_mousePress = false;
684 m_isTouchEvent = false;
685
686 if (!m_view) {
687 return false;
688 }
689
690 if (m_view->m_tapAndHoldIndicator->isActive()) {
691 m_view->m_tapAndHoldIndicator->setActive(false);
692 }
693
694 KItemListRubberBand *rubberBand = m_view->rubberBand();
695 if (event->source() == Qt::MouseEventSynthesizedByQt && !rubberBand->isActive() && m_isTouchEvent) {
696 return false;
697 }
698
699 Q_EMIT mouseButtonReleased(m_pressedIndex.value_or(-1), event->buttons());
700
701 return onRelease(transform.map(event->pos()), event->modifiers(), event->button(), false);
702 }
703
704 bool KItemListController::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event, const QTransform &transform)
705 {
706 const QPointF pos = transform.map(event->pos());
707 const std::optional<int> index = m_view->itemAt(pos);
708
709 if (event->button() & (Qt::ForwardButton | Qt::BackButton)) {
710 // "Forward" and "Back" are reserved for quickly navigating through the
711 // history. Double-clicking those buttons should be interpreted as two
712 // separate button presses. We arrive here for the second click, which
713 // we now react to just as we would for a singular click
714 Q_EMIT mouseButtonPressed(index.value_or(-1), event->button());
715 return false;
716 }
717
718 if (!index.has_value()) {
719 Q_EMIT doubleClickViewBackground(event->button());
720 return false;
721 }
722
723 // Expand item if desired - See Bug 295573
724 if (m_mouseDoubleClickAction != ActivateItemOnly) {
725 if (m_view && m_model && m_view->supportsItemExpanding() && m_model->isExpandable(index.value_or(-1))) {
726 const bool expanded = m_model->isExpanded(index.value());
727 m_model->setExpanded(index.value(), !expanded);
728 }
729 }
730
731 if (event->button() & ~Qt::LeftButton) {
732 return false;
733 }
734
735 if (m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced) {
736 return false;
737 }
738
739 const bool emitItemActivated = index.has_value() && index.value() < m_model->count() && !m_view->isAboveExpansionToggle(index.value(), pos);
740 if (emitItemActivated) {
741 if (!QApplication::keyboardModifiers()) {
742 m_selectionManager->clearSelection(); // The user does not want to manage/manipulate the item currently, only activate it.
743 }
744 Q_EMIT itemActivated(index.value());
745 }
746 return false;
747 }
748
749 bool KItemListController::contextMenuEvent(QContextMenuEvent *event)
750 {
751 if (event->reason() == QContextMenuEvent::Keyboard) {
752 // Emit the signal itemContextMenuRequested() if at least one item is selected.
753 // Otherwise the signal viewContextMenuRequested() will be emitted.
754 const KItemSet selectedItems = m_selectionManager->selectedItems();
755 int index = -1;
756 if (selectedItems.count() >= 2) {
757 const int currentItemIndex = m_selectionManager->currentItem();
758 index = selectedItems.contains(currentItemIndex) ? currentItemIndex : selectedItems.first();
759 } else if (selectedItems.count() == 1) {
760 index = selectedItems.first();
761 }
762
763 if (index >= 0) {
764 const QRectF contextRect = m_view->itemContextRect(index);
765 const QPointF pos(m_view->scene()->views().first()->mapToGlobal(contextRect.bottomRight().toPoint()));
766 Q_EMIT itemContextMenuRequested(index, pos);
767 } else {
768 Q_EMIT viewContextMenuRequested(event->globalPos());
769 }
770 return true;
771 }
772
773 const auto pos = event->pos();
774 const auto globalPos = event->globalPos();
775
776 if (m_view->headerBoundaries().contains(pos)) {
777 Q_EMIT headerContextMenuRequested(globalPos);
778 return true;
779 }
780
781 const auto pressedItem = m_view->itemAt(pos);
782 // We only open a context menu for the pressed item if it is selected.
783 // That's because the same click might have de-selected the item or because the press was only in the row of the item but not on it.
784 if (pressedItem && m_selectionManager->selectedItems().contains(pressedItem.value())) {
785 // The selection rectangle for an item was clicked
786 Q_EMIT itemContextMenuRequested(pressedItem.value(), globalPos);
787 return true;
788 }
789
790 // Remove any hover highlights so the context menu doesn't look like it applies to a row.
791 const auto widgets = m_view->visibleItemListWidgets();
792 for (KItemListWidget *widget : widgets) {
793 if (widget->isHovered()) {
794 widget->setHovered(false);
795 Q_EMIT itemUnhovered(widget->index());
796 }
797 }
798 Q_EMIT viewContextMenuRequested(globalPos);
799 return true;
800 }
801
802 bool KItemListController::dragEnterEvent(QGraphicsSceneDragDropEvent *event, const QTransform &transform)
803 {
804 Q_UNUSED(event)
805 Q_UNUSED(transform)
806
807 DragAndDropHelper::clearUrlListMatchesUrlCache();
808
809 return false;
810 }
811
812 bool KItemListController::dragLeaveEvent(QGraphicsSceneDragDropEvent *event, const QTransform &transform)
813 {
814 Q_UNUSED(event)
815 Q_UNUSED(transform)
816
817 m_autoActivationTimer->stop();
818 m_view->setAutoScroll(false);
819 m_view->hideDropIndicator();
820
821 KItemListWidget *widget = hoveredWidget();
822 if (widget) {
823 widget->setHovered(false);
824 Q_EMIT itemUnhovered(widget->index());
825 }
826 return false;
827 }
828
829 bool KItemListController::dragMoveEvent(QGraphicsSceneDragDropEvent *event, const QTransform &transform)
830 {
831 if (!m_model || !m_view) {
832 return false;
833 }
834
835 QUrl hoveredDir = m_model->directory();
836 KItemListWidget *oldHoveredWidget = hoveredWidget();
837
838 const QPointF pos = transform.map(event->pos());
839 KItemListWidget *newHoveredWidget = widgetForDropPos(pos);
840 int index = -1;
841
842 if (oldHoveredWidget != newHoveredWidget) {
843 m_autoActivationTimer->stop();
844
845 if (oldHoveredWidget) {
846 oldHoveredWidget->setHovered(false);
847 Q_EMIT itemUnhovered(oldHoveredWidget->index());
848 }
849 }
850
851 if (newHoveredWidget) {
852 bool droppingBetweenItems = false;
853 if (m_model->sortRole().isEmpty()) {
854 // The model supports inserting items between other items.
855 droppingBetweenItems = (m_view->showDropIndicator(pos) >= 0);
856 }
857
858 index = newHoveredWidget->index();
859
860 if (m_model->isDir(index)) {
861 hoveredDir = m_model->url(index);
862 }
863
864 if (!droppingBetweenItems) {
865 // Something has been dragged on an item.
866 m_view->hideDropIndicator();
867 if (!newHoveredWidget->isHovered()) {
868 newHoveredWidget->setHovered(true);
869 Q_EMIT itemHovered(index);
870 }
871
872 if (!m_autoActivationTimer->isActive() && m_autoActivationTimer->interval() >= 0 && m_model->canEnterOnHover(index)) {
873 m_autoActivationTimer->setProperty("index", index);
874 m_autoActivationTimer->start();
875 newHoveredWidget->startActivateSoonAnimation(m_autoActivationTimer->remainingTime());
876 }
877
878 } else {
879 m_autoActivationTimer->stop();
880 if (newHoveredWidget && newHoveredWidget->isHovered()) {
881 newHoveredWidget->setHovered(false);
882 Q_EMIT itemUnhovered(index);
883 }
884 }
885 } else {
886 m_view->hideDropIndicator();
887 }
888
889 if (DragAndDropHelper::urlListMatchesUrl(event->mimeData()->urls(), hoveredDir)) {
890 event->setDropAction(Qt::IgnoreAction);
891 event->ignore();
892 } else {
893 if (m_model->supportsDropping(index)) {
894 event->setDropAction(event->proposedAction());
895 event->accept();
896 } else {
897 event->setDropAction(Qt::IgnoreAction);
898 event->ignore();
899 }
900 }
901 return false;
902 }
903
904 bool KItemListController::dropEvent(QGraphicsSceneDragDropEvent *event, const QTransform &transform)
905 {
906 if (!m_view) {
907 return false;
908 }
909
910 m_autoActivationTimer->stop();
911 m_view->setAutoScroll(false);
912
913 const QPointF pos = transform.map(event->pos());
914
915 int dropAboveIndex = -1;
916 if (m_model->sortRole().isEmpty()) {
917 // The model supports inserting of items between other items.
918 dropAboveIndex = m_view->showDropIndicator(pos);
919 }
920
921 if (dropAboveIndex >= 0) {
922 // Something has been dropped between two items.
923 m_view->hideDropIndicator();
924 Q_EMIT aboveItemDropEvent(dropAboveIndex, event);
925 } else if (!event->mimeData()->hasFormat(m_model->blacklistItemDropEventMimeType())) {
926 // Something has been dropped on an item or on an empty part of the view.
927 const KItemListWidget *receivingWidget = widgetForDropPos(pos);
928 if (receivingWidget) {
929 Q_EMIT itemDropEvent(receivingWidget->index(), event);
930 } else {
931 Q_EMIT itemDropEvent(-1, event);
932 }
933 }
934
935 QAccessibleEvent accessibilityEvent(view(), QAccessible::DragDropEnd);
936 QAccessible::updateAccessibility(&accessibilityEvent);
937
938 return true;
939 }
940
941 bool KItemListController::hoverEnterEvent(QGraphicsSceneHoverEvent *event, const QTransform &transform)
942 {
943 Q_UNUSED(event)
944 Q_UNUSED(transform)
945 return false;
946 }
947
948 bool KItemListController::hoverMoveEvent(QGraphicsSceneHoverEvent *event, const QTransform &transform)
949 {
950 Q_UNUSED(transform)
951 if (!m_model || !m_view) {
952 return false;
953 }
954
955 // We identify the widget whose expansionArea had been hovered before this hoverMoveEvent() triggered.
956 // we can't use hoveredWidget() here (it handles the icon+text rect, not the expansion rect)
957 // like hoveredWidget(), we find the hovered widget for the expansion rect
958 const auto visibleItemListWidgets = m_view->visibleItemListWidgets();
959 const auto oldHoveredExpansionWidgetIterator = std::find_if(visibleItemListWidgets.begin(), visibleItemListWidgets.end(), [](auto &widget) {
960 return widget->expansionAreaHovered();
961 });
962 const auto oldHoveredExpansionWidget =
963 oldHoveredExpansionWidgetIterator == visibleItemListWidgets.end() ? std::nullopt : std::make_optional(*oldHoveredExpansionWidgetIterator);
964
965 const auto unhoverOldHoveredWidget = [&]() {
966 if (auto oldHoveredWidget = hoveredWidget(); oldHoveredWidget) {
967 // handle the text+icon one
968 oldHoveredWidget->setHovered(false);
969 Q_EMIT itemUnhovered(oldHoveredWidget->index());
970 }
971 };
972
973 const auto unhoverOldExpansionWidget = [&]() {
974 if (oldHoveredExpansionWidget) {
975 // then the expansion toggle
976 (*oldHoveredExpansionWidget)->setExpansionAreaHovered(false);
977 }
978 };
979
980 const QPointF pos = transform.map(event->pos());
981 if (KItemListWidget *newHoveredWidget = widgetForPos(pos); newHoveredWidget) {
982 // something got hovered, work out which part and set hover for the appropriate widget
983 const auto mappedPos = newHoveredWidget->mapFromItem(m_view, pos);
984 const bool isOnExpansionToggle = newHoveredWidget->expansionToggleRect().contains(mappedPos);
985
986 if (isOnExpansionToggle) {
987 // make sure we unhover the old one first if old!=new
988 if (oldHoveredExpansionWidget && *oldHoveredExpansionWidget != newHoveredWidget) {
989 (*oldHoveredExpansionWidget)->setExpansionAreaHovered(false);
990 }
991 // we also unhover any old icon+text hovers, in case the mouse movement from icon+text to expansion toggle is too fast (i.e. newHoveredWidget is never null between the transition)
992 unhoverOldHoveredWidget();
993
994 newHoveredWidget->setExpansionAreaHovered(true);
995 } else {
996 // make sure we unhover the old one first if old!=new
997 auto oldHoveredWidget = hoveredWidget();
998 if (oldHoveredWidget && oldHoveredWidget != newHoveredWidget) {
999 oldHoveredWidget->setHovered(false);
1000 Q_EMIT itemUnhovered(oldHoveredWidget->index());
1001 }
1002 // we also unhover any old expansion toggle hovers, in case the mouse movement from expansion toggle to icon+text is too fast (i.e. newHoveredWidget is never null between the transition)
1003 unhoverOldExpansionWidget();
1004
1005 const bool isOverIconAndText = newHoveredWidget->iconRect().contains(mappedPos) || newHoveredWidget->textRect().contains(mappedPos);
1006 const bool hasMultipleSelection = m_selectionManager->selectedItems().count() > 1;
1007
1008 if (hasMultipleSelection && !isOverIconAndText) {
1009 // In case we have multiple selections, clicking on any row will deselect the selection.
1010 // So, as a visual cue for signalling that clicking anywhere won't select, but clear current highlights,
1011 // we disable hover of the *row*(i.e. blank space to the right of the icon+text)
1012
1013 // (no-op in this branch for masked hover)
1014 } else {
1015 newHoveredWidget->setHoverPosition(mappedPos);
1016 if (oldHoveredWidget != newHoveredWidget) {
1017 newHoveredWidget->setHovered(true);
1018 Q_EMIT itemHovered(newHoveredWidget->index());
1019 }
1020 }
1021 }
1022 } else {
1023 // unhover any currently hovered expansion and text+icon widgets
1024 unhoverOldHoveredWidget();
1025 unhoverOldExpansionWidget();
1026 }
1027 return false;
1028 }
1029
1030 bool KItemListController::hoverLeaveEvent(QGraphicsSceneHoverEvent *event, const QTransform &transform)
1031 {
1032 Q_UNUSED(event)
1033 Q_UNUSED(transform)
1034
1035 m_mousePress = false;
1036 m_isTouchEvent = false;
1037
1038 if (!m_model || !m_view) {
1039 return false;
1040 }
1041
1042 const auto widgets = m_view->visibleItemListWidgets();
1043 for (KItemListWidget *widget : widgets) {
1044 if (widget->isHovered()) {
1045 widget->setHovered(false);
1046 Q_EMIT itemUnhovered(widget->index());
1047 }
1048 }
1049 return false;
1050 }
1051
1052 bool KItemListController::wheelEvent(QGraphicsSceneWheelEvent *event, const QTransform &transform)
1053 {
1054 Q_UNUSED(event)
1055 Q_UNUSED(transform)
1056 return false;
1057 }
1058
1059 bool KItemListController::resizeEvent(QGraphicsSceneResizeEvent *event, const QTransform &transform)
1060 {
1061 Q_UNUSED(event)
1062 Q_UNUSED(transform)
1063 return false;
1064 }
1065
1066 bool KItemListController::gestureEvent(QGestureEvent *event, const QTransform &transform)
1067 {
1068 if (!m_view) {
1069 return false;
1070 }
1071
1072 //you can touch on different views at the same time, but only one QWidget gets a mousePressEvent
1073 //we use this to get the right QWidget
1074 //the only exception is a tap gesture with state GestureStarted, we need to reset some variable
1075 if (!m_mousePress) {
1076 if (QGesture *tap = event->gesture(Qt::TapGesture)) {
1077 QTapGesture *tapGesture = static_cast<QTapGesture *>(tap);
1078 if (tapGesture->state() == Qt::GestureStarted) {
1079 tapTriggered(tapGesture, transform);
1080 }
1081 }
1082 return false;
1083 }
1084
1085 bool accepted = false;
1086
1087 if (QGesture *tap = event->gesture(Qt::TapGesture)) {
1088 tapTriggered(static_cast<QTapGesture *>(tap), transform);
1089 accepted = true;
1090 }
1091 if (event->gesture(Qt::TapAndHoldGesture)) {
1092 tapAndHoldTriggered(event, transform);
1093 accepted = true;
1094 }
1095 if (event->gesture(Qt::PinchGesture)) {
1096 pinchTriggered(event, transform);
1097 accepted = true;
1098 }
1099 if (event->gesture(m_swipeGesture)) {
1100 swipeTriggered(event, transform);
1101 accepted = true;
1102 }
1103 if (event->gesture(m_twoFingerTapGesture)) {
1104 twoFingerTapTriggered(event, transform);
1105 accepted = true;
1106 }
1107 return accepted;
1108 }
1109
1110 bool KItemListController::touchBeginEvent(QTouchEvent *event, const QTransform &transform)
1111 {
1112 Q_UNUSED(event)
1113 Q_UNUSED(transform)
1114
1115 m_isTouchEvent = true;
1116 return false;
1117 }
1118
1119 void KItemListController::tapTriggered(QTapGesture *tap, const QTransform &transform)
1120 {
1121 static bool scrollerWasActive = false;
1122
1123 if (tap->state() == Qt::GestureStarted) {
1124 m_dragActionOrRightClick = false;
1125 m_isSwipeGesture = false;
1126 m_pinchGestureInProgress = false;
1127 scrollerWasActive = m_scrollerIsScrolling;
1128 }
1129
1130 if (tap->state() == Qt::GestureFinished) {
1131 m_mousePress = false;
1132
1133 //if at the moment of the gesture start the QScroller was active, the user made the tap
1134 //to stop the QScroller and not to tap on an item
1135 if (scrollerWasActive) {
1136 return;
1137 }
1138
1139 if (m_view->m_tapAndHoldIndicator->isActive()) {
1140 m_view->m_tapAndHoldIndicator->setActive(false);
1141 }
1142
1143 const QPointF pressedMousePos = transform.map(tap->position());
1144 m_pressedIndex = m_view->itemAt(pressedMousePos);
1145 if (m_dragActionOrRightClick) {
1146 m_dragActionOrRightClick = false;
1147 } else {
1148 onPress(tap->position().toPoint(), Qt::NoModifier, Qt::LeftButton);
1149 onRelease(transform.map(tap->position()), Qt::NoModifier, Qt::LeftButton, true);
1150 }
1151 m_isTouchEvent = false;
1152 }
1153 }
1154
1155 void KItemListController::tapAndHoldTriggered(QGestureEvent *event, const QTransform &transform)
1156 {
1157 //the Qt TabAndHold gesture is triggerable with a mouse click, we don't want this
1158 if (!m_isTouchEvent) {
1159 return;
1160 }
1161
1162 const QTapAndHoldGesture *tap = static_cast<QTapAndHoldGesture *>(event->gesture(Qt::TapAndHoldGesture));
1163 if (tap->state() == Qt::GestureFinished) {
1164 //if a pinch gesture is in progress we don't want a TabAndHold gesture
1165 if (m_pinchGestureInProgress) {
1166 return;
1167 }
1168 const QPointF pressedMousePos = transform.map(event->mapToGraphicsScene(tap->position()));
1169 m_pressedIndex = m_view->itemAt(pressedMousePos);
1170 if (m_pressedIndex.has_value()) {
1171 if (!m_selectionManager->isSelected(m_pressedIndex.value())) {
1172 m_selectionManager->clearSelection();
1173 m_selectionManager->setSelected(m_pressedIndex.value());
1174 }
1175 if (!m_selectionMode) {
1176 Q_EMIT selectionModeChangeRequested(true);
1177 }
1178 } else {
1179 m_selectionManager->clearSelection();
1180 startRubberBand();
1181 }
1182
1183 Q_EMIT scrollerStop();
1184
1185 m_view->m_tapAndHoldIndicator->setStartPosition(pressedMousePos);
1186 m_view->m_tapAndHoldIndicator->setActive(true);
1187
1188 m_dragActionOrRightClick = true;
1189 }
1190 }
1191
1192 void KItemListController::pinchTriggered(QGestureEvent *event, const QTransform &transform)
1193 {
1194 Q_UNUSED(transform)
1195
1196 const QPinchGesture *pinch = static_cast<QPinchGesture *>(event->gesture(Qt::PinchGesture));
1197 const qreal sensitivityModifier = 0.2;
1198 static qreal counter = 0;
1199
1200 if (pinch->state() == Qt::GestureStarted) {
1201 m_pinchGestureInProgress = true;
1202 counter = 0;
1203 }
1204 if (pinch->state() == Qt::GestureUpdated) {
1205 //if a swipe gesture was recognized or in progress, we don't want a pinch gesture to change the zoom
1206 if (m_isSwipeGesture) {
1207 return;
1208 }
1209 counter = counter + (pinch->scaleFactor() - 1);
1210 if (counter >= sensitivityModifier) {
1211 Q_EMIT increaseZoom();
1212 counter = 0;
1213 } else if (counter <= -sensitivityModifier) {
1214 Q_EMIT decreaseZoom();
1215 counter = 0;
1216 }
1217 }
1218 }
1219
1220 void KItemListController::swipeTriggered(QGestureEvent *event, const QTransform &transform)
1221 {
1222 Q_UNUSED(transform)
1223
1224 const KTwoFingerSwipe *swipe = static_cast<KTwoFingerSwipe *>(event->gesture(m_swipeGesture));
1225
1226 if (!swipe) {
1227 return;
1228 }
1229 if (swipe->state() == Qt::GestureStarted) {
1230 m_isSwipeGesture = true;
1231 }
1232
1233 if (swipe->state() == Qt::GestureCanceled) {
1234 m_isSwipeGesture = false;
1235 }
1236
1237 if (swipe->state() == Qt::GestureFinished) {
1238 Q_EMIT scrollerStop();
1239
1240 if (swipe->swipeAngle() <= 20 || swipe->swipeAngle() >= 340) {
1241 Q_EMIT mouseButtonPressed(m_pressedIndex.value_or(-1), Qt::BackButton);
1242 } else if (swipe->swipeAngle() <= 200 && swipe->swipeAngle() >= 160) {
1243 Q_EMIT mouseButtonPressed(m_pressedIndex.value_or(-1), Qt::ForwardButton);
1244 } else if (swipe->swipeAngle() <= 110 && swipe->swipeAngle() >= 60) {
1245 Q_EMIT swipeUp();
1246 }
1247 m_isSwipeGesture = true;
1248 }
1249 }
1250
1251 void KItemListController::twoFingerTapTriggered(QGestureEvent *event, const QTransform &transform)
1252 {
1253 const KTwoFingerTap *twoTap = static_cast<KTwoFingerTap *>(event->gesture(m_twoFingerTapGesture));
1254
1255 if (!twoTap) {
1256 return;
1257 }
1258
1259 if (twoTap->state() == Qt::GestureStarted) {
1260 const QPointF pressedMousePos = transform.map(twoTap->pos());
1261 m_pressedIndex = m_view->itemAt(pressedMousePos);
1262 if (m_pressedIndex.has_value()) {
1263 onPress(twoTap->pos().toPoint(), Qt::ControlModifier, Qt::LeftButton);
1264 onRelease(transform.map(twoTap->pos()), Qt::ControlModifier, Qt::LeftButton, false);
1265 }
1266 }
1267 }
1268
1269 bool KItemListController::processEvent(QEvent *event, const QTransform &transform)
1270 {
1271 if (!event) {
1272 return false;
1273 }
1274
1275 switch (event->type()) {
1276 case QEvent::KeyPress:
1277 return keyPressEvent(static_cast<QKeyEvent *>(event));
1278 case QEvent::InputMethod:
1279 return inputMethodEvent(static_cast<QInputMethodEvent *>(event));
1280 case QEvent::GraphicsSceneMousePress:
1281 return mousePressEvent(static_cast<QGraphicsSceneMouseEvent *>(event), QTransform());
1282 case QEvent::GraphicsSceneMouseMove:
1283 return mouseMoveEvent(static_cast<QGraphicsSceneMouseEvent *>(event), QTransform());
1284 case QEvent::GraphicsSceneMouseRelease:
1285 return mouseReleaseEvent(static_cast<QGraphicsSceneMouseEvent *>(event), QTransform());
1286 case QEvent::GraphicsSceneMouseDoubleClick:
1287 return mouseDoubleClickEvent(static_cast<QGraphicsSceneMouseEvent *>(event), QTransform());
1288 case QEvent::ContextMenu:
1289 return contextMenuEvent(static_cast<QContextMenuEvent *>(event));
1290 case QEvent::GraphicsSceneWheel:
1291 return wheelEvent(static_cast<QGraphicsSceneWheelEvent *>(event), QTransform());
1292 case QEvent::GraphicsSceneDragEnter:
1293 return dragEnterEvent(static_cast<QGraphicsSceneDragDropEvent *>(event), QTransform());
1294 case QEvent::GraphicsSceneDragLeave:
1295 return dragLeaveEvent(static_cast<QGraphicsSceneDragDropEvent *>(event), QTransform());
1296 case QEvent::GraphicsSceneDragMove:
1297 return dragMoveEvent(static_cast<QGraphicsSceneDragDropEvent *>(event), QTransform());
1298 case QEvent::GraphicsSceneDrop:
1299 return dropEvent(static_cast<QGraphicsSceneDragDropEvent *>(event), QTransform());
1300 case QEvent::GraphicsSceneHoverEnter:
1301 return hoverEnterEvent(static_cast<QGraphicsSceneHoverEvent *>(event), QTransform());
1302 case QEvent::GraphicsSceneHoverMove:
1303 return hoverMoveEvent(static_cast<QGraphicsSceneHoverEvent *>(event), QTransform());
1304 case QEvent::GraphicsSceneHoverLeave:
1305 return hoverLeaveEvent(static_cast<QGraphicsSceneHoverEvent *>(event), QTransform());
1306 case QEvent::GraphicsSceneResize:
1307 return resizeEvent(static_cast<QGraphicsSceneResizeEvent *>(event), transform);
1308 case QEvent::Gesture:
1309 return gestureEvent(static_cast<QGestureEvent *>(event), transform);
1310 case QEvent::TouchBegin:
1311 return touchBeginEvent(static_cast<QTouchEvent *>(event), transform);
1312 default:
1313 break;
1314 }
1315
1316 return false;
1317 }
1318
1319 void KItemListController::slotViewScrollOffsetChanged(qreal current, qreal previous)
1320 {
1321 if (!m_view) {
1322 return;
1323 }
1324
1325 KItemListRubberBand *rubberBand = m_view->rubberBand();
1326 if (rubberBand->isActive()) {
1327 const qreal diff = current - previous;
1328 // TODO: Ideally just QCursor::pos() should be used as
1329 // new end-position but it seems there is no easy way
1330 // to have something like QWidget::mapFromGlobal() for QGraphicsWidget
1331 // (... or I just missed an easy way to do the mapping)
1332 QPointF endPos = rubberBand->endPosition();
1333 if (m_view->scrollOrientation() == Qt::Vertical) {
1334 endPos.ry() += diff;
1335 } else {
1336 endPos.rx() += diff;
1337 }
1338
1339 rubberBand->setEndPosition(endPos);
1340 }
1341 }
1342
1343 void KItemListController::slotRubberBandChanged()
1344 {
1345 if (!m_view || !m_model || m_model->count() <= 0) {
1346 return;
1347 }
1348
1349 const KItemListRubberBand *rubberBand = m_view->rubberBand();
1350 const QPointF startPos = rubberBand->startPosition();
1351 const QPointF endPos = rubberBand->endPosition();
1352 QRectF rubberBandRect = QRectF(startPos, endPos).normalized();
1353
1354 const bool scrollVertical = (m_view->scrollOrientation() == Qt::Vertical);
1355 if (scrollVertical) {
1356 rubberBandRect.translate(0, -m_view->scrollOffset());
1357 } else {
1358 rubberBandRect.translate(-m_view->scrollOffset(), 0);
1359 }
1360
1361 if (!m_oldSelection.isEmpty()) {
1362 // Clear the old selection that was available before the rubberband has
1363 // been activated in case if no Shift- or Control-key are pressed
1364 const bool shiftOrControlPressed = QApplication::keyboardModifiers() & Qt::ShiftModifier || QApplication::keyboardModifiers() & Qt::ControlModifier;
1365 if (!shiftOrControlPressed && !m_selectionMode) {
1366 m_oldSelection.clear();
1367 }
1368 }
1369
1370 KItemSet selectedItems;
1371
1372 // Select all visible items that intersect with the rubberband
1373 const auto widgets = m_view->visibleItemListWidgets();
1374 for (const KItemListWidget *widget : widgets) {
1375 const int index = widget->index();
1376
1377 const QRectF widgetRect = m_view->itemRect(index);
1378 if (widgetRect.intersects(rubberBandRect)) {
1379 // Select the full row intersecting with the rubberband rectangle
1380 const QRectF selectionRect = widget->selectionRect().translated(widgetRect.topLeft());
1381 const QRectF iconRect = widget->iconRect().translated(widgetRect.topLeft());
1382 if (selectionRect.intersects(rubberBandRect) || iconRect.intersects(rubberBandRect)) {
1383 selectedItems.insert(index);
1384 }
1385 }
1386 }
1387
1388 // Select all invisible items that intersect with the rubberband. Instead of
1389 // iterating all items only the area which might be touched by the rubberband
1390 // will be checked.
1391 const bool increaseIndex = scrollVertical ? startPos.y() > endPos.y() : startPos.x() > endPos.x();
1392
1393 int index = increaseIndex ? m_view->lastVisibleIndex() + 1 : m_view->firstVisibleIndex() - 1;
1394 bool selectionFinished = false;
1395 do {
1396 const QRectF widgetRect = m_view->itemRect(index);
1397 if (widgetRect.intersects(rubberBandRect)) {
1398 selectedItems.insert(index);
1399 }
1400
1401 if (increaseIndex) {
1402 ++index;
1403 selectionFinished = (index >= m_model->count()) || (scrollVertical && widgetRect.top() > rubberBandRect.bottom())
1404 || (!scrollVertical && widgetRect.left() > rubberBandRect.right());
1405 } else {
1406 --index;
1407 selectionFinished = (index < 0) || (scrollVertical && widgetRect.bottom() < rubberBandRect.top())
1408 || (!scrollVertical && widgetRect.right() < rubberBandRect.left());
1409 }
1410 } while (!selectionFinished);
1411
1412 if ((QApplication::keyboardModifiers() & Qt::ControlModifier) || m_selectionMode) {
1413 // If Control is pressed, the selection state of all items in the rubberband is toggled.
1414 // Therefore, the new selection contains:
1415 // 1. All previously selected items which are not inside the rubberband, and
1416 // 2. all items inside the rubberband which have not been selected previously.
1417 m_selectionManager->setSelectedItems(m_oldSelection ^ selectedItems);
1418 } else {
1419 m_selectionManager->setSelectedItems(selectedItems + m_oldSelection);
1420 }
1421 }
1422
1423 void KItemListController::startDragging()
1424 {
1425 if (!m_view || !m_model) {
1426 return;
1427 }
1428
1429 const KItemSet selectedItems = m_selectionManager->selectedItems();
1430 if (selectedItems.isEmpty()) {
1431 return;
1432 }
1433
1434 QMimeData *data = m_model->createMimeData(selectedItems);
1435 if (!data) {
1436 return;
1437 }
1438 KUrlMimeData::exportUrlsToPortal(data);
1439
1440 // The created drag object will be owned and deleted
1441 // by QApplication::activeWindow().
1442 QDrag *drag = new QDrag(QApplication::activeWindow());
1443 drag->setMimeData(data);
1444
1445 const QPixmap pixmap = m_view->createDragPixmap(selectedItems);
1446 drag->setPixmap(pixmap);
1447
1448 const QPoint hotSpot((pixmap.width() / pixmap.devicePixelRatio()) / 2, 0);
1449 drag->setHotSpot(hotSpot);
1450
1451 drag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction);
1452
1453 QAccessibleEvent accessibilityEvent(view(), QAccessible::DragDropStart);
1454 QAccessible::updateAccessibility(&accessibilityEvent);
1455 }
1456
1457 KItemListWidget *KItemListController::hoveredWidget() const
1458 {
1459 Q_ASSERT(m_view);
1460
1461 const auto widgets = m_view->visibleItemListWidgets();
1462 for (KItemListWidget *widget : widgets) {
1463 if (widget->isHovered()) {
1464 return widget;
1465 }
1466 }
1467
1468 return nullptr;
1469 }
1470
1471 KItemListWidget *KItemListController::widgetForPos(const QPointF &pos) const
1472 {
1473 Q_ASSERT(m_view);
1474
1475 if (m_view->headerBoundaries().contains(pos)) {
1476 return nullptr;
1477 }
1478
1479 const auto widgets = m_view->visibleItemListWidgets();
1480 for (KItemListWidget *widget : widgets) {
1481 const QPointF mappedPos = widget->mapFromItem(m_view, pos);
1482 if (widget->contains(mappedPos) || widget->selectionRect().contains(mappedPos)) {
1483 return widget;
1484 }
1485 }
1486
1487 return nullptr;
1488 }
1489
1490 KItemListWidget *KItemListController::widgetForDropPos(const QPointF &pos) const
1491 {
1492 Q_ASSERT(m_view);
1493
1494 if (m_view->headerBoundaries().contains(pos)) {
1495 return nullptr;
1496 }
1497
1498 const auto widgets = m_view->visibleItemListWidgets();
1499 for (KItemListWidget *widget : widgets) {
1500 const QPointF mappedPos = widget->mapFromItem(m_view, pos);
1501 if (widget->contains(mappedPos)) {
1502 return widget;
1503 }
1504 }
1505
1506 return nullptr;
1507 }
1508
1509 void KItemListController::updateKeyboardAnchor()
1510 {
1511 const bool validAnchor =
1512 m_keyboardAnchorIndex >= 0 && m_keyboardAnchorIndex < m_model->count() && keyboardAnchorPos(m_keyboardAnchorIndex) == m_keyboardAnchorPos;
1513 if (!validAnchor) {
1514 const int index = m_selectionManager->currentItem();
1515 m_keyboardAnchorIndex = index;
1516 m_keyboardAnchorPos = keyboardAnchorPos(index);
1517 }
1518 }
1519
1520 int KItemListController::nextRowIndex(int index) const
1521 {
1522 if (m_keyboardAnchorIndex < 0) {
1523 return index;
1524 }
1525
1526 const int maxIndex = m_model->count() - 1;
1527 if (index == maxIndex) {
1528 return index;
1529 }
1530
1531 const bool reversed = m_view->layoutDirection() == Qt::RightToLeft && m_view->scrollOrientation() == Qt::Vertical;
1532
1533 // Calculate the index of the last column inside the row of the current index
1534 int lastColumnIndex = index;
1535 while ((!reversed && keyboardAnchorPos(lastColumnIndex + 1) > keyboardAnchorPos(lastColumnIndex))
1536 || (reversed && keyboardAnchorPos(lastColumnIndex + 1) < keyboardAnchorPos(lastColumnIndex))) {
1537 ++lastColumnIndex;
1538 if (lastColumnIndex >= maxIndex) {
1539 return index;
1540 }
1541 }
1542
1543 // Based on the last column index go to the next row and calculate the nearest index
1544 // that is below the current index
1545 int nextRowIndex = lastColumnIndex + 1;
1546 int searchIndex = nextRowIndex;
1547 qreal minDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(nextRowIndex));
1548 while (searchIndex < maxIndex
1549 && ((!reversed && keyboardAnchorPos(searchIndex + 1) > keyboardAnchorPos(searchIndex))
1550 || (reversed && keyboardAnchorPos(searchIndex + 1) < keyboardAnchorPos(searchIndex)))) {
1551 ++searchIndex;
1552 const qreal searchDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(searchIndex));
1553 if (searchDiff < minDiff) {
1554 minDiff = searchDiff;
1555 nextRowIndex = searchIndex;
1556 }
1557 }
1558
1559 return nextRowIndex;
1560 }
1561
1562 int KItemListController::previousRowIndex(int index) const
1563 {
1564 if (m_keyboardAnchorIndex < 0 || index == 0) {
1565 return index;
1566 }
1567
1568 const bool reversed = m_view->layoutDirection() == Qt::RightToLeft && m_view->scrollOrientation() == Qt::Vertical;
1569
1570 // Calculate the index of the first column inside the row of the current index
1571 int firstColumnIndex = index;
1572 while ((!reversed && keyboardAnchorPos(firstColumnIndex - 1) < keyboardAnchorPos(firstColumnIndex))
1573 || (reversed && keyboardAnchorPos(firstColumnIndex - 1) > keyboardAnchorPos(firstColumnIndex))) {
1574 --firstColumnIndex;
1575 if (firstColumnIndex <= 0) {
1576 return index;
1577 }
1578 }
1579
1580 // Based on the first column index go to the previous row and calculate the nearest index
1581 // that is above the current index
1582 int previousRowIndex = firstColumnIndex - 1;
1583 int searchIndex = previousRowIndex;
1584 qreal minDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(previousRowIndex));
1585 while (searchIndex > 0
1586 && ((!reversed && keyboardAnchorPos(searchIndex - 1) < keyboardAnchorPos(searchIndex))
1587 || (reversed && keyboardAnchorPos(searchIndex - 1) > keyboardAnchorPos(searchIndex)))) {
1588 --searchIndex;
1589 const qreal searchDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(searchIndex));
1590 if (searchDiff < minDiff) {
1591 minDiff = searchDiff;
1592 previousRowIndex = searchIndex;
1593 }
1594 }
1595
1596 return previousRowIndex;
1597 }
1598
1599 qreal KItemListController::keyboardAnchorPos(int index) const
1600 {
1601 const QRectF itemRect = m_view->itemRect(index);
1602 if (!itemRect.isEmpty()) {
1603 return (m_view->scrollOrientation() == Qt::Vertical) ? itemRect.x() : itemRect.y();
1604 }
1605
1606 return 0;
1607 }
1608
1609 void KItemListController::updateExtendedSelectionRegion()
1610 {
1611 if (m_view) {
1612 const bool extend = (m_selectionBehavior != MultiSelection);
1613 KItemListStyleOption option = m_view->styleOption();
1614 if (option.extendedSelectionRegion != extend) {
1615 option.extendedSelectionRegion = extend;
1616 m_view->setStyleOption(option);
1617 }
1618 }
1619 }
1620
1621 bool KItemListController::onPress(const QPointF &pos, const Qt::KeyboardModifiers modifiers, const Qt::MouseButtons buttons)
1622 {
1623 Q_EMIT mouseButtonPressed(m_pressedIndex.value_or(-1), buttons);
1624
1625 if (buttons & (Qt::BackButton | Qt::ForwardButton)) {
1626 // Do not select items when clicking the back/forward buttons, see
1627 // https://bugs.kde.org/show_bug.cgi?id=327412.
1628 return true;
1629 }
1630
1631 const QPointF pressedMousePos = m_view->transform().map(pos);
1632
1633 if (m_view->isAboveExpansionToggle(m_pressedIndex.value_or(-1), pressedMousePos)) {
1634 m_selectionManager->endAnchoredSelection();
1635 m_selectionManager->setCurrentItem(m_pressedIndex.value());
1636 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1637 return true;
1638 }
1639
1640 m_selectionTogglePressed = m_view->isAboveSelectionToggle(m_pressedIndex.value_or(-1), pressedMousePos);
1641 if (m_selectionTogglePressed) {
1642 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
1643 // The previous anchored selection has been finished already in
1644 // KItemListSelectionManager::setSelected(). We can safely change
1645 // the current item and start a new anchored selection now.
1646 m_selectionManager->setCurrentItem(m_pressedIndex.value());
1647 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1648 return true;
1649 }
1650
1651 const bool shiftPressed = modifiers & Qt::ShiftModifier;
1652 const bool controlPressed = (modifiers & Qt::ControlModifier) || m_selectionMode; // Keeping selectionMode similar to pressing control will hopefully
1653 // simplify the overall logic and possibilities both for users and devs.
1654 const bool leftClick = buttons & Qt::LeftButton;
1655 const bool rightClick = buttons & Qt::RightButton;
1656
1657 // The previous selection is cleared if either
1658 // 1. The selection mode is SingleSelection, or
1659 // 2. the selection mode is MultiSelection, and *none* of the following conditions are met:
1660 // a) Shift or Control are pressed.
1661 // b) The clicked item is selected already. In that case, the user might want to:
1662 // - start dragging multiple items, or
1663 // - open the context menu and perform an action for all selected items.
1664 const bool shiftOrControlPressed = shiftPressed || controlPressed;
1665 const bool pressedItemAlreadySelected = m_pressedIndex.has_value() && m_selectionManager->isSelected(m_pressedIndex.value());
1666 const bool clearSelection = m_selectionBehavior == SingleSelection || (!shiftOrControlPressed && !pressedItemAlreadySelected);
1667
1668 // When this method returns false, a rubberBand selection is created using KItemListController::startRubberBand via the caller.
1669 if (clearSelection) {
1670 const int selectedItemsCount = m_selectionManager->selectedItems().count();
1671 m_selectionManager->clearSelection();
1672 // clear and bail when we got an existing multi-selection
1673 if (selectedItemsCount > 1 && m_pressedIndex.has_value()) {
1674 const auto row = m_view->m_visibleItems.value(m_pressedIndex.value());
1675 const auto mappedPos = row->mapFromItem(m_view, pos);
1676 if (pressedItemAlreadySelected || row->iconRect().contains(mappedPos) || row->textRect().contains(mappedPos)) {
1677 // we are indeed inside the text/icon rect, keep m_pressedIndex what it is
1678 // and short-circuit for single-click activation (it will then propagate to onRelease and activate the item)
1679 // or we just keep going for double-click activation
1680 if (m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced) {
1681 if (!pressedItemAlreadySelected) {
1682 // An unselected item was clicked directly while deselecting multiple other items so we mark it "current".
1683 m_selectionManager->setCurrentItem(m_pressedIndex.value());
1684 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1685 if (!leftClick) {
1686 // We select the item here because this press is not meant to directly activate the item.
1687 // We do not want to select items unless the user wants to edit them.
1688 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
1689 }
1690 }
1691 return true; // event handled, don't create rubber band
1692 }
1693 } else {
1694 // we're not inside the text/icon rect, as we've already cleared the selection
1695 // we can just stop here and make sure handlers down the line (i.e. onRelease) don't activate
1696 m_pressedIndex.reset();
1697 // we don't stop event propagation and proceed to create a rubber band and let onRelease
1698 // decide (based on m_pressedIndex) whether we're in a drag (drag => new rubber band, click => don't select the item)
1699 return false;
1700 }
1701 }
1702 } else if (pressedItemAlreadySelected && !shiftOrControlPressed && leftClick) {
1703 // The user might want to start dragging multiple items, but if he clicks the item
1704 // in order to trigger it instead, the other selected items must be deselected.
1705 // However, we do not know yet what the user is going to do.
1706 // -> remember that the user pressed an item which had been selected already and
1707 // clear the selection in mouseReleaseEvent(), unless the items are dragged.
1708 m_clearSelectionIfItemsAreNotDragged = true;
1709
1710 if (m_selectionManager->selectedItems().count() == 1 && m_view->isAboveText(m_pressedIndex.value_or(-1), pressedMousePos)) {
1711 Q_EMIT selectedItemTextPressed(m_pressedIndex.value_or(-1));
1712 }
1713 }
1714
1715 if (!shiftPressed) {
1716 // Finish the anchored selection before the current index is changed
1717 m_selectionManager->endAnchoredSelection();
1718 }
1719
1720 if (rightClick) {
1721 // Stop rubber band from persisting after right-clicks
1722 KItemListRubberBand *rubberBand = m_view->rubberBand();
1723 if (rubberBand->isActive()) {
1724 disconnect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged);
1725 rubberBand->setActive(false);
1726 m_view->setAutoScroll(false);
1727 }
1728
1729 if (!m_pressedIndex.has_value()) {
1730 // We have a right-click in an empty region, don't create rubber band.
1731 return true;
1732 }
1733 }
1734
1735 if (m_pressedIndex.has_value()) {
1736 // The hover highlight area of an item is being pressed.
1737 const auto row = m_view->m_visibleItems.value(m_pressedIndex.value()); // anything outside of row.contains() will be the empty region of the row rect
1738 const bool hitTargetIsRowEmptyRegion = !row->contains(row->mapFromItem(m_view, pos));
1739 // again, when this method returns false, a rubberBand selection is created as the event is not consumed;
1740 // createRubberBand here tells us whether to return true or false.
1741 bool createRubberBand = (hitTargetIsRowEmptyRegion && m_selectionManager->selectedItems().isEmpty());
1742
1743 if (rightClick && hitTargetIsRowEmptyRegion) {
1744 // We have a right click outside the icon and text rect but within the hover highlight area.
1745 // We don't want items to get selected through this, so we return now.
1746 return true;
1747 }
1748
1749 m_selectionManager->setCurrentItem(m_pressedIndex.value());
1750
1751 switch (m_selectionBehavior) {
1752 case NoSelection:
1753 break;
1754
1755 case SingleSelection:
1756 if (!leftClick || shiftOrControlPressed
1757 || (!m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) && !m_singleClickActivationEnforced)) {
1758 m_selectionManager->setSelected(m_pressedIndex.value());
1759 }
1760 break;
1761
1762 case MultiSelection:
1763 if (controlPressed && !shiftPressed && leftClick) {
1764 // A left mouse button press is happening on an item while control is pressed. This either means a user wants to:
1765 // - toggle the selection of item(s) or
1766 // - they want to begin a drag on the item(s) to copy them.
1767 // We rule out the latter, if the item is not clicked directly and was unselected previously.
1768 const auto row = m_view->m_visibleItems.value(m_pressedIndex.value());
1769 const auto mappedPos = row->mapFromItem(m_view, pos);
1770 if (!row->iconRect().contains(mappedPos) && !row->textRect().contains(mappedPos) && !pressedItemAlreadySelected) {
1771 createRubberBand = true;
1772 } else {
1773 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
1774 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1775 createRubberBand = false; // multi selection, don't propagate any further
1776 // This will be the start of an item drag-to-copy operation if the user now moves the mouse before releasing the mouse button.
1777 }
1778 } else if (!shiftPressed || !m_selectionManager->isAnchoredSelectionActive()) {
1779 // Select the pressed item and start a new anchored selection
1780 if (!leftClick || shiftOrControlPressed
1781 || (!m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) && !m_singleClickActivationEnforced)) {
1782 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Select);
1783 }
1784 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1785 }
1786 break;
1787
1788 default:
1789 Q_ASSERT(false);
1790 break;
1791 }
1792
1793 return !createRubberBand;
1794 }
1795
1796 return false;
1797 }
1798
1799 bool KItemListController::onRelease(const QPointF &pos, const Qt::KeyboardModifiers modifiers, const Qt::MouseButtons buttons, bool touch)
1800 {
1801 const QPointF pressedMousePos = pos;
1802 const bool isAboveSelectionToggle = m_view->isAboveSelectionToggle(m_pressedIndex.value_or(-1), pressedMousePos);
1803 if (isAboveSelectionToggle) {
1804 m_selectionTogglePressed = false;
1805 return true;
1806 }
1807
1808 if (!isAboveSelectionToggle && m_selectionTogglePressed) {
1809 m_selectionManager->setSelected(m_pressedIndex.value_or(-1), 1, KItemListSelectionManager::Toggle);
1810 m_selectionTogglePressed = false;
1811 return true;
1812 }
1813
1814 const bool controlPressed = modifiers & Qt::ControlModifier;
1815 const bool shiftOrControlPressed = modifiers & Qt::ShiftModifier || controlPressed;
1816
1817 const std::optional<int> index = m_view->itemAt(pos);
1818
1819 KItemListRubberBand *rubberBand = m_view->rubberBand();
1820 bool rubberBandRelease = false;
1821 if (rubberBand->isActive()) {
1822 disconnect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged);
1823 rubberBand->setActive(false);
1824 m_oldSelection.clear();
1825 m_view->setAutoScroll(false);
1826 rubberBandRelease = true;
1827 // We check for actual rubber band drag here: if delta between start and end is less than drag threshold,
1828 // then we have a single click on one of the rows
1829 if ((rubberBand->endPosition() - rubberBand->startPosition()).manhattanLength() < QApplication::startDragDistance()) {
1830 rubberBandRelease = false; // since we're only selecting, unmark rubber band release flag
1831 // m_pressedIndex will have no value if we came from a multi-selection clearing onPress
1832 // in that case, we don't select anything
1833 if (index.has_value() && m_pressedIndex.has_value()) {
1834 if (controlPressed && m_selectionBehavior == MultiSelection) {
1835 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
1836 } else {
1837 m_selectionManager->setSelected(index.value());
1838 }
1839 if (!m_selectionManager->isAnchoredSelectionActive()) {
1840 m_selectionManager->beginAnchoredSelection(index.value());
1841 }
1842 }
1843 }
1844 }
1845
1846 if (index.has_value() && index == m_pressedIndex) {
1847 // The release event is done above the same item as the press event
1848
1849 if (m_clearSelectionIfItemsAreNotDragged) {
1850 // A selected item has been clicked, but no drag operation has been started
1851 // -> clear the rest of the selection.
1852 m_selectionManager->clearSelection();
1853 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Select);
1854 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1855 }
1856
1857 if (buttons & Qt::LeftButton) {
1858 bool emitItemActivated = true;
1859 if (m_view->isAboveExpansionToggle(index.value(), pos)) {
1860 const bool expanded = m_model->isExpanded(index.value());
1861 m_model->setExpanded(index.value(), !expanded);
1862
1863 Q_EMIT itemExpansionToggleClicked(index.value());
1864 emitItemActivated = false;
1865 } else if (shiftOrControlPressed && m_selectionBehavior != SingleSelection) {
1866 // The mouse click should only update the selection, not trigger the item, except when
1867 // we are in single selection mode
1868 emitItemActivated = false;
1869 } else {
1870 const bool singleClickActivation = m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced;
1871 if (!singleClickActivation) {
1872 emitItemActivated = touch && !m_selectionMode;
1873 } else {
1874 // activate on single click only if we didn't come from a rubber band release
1875 emitItemActivated = !rubberBandRelease;
1876 }
1877 }
1878 if (emitItemActivated) {
1879 Q_EMIT itemActivated(index.value());
1880 }
1881 } else if (buttons & Qt::MiddleButton) {
1882 Q_EMIT itemMiddleClicked(index.value());
1883 }
1884 }
1885
1886 m_pressedMouseGlobalPos = QPointF();
1887 m_pressedIndex = std::nullopt;
1888 m_clearSelectionIfItemsAreNotDragged = false;
1889 return false;
1890 }
1891
1892 void KItemListController::startRubberBand()
1893 {
1894 if (m_selectionBehavior == MultiSelection) {
1895 QPoint startPos = m_view->transform().map(m_view->scene()->views().first()->mapFromGlobal(m_pressedMouseGlobalPos.toPoint()));
1896 if (m_view->scrollOrientation() == Qt::Vertical) {
1897 startPos.ry() += m_view->scrollOffset();
1898 } else {
1899 startPos.rx() += m_view->scrollOffset();
1900 }
1901
1902 m_oldSelection = m_selectionManager->selectedItems();
1903 KItemListRubberBand *rubberBand = m_view->rubberBand();
1904 rubberBand->setStartPosition(startPos);
1905 rubberBand->setEndPosition(startPos);
1906 rubberBand->setActive(true);
1907 connect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged);
1908 m_view->setAutoScroll(true);
1909 }
1910 }
1911
1912 void KItemListController::slotStateChanged(QScroller::State newState)
1913 {
1914 if (newState == QScroller::Scrolling) {
1915 m_scrollerIsScrolling = true;
1916 } else if (newState == QScroller::Inactive) {
1917 m_scrollerIsScrolling = false;
1918 }
1919 }
1920
1921 #include "moc_kitemlistcontroller.cpp"