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