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