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