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