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