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