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