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