]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kitemlistcontroller.cpp
Prevent dragging on non-writable directories
[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_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 int index = -1;
742
743 if (oldHoveredWidget != newHoveredWidget) {
744 m_autoActivationTimer->stop();
745
746 if (oldHoveredWidget) {
747 oldHoveredWidget->setHovered(false);
748 Q_EMIT itemUnhovered(oldHoveredWidget->index());
749 }
750 }
751
752 if (newHoveredWidget) {
753 bool droppingBetweenItems = false;
754 if (m_model->sortRole().isEmpty()) {
755 // The model supports inserting items between other items.
756 droppingBetweenItems = (m_view->showDropIndicator(pos) >= 0);
757 }
758
759 index = newHoveredWidget->index();
760
761 if (m_model->isDir(index)) {
762 hoveredDir = m_model->url(index);
763 }
764
765 if (!droppingBetweenItems) {
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 } else {
778 m_autoActivationTimer->stop();
779 if (newHoveredWidget && newHoveredWidget->isHovered()) {
780 newHoveredWidget->setHovered(false);
781 Q_EMIT itemUnhovered(index);
782 }
783 }
784 } else {
785 m_view->hideDropIndicator();
786 }
787
788 if (DragAndDropHelper::urlListMatchesUrl(event->mimeData()->urls(), hoveredDir)) {
789 event->setDropAction(Qt::IgnoreAction);
790 event->ignore();
791 } else {
792 if (m_model->supportsDropping(index)) {
793 event->setDropAction(event->proposedAction());
794 event->accept();
795 } else {
796 event->setDropAction(Qt::IgnoreAction);
797 event->ignore();
798 }
799 }
800 return false;
801 }
802
803 bool KItemListController::dropEvent(QGraphicsSceneDragDropEvent *event, const QTransform &transform)
804 {
805 if (!m_view) {
806 return false;
807 }
808
809 m_autoActivationTimer->stop();
810 m_view->setAutoScroll(false);
811
812 const QPointF pos = transform.map(event->pos());
813
814 int dropAboveIndex = -1;
815 if (m_model->sortRole().isEmpty()) {
816 // The model supports inserting of items between other items.
817 dropAboveIndex = m_view->showDropIndicator(pos);
818 }
819
820 if (dropAboveIndex >= 0) {
821 // Something has been dropped between two items.
822 m_view->hideDropIndicator();
823 Q_EMIT aboveItemDropEvent(dropAboveIndex, event);
824 } else if (!event->mimeData()->hasFormat(m_model->blacklistItemDropEventMimeType())) {
825 // Something has been dropped on an item or on an empty part of the view.
826 const KItemListWidget *receivingWidget = widgetForDropPos(pos);
827 if (receivingWidget) {
828 Q_EMIT itemDropEvent(receivingWidget->index(), event);
829 } else {
830 Q_EMIT itemDropEvent(-1, event);
831 }
832 }
833
834 QAccessibleEvent accessibilityEvent(view(), QAccessible::DragDropEnd);
835 QAccessible::updateAccessibility(&accessibilityEvent);
836
837 return true;
838 }
839
840 bool KItemListController::hoverEnterEvent(QGraphicsSceneHoverEvent *event, const QTransform &transform)
841 {
842 Q_UNUSED(event)
843 Q_UNUSED(transform)
844 return false;
845 }
846
847 bool KItemListController::hoverMoveEvent(QGraphicsSceneHoverEvent *event, const QTransform &transform)
848 {
849 Q_UNUSED(transform)
850 if (!m_model || !m_view) {
851 return false;
852 }
853
854 // We identify the widget whose expansionArea had been hovered before this hoverMoveEvent() triggered.
855 // we can't use hoveredWidget() here (it handles the icon+text rect, not the expansion rect)
856 // like hoveredWidget(), we find the hovered widget for the expansion rect
857 const auto visibleItemListWidgets = m_view->visibleItemListWidgets();
858 const auto oldHoveredExpansionWidgetIterator = std::find_if(visibleItemListWidgets.begin(), visibleItemListWidgets.end(), [](auto &widget) {
859 return widget->expansionAreaHovered();
860 });
861 const auto oldHoveredExpansionWidget =
862 oldHoveredExpansionWidgetIterator == visibleItemListWidgets.end() ? std::nullopt : std::make_optional(*oldHoveredExpansionWidgetIterator);
863
864 const auto unhoverOldHoveredWidget = [&]() {
865 if (auto oldHoveredWidget = hoveredWidget(); oldHoveredWidget) {
866 // handle the text+icon one
867 oldHoveredWidget->setHovered(false);
868 Q_EMIT itemUnhovered(oldHoveredWidget->index());
869 }
870 };
871
872 const auto unhoverOldExpansionWidget = [&]() {
873 if (oldHoveredExpansionWidget) {
874 // then the expansion toggle
875 (*oldHoveredExpansionWidget)->setExpansionAreaHovered(false);
876 }
877 };
878
879 const QPointF pos = transform.map(event->pos());
880 if (KItemListWidget *newHoveredWidget = widgetForPos(pos); newHoveredWidget) {
881 // something got hovered, work out which part and set hover for the appropriate widget
882 const auto mappedPos = newHoveredWidget->mapFromItem(m_view, pos);
883 const bool isOnExpansionToggle = newHoveredWidget->expansionToggleRect().contains(mappedPos);
884
885 if (isOnExpansionToggle) {
886 // make sure we unhover the old one first if old!=new
887 if (oldHoveredExpansionWidget && *oldHoveredExpansionWidget != newHoveredWidget) {
888 (*oldHoveredExpansionWidget)->setExpansionAreaHovered(false);
889 }
890 // 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)
891 unhoverOldHoveredWidget();
892
893 newHoveredWidget->setExpansionAreaHovered(true);
894 } else {
895 // make sure we unhover the old one first if old!=new
896 auto oldHoveredWidget = hoveredWidget();
897 if (oldHoveredWidget && oldHoveredWidget != newHoveredWidget) {
898 oldHoveredWidget->setHovered(false);
899 Q_EMIT itemUnhovered(oldHoveredWidget->index());
900 }
901 // 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)
902 unhoverOldExpansionWidget();
903
904 const bool isOverIconAndText = newHoveredWidget->iconRect().contains(mappedPos) || newHoveredWidget->textRect().contains(mappedPos);
905 const bool hasMultipleSelection = m_selectionManager->selectedItems().count() > 1;
906
907 if (hasMultipleSelection && !isOverIconAndText) {
908 // In case we have multiple selections, clicking on any row will deselect the selection.
909 // So, as a visual cue for signalling that clicking anywhere won't select, but clear current highlights,
910 // we disable hover of the *row*(i.e. blank space to the right of the icon+text)
911
912 // (no-op in this branch for masked hover)
913 } else {
914 newHoveredWidget->setHoverPosition(mappedPos);
915 if (oldHoveredWidget != newHoveredWidget) {
916 newHoveredWidget->setHovered(true);
917 Q_EMIT itemHovered(newHoveredWidget->index());
918 }
919 }
920 }
921 } else {
922 // unhover any currently hovered expansion and text+icon widgets
923 unhoverOldHoveredWidget();
924 unhoverOldExpansionWidget();
925 }
926 return false;
927 }
928
929 bool KItemListController::hoverLeaveEvent(QGraphicsSceneHoverEvent *event, const QTransform &transform)
930 {
931 Q_UNUSED(event)
932 Q_UNUSED(transform)
933
934 m_mousePress = false;
935 m_isTouchEvent = false;
936
937 if (!m_model || !m_view) {
938 return false;
939 }
940
941 const auto widgets = m_view->visibleItemListWidgets();
942 for (KItemListWidget *widget : widgets) {
943 if (widget->isHovered()) {
944 widget->setHovered(false);
945 Q_EMIT itemUnhovered(widget->index());
946 }
947 }
948 return false;
949 }
950
951 bool KItemListController::wheelEvent(QGraphicsSceneWheelEvent *event, const QTransform &transform)
952 {
953 Q_UNUSED(event)
954 Q_UNUSED(transform)
955 return false;
956 }
957
958 bool KItemListController::resizeEvent(QGraphicsSceneResizeEvent *event, const QTransform &transform)
959 {
960 Q_UNUSED(event)
961 Q_UNUSED(transform)
962 return false;
963 }
964
965 bool KItemListController::gestureEvent(QGestureEvent *event, const QTransform &transform)
966 {
967 if (!m_view) {
968 return false;
969 }
970
971 //you can touch on different views at the same time, but only one QWidget gets a mousePressEvent
972 //we use this to get the right QWidget
973 //the only exception is a tap gesture with state GestureStarted, we need to reset some variable
974 if (!m_mousePress) {
975 if (QGesture *tap = event->gesture(Qt::TapGesture)) {
976 QTapGesture *tapGesture = static_cast<QTapGesture *>(tap);
977 if (tapGesture->state() == Qt::GestureStarted) {
978 tapTriggered(tapGesture, transform);
979 }
980 }
981 return false;
982 }
983
984 bool accepted = false;
985
986 if (QGesture *tap = event->gesture(Qt::TapGesture)) {
987 tapTriggered(static_cast<QTapGesture *>(tap), transform);
988 accepted = true;
989 }
990 if (event->gesture(Qt::TapAndHoldGesture)) {
991 tapAndHoldTriggered(event, transform);
992 accepted = true;
993 }
994 if (event->gesture(Qt::PinchGesture)) {
995 pinchTriggered(event, transform);
996 accepted = true;
997 }
998 if (event->gesture(m_swipeGesture)) {
999 swipeTriggered(event, transform);
1000 accepted = true;
1001 }
1002 if (event->gesture(m_twoFingerTapGesture)) {
1003 twoFingerTapTriggered(event, transform);
1004 accepted = true;
1005 }
1006 return accepted;
1007 }
1008
1009 bool KItemListController::touchBeginEvent(QTouchEvent *event, const QTransform &transform)
1010 {
1011 Q_UNUSED(event)
1012 Q_UNUSED(transform)
1013
1014 m_isTouchEvent = true;
1015 return false;
1016 }
1017
1018 void KItemListController::tapTriggered(QTapGesture *tap, const QTransform &transform)
1019 {
1020 static bool scrollerWasActive = false;
1021
1022 if (tap->state() == Qt::GestureStarted) {
1023 m_dragActionOrRightClick = false;
1024 m_isSwipeGesture = false;
1025 m_pinchGestureInProgress = false;
1026 scrollerWasActive = m_scrollerIsScrolling;
1027 }
1028
1029 if (tap->state() == Qt::GestureFinished) {
1030 m_mousePress = false;
1031
1032 //if at the moment of the gesture start the QScroller was active, the user made the tap
1033 //to stop the QScroller and not to tap on an item
1034 if (scrollerWasActive) {
1035 return;
1036 }
1037
1038 if (m_view->m_tapAndHoldIndicator->isActive()) {
1039 m_view->m_tapAndHoldIndicator->setActive(false);
1040 }
1041
1042 m_pressedMousePos = transform.map(tap->position());
1043 m_pressedIndex = m_view->itemAt(m_pressedMousePos);
1044
1045 if (m_dragActionOrRightClick) {
1046 m_dragActionOrRightClick = false;
1047 } else {
1048 onPress(tap->hotSpot().toPoint(), tap->position().toPoint(), Qt::NoModifier, Qt::LeftButton);
1049 onRelease(transform.map(tap->position()), Qt::NoModifier, Qt::LeftButton, true);
1050 }
1051 m_isTouchEvent = false;
1052 }
1053 }
1054
1055 void KItemListController::tapAndHoldTriggered(QGestureEvent *event, const QTransform &transform)
1056 {
1057 //the Qt TabAndHold gesture is triggerable with a mouse click, we don't want this
1058 if (!m_isTouchEvent) {
1059 return;
1060 }
1061
1062 const QTapAndHoldGesture *tap = static_cast<QTapAndHoldGesture *>(event->gesture(Qt::TapAndHoldGesture));
1063 if (tap->state() == Qt::GestureFinished) {
1064 //if a pinch gesture is in progress we don't want a TabAndHold gesture
1065 if (m_pinchGestureInProgress) {
1066 return;
1067 }
1068 m_pressedMousePos = transform.map(event->mapToGraphicsScene(tap->position()));
1069 m_pressedIndex = m_view->itemAt(m_pressedMousePos);
1070
1071 if (m_pressedIndex.has_value() && !m_selectionManager->isSelected(m_pressedIndex.value())) {
1072 m_selectionManager->clearSelection();
1073 m_selectionManager->setSelected(m_pressedIndex.value());
1074 if (!m_selectionMode) {
1075 Q_EMIT selectionModeChangeRequested(true);
1076 }
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 bool KItemListController::processEvent(QEvent *event, const QTransform &transform)
1169 {
1170 if (!event) {
1171 return false;
1172 }
1173
1174 switch (event->type()) {
1175 case QEvent::KeyPress:
1176 return keyPressEvent(static_cast<QKeyEvent *>(event));
1177 case QEvent::InputMethod:
1178 return inputMethodEvent(static_cast<QInputMethodEvent *>(event));
1179 case QEvent::GraphicsSceneMousePress:
1180 return mousePressEvent(static_cast<QGraphicsSceneMouseEvent *>(event), QTransform());
1181 case QEvent::GraphicsSceneMouseMove:
1182 return mouseMoveEvent(static_cast<QGraphicsSceneMouseEvent *>(event), QTransform());
1183 case QEvent::GraphicsSceneMouseRelease:
1184 return mouseReleaseEvent(static_cast<QGraphicsSceneMouseEvent *>(event), QTransform());
1185 case QEvent::GraphicsSceneMouseDoubleClick:
1186 return mouseDoubleClickEvent(static_cast<QGraphicsSceneMouseEvent *>(event), QTransform());
1187 case QEvent::GraphicsSceneWheel:
1188 return wheelEvent(static_cast<QGraphicsSceneWheelEvent *>(event), QTransform());
1189 case QEvent::GraphicsSceneDragEnter:
1190 return dragEnterEvent(static_cast<QGraphicsSceneDragDropEvent *>(event), QTransform());
1191 case QEvent::GraphicsSceneDragLeave:
1192 return dragLeaveEvent(static_cast<QGraphicsSceneDragDropEvent *>(event), QTransform());
1193 case QEvent::GraphicsSceneDragMove:
1194 return dragMoveEvent(static_cast<QGraphicsSceneDragDropEvent *>(event), QTransform());
1195 case QEvent::GraphicsSceneDrop:
1196 return dropEvent(static_cast<QGraphicsSceneDragDropEvent *>(event), QTransform());
1197 case QEvent::GraphicsSceneHoverEnter:
1198 return hoverEnterEvent(static_cast<QGraphicsSceneHoverEvent *>(event), QTransform());
1199 case QEvent::GraphicsSceneHoverMove:
1200 return hoverMoveEvent(static_cast<QGraphicsSceneHoverEvent *>(event), QTransform());
1201 case QEvent::GraphicsSceneHoverLeave:
1202 return hoverLeaveEvent(static_cast<QGraphicsSceneHoverEvent *>(event), QTransform());
1203 case QEvent::GraphicsSceneResize:
1204 return resizeEvent(static_cast<QGraphicsSceneResizeEvent *>(event), transform);
1205 case QEvent::Gesture:
1206 return gestureEvent(static_cast<QGestureEvent *>(event), transform);
1207 case QEvent::TouchBegin:
1208 return touchBeginEvent(static_cast<QTouchEvent *>(event), transform);
1209 default:
1210 break;
1211 }
1212
1213 return false;
1214 }
1215
1216 void KItemListController::slotViewScrollOffsetChanged(qreal current, qreal previous)
1217 {
1218 if (!m_view) {
1219 return;
1220 }
1221
1222 KItemListRubberBand *rubberBand = m_view->rubberBand();
1223 if (rubberBand->isActive()) {
1224 const qreal diff = current - previous;
1225 // TODO: Ideally just QCursor::pos() should be used as
1226 // new end-position but it seems there is no easy way
1227 // to have something like QWidget::mapFromGlobal() for QGraphicsWidget
1228 // (... or I just missed an easy way to do the mapping)
1229 QPointF endPos = rubberBand->endPosition();
1230 if (m_view->scrollOrientation() == Qt::Vertical) {
1231 endPos.ry() += diff;
1232 } else {
1233 endPos.rx() += diff;
1234 }
1235
1236 rubberBand->setEndPosition(endPos);
1237 }
1238 }
1239
1240 void KItemListController::slotRubberBandChanged()
1241 {
1242 if (!m_view || !m_model || m_model->count() <= 0) {
1243 return;
1244 }
1245
1246 const KItemListRubberBand *rubberBand = m_view->rubberBand();
1247 const QPointF startPos = rubberBand->startPosition();
1248 const QPointF endPos = rubberBand->endPosition();
1249 QRectF rubberBandRect = QRectF(startPos, endPos).normalized();
1250
1251 const bool scrollVertical = (m_view->scrollOrientation() == Qt::Vertical);
1252 if (scrollVertical) {
1253 rubberBandRect.translate(0, -m_view->scrollOffset());
1254 } else {
1255 rubberBandRect.translate(-m_view->scrollOffset(), 0);
1256 }
1257
1258 if (!m_oldSelection.isEmpty()) {
1259 // Clear the old selection that was available before the rubberband has
1260 // been activated in case if no Shift- or Control-key are pressed
1261 const bool shiftOrControlPressed = QApplication::keyboardModifiers() & Qt::ShiftModifier || QApplication::keyboardModifiers() & Qt::ControlModifier;
1262 if (!shiftOrControlPressed && !m_selectionMode) {
1263 m_oldSelection.clear();
1264 }
1265 }
1266
1267 KItemSet selectedItems;
1268
1269 // Select all visible items that intersect with the rubberband
1270 const auto widgets = m_view->visibleItemListWidgets();
1271 for (const KItemListWidget *widget : widgets) {
1272 const int index = widget->index();
1273
1274 const QRectF widgetRect = m_view->itemRect(index);
1275 if (widgetRect.intersects(rubberBandRect)) {
1276 const QRectF iconRect = widget->iconRect().translated(widgetRect.topLeft());
1277 const QRectF textRect = widget->textRect().translated(widgetRect.topLeft());
1278 if (iconRect.intersects(rubberBandRect) || textRect.intersects(rubberBandRect)) {
1279 selectedItems.insert(index);
1280 }
1281 }
1282 }
1283
1284 // Select all invisible items that intersect with the rubberband. Instead of
1285 // iterating all items only the area which might be touched by the rubberband
1286 // will be checked.
1287 const bool increaseIndex = scrollVertical ? startPos.y() > endPos.y() : startPos.x() > endPos.x();
1288
1289 int index = increaseIndex ? m_view->lastVisibleIndex() + 1 : m_view->firstVisibleIndex() - 1;
1290 bool selectionFinished = false;
1291 do {
1292 const QRectF widgetRect = m_view->itemRect(index);
1293 if (widgetRect.intersects(rubberBandRect)) {
1294 selectedItems.insert(index);
1295 }
1296
1297 if (increaseIndex) {
1298 ++index;
1299 selectionFinished = (index >= m_model->count()) || (scrollVertical && widgetRect.top() > rubberBandRect.bottom())
1300 || (!scrollVertical && widgetRect.left() > rubberBandRect.right());
1301 } else {
1302 --index;
1303 selectionFinished = (index < 0) || (scrollVertical && widgetRect.bottom() < rubberBandRect.top())
1304 || (!scrollVertical && widgetRect.right() < rubberBandRect.left());
1305 }
1306 } while (!selectionFinished);
1307
1308 if ((QApplication::keyboardModifiers() & Qt::ControlModifier) || m_selectionMode) {
1309 // If Control is pressed, the selection state of all items in the rubberband is toggled.
1310 // Therefore, the new selection contains:
1311 // 1. All previously selected items which are not inside the rubberband, and
1312 // 2. all items inside the rubberband which have not been selected previously.
1313 m_selectionManager->setSelectedItems(m_oldSelection ^ selectedItems);
1314 } else {
1315 m_selectionManager->setSelectedItems(selectedItems + m_oldSelection);
1316 }
1317 }
1318
1319 void KItemListController::startDragging()
1320 {
1321 if (!m_view || !m_model) {
1322 return;
1323 }
1324
1325 const KItemSet selectedItems = m_selectionManager->selectedItems();
1326 if (selectedItems.isEmpty()) {
1327 return;
1328 }
1329
1330 QMimeData *data = m_model->createMimeData(selectedItems);
1331 if (!data) {
1332 return;
1333 }
1334 KUrlMimeData::exportUrlsToPortal(data);
1335
1336 // The created drag object will be owned and deleted
1337 // by QApplication::activeWindow().
1338 QDrag *drag = new QDrag(QApplication::activeWindow());
1339 drag->setMimeData(data);
1340
1341 const QPixmap pixmap = m_view->createDragPixmap(selectedItems);
1342 drag->setPixmap(pixmap);
1343
1344 const QPoint hotSpot((pixmap.width() / pixmap.devicePixelRatio()) / 2, 0);
1345 drag->setHotSpot(hotSpot);
1346
1347 drag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction);
1348
1349 QAccessibleEvent accessibilityEvent(view(), QAccessible::DragDropStart);
1350 QAccessible::updateAccessibility(&accessibilityEvent);
1351 }
1352
1353 KItemListWidget *KItemListController::hoveredWidget() const
1354 {
1355 Q_ASSERT(m_view);
1356
1357 const auto widgets = m_view->visibleItemListWidgets();
1358 for (KItemListWidget *widget : widgets) {
1359 if (widget->isHovered()) {
1360 return widget;
1361 }
1362 }
1363
1364 return nullptr;
1365 }
1366
1367 KItemListWidget *KItemListController::widgetForPos(const QPointF &pos) const
1368 {
1369 Q_ASSERT(m_view);
1370
1371 const auto widgets = m_view->visibleItemListWidgets();
1372 for (KItemListWidget *widget : widgets) {
1373 const QPointF mappedPos = widget->mapFromItem(m_view, pos);
1374 if (widget->contains(mappedPos) || widget->selectionRect().contains(mappedPos)) {
1375 return widget;
1376 }
1377 }
1378
1379 return nullptr;
1380 }
1381
1382 KItemListWidget *KItemListController::widgetForDropPos(const QPointF &pos) const
1383 {
1384 Q_ASSERT(m_view);
1385
1386 const auto widgets = m_view->visibleItemListWidgets();
1387 for (KItemListWidget *widget : widgets) {
1388 const QPointF mappedPos = widget->mapFromItem(m_view, pos);
1389 if (widget->contains(mappedPos)) {
1390 return widget;
1391 }
1392 }
1393
1394 return nullptr;
1395 }
1396
1397 void KItemListController::updateKeyboardAnchor()
1398 {
1399 const bool validAnchor =
1400 m_keyboardAnchorIndex >= 0 && m_keyboardAnchorIndex < m_model->count() && keyboardAnchorPos(m_keyboardAnchorIndex) == m_keyboardAnchorPos;
1401 if (!validAnchor) {
1402 const int index = m_selectionManager->currentItem();
1403 m_keyboardAnchorIndex = index;
1404 m_keyboardAnchorPos = keyboardAnchorPos(index);
1405 }
1406 }
1407
1408 int KItemListController::nextRowIndex(int index) const
1409 {
1410 if (m_keyboardAnchorIndex < 0) {
1411 return index;
1412 }
1413
1414 const int maxIndex = m_model->count() - 1;
1415 if (index == maxIndex) {
1416 return index;
1417 }
1418
1419 // Calculate the index of the last column inside the row of the current index
1420 int lastColumnIndex = index;
1421 while (keyboardAnchorPos(lastColumnIndex + 1) > keyboardAnchorPos(lastColumnIndex)) {
1422 ++lastColumnIndex;
1423 if (lastColumnIndex >= maxIndex) {
1424 return index;
1425 }
1426 }
1427
1428 // Based on the last column index go to the next row and calculate the nearest index
1429 // that is below the current index
1430 int nextRowIndex = lastColumnIndex + 1;
1431 int searchIndex = nextRowIndex;
1432 qreal minDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(nextRowIndex));
1433 while (searchIndex < maxIndex && keyboardAnchorPos(searchIndex + 1) > keyboardAnchorPos(searchIndex)) {
1434 ++searchIndex;
1435 const qreal searchDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(searchIndex));
1436 if (searchDiff < minDiff) {
1437 minDiff = searchDiff;
1438 nextRowIndex = searchIndex;
1439 }
1440 }
1441
1442 return nextRowIndex;
1443 }
1444
1445 int KItemListController::previousRowIndex(int index) const
1446 {
1447 if (m_keyboardAnchorIndex < 0 || index == 0) {
1448 return index;
1449 }
1450
1451 // Calculate the index of the first column inside the row of the current index
1452 int firstColumnIndex = index;
1453 while (keyboardAnchorPos(firstColumnIndex - 1) < keyboardAnchorPos(firstColumnIndex)) {
1454 --firstColumnIndex;
1455 if (firstColumnIndex <= 0) {
1456 return index;
1457 }
1458 }
1459
1460 // Based on the first column index go to the previous row and calculate the nearest index
1461 // that is above the current index
1462 int previousRowIndex = firstColumnIndex - 1;
1463 int searchIndex = previousRowIndex;
1464 qreal minDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(previousRowIndex));
1465 while (searchIndex > 0 && keyboardAnchorPos(searchIndex - 1) < keyboardAnchorPos(searchIndex)) {
1466 --searchIndex;
1467 const qreal searchDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(searchIndex));
1468 if (searchDiff < minDiff) {
1469 minDiff = searchDiff;
1470 previousRowIndex = searchIndex;
1471 }
1472 }
1473
1474 return previousRowIndex;
1475 }
1476
1477 qreal KItemListController::keyboardAnchorPos(int index) const
1478 {
1479 const QRectF itemRect = m_view->itemRect(index);
1480 if (!itemRect.isEmpty()) {
1481 return (m_view->scrollOrientation() == Qt::Vertical) ? itemRect.x() : itemRect.y();
1482 }
1483
1484 return 0;
1485 }
1486
1487 void KItemListController::updateExtendedSelectionRegion()
1488 {
1489 if (m_view) {
1490 const bool extend = (m_selectionBehavior != MultiSelection);
1491 KItemListStyleOption option = m_view->styleOption();
1492 if (option.extendedSelectionRegion != extend) {
1493 option.extendedSelectionRegion = extend;
1494 m_view->setStyleOption(option);
1495 }
1496 }
1497 }
1498
1499 bool KItemListController::onPress(const QPoint &screenPos, const QPointF &pos, const Qt::KeyboardModifiers modifiers, const Qt::MouseButtons buttons)
1500 {
1501 Q_EMIT mouseButtonPressed(m_pressedIndex.value_or(-1), buttons);
1502
1503 if (buttons & (Qt::BackButton | Qt::ForwardButton)) {
1504 // Do not select items when clicking the back/forward buttons, see
1505 // https://bugs.kde.org/show_bug.cgi?id=327412.
1506 return true;
1507 }
1508
1509 if (m_view->isAboveExpansionToggle(m_pressedIndex.value_or(-1), m_pressedMousePos)) {
1510 m_selectionManager->endAnchoredSelection();
1511 m_selectionManager->setCurrentItem(m_pressedIndex.value());
1512 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1513 return true;
1514 }
1515
1516 m_selectionTogglePressed = m_view->isAboveSelectionToggle(m_pressedIndex.value_or(-1), m_pressedMousePos);
1517 if (m_selectionTogglePressed) {
1518 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
1519 // The previous anchored selection has been finished already in
1520 // KItemListSelectionManager::setSelected(). We can safely change
1521 // the current item and start a new anchored selection now.
1522 m_selectionManager->setCurrentItem(m_pressedIndex.value());
1523 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1524 return true;
1525 }
1526
1527 const bool shiftPressed = modifiers & Qt::ShiftModifier;
1528 const bool controlPressed = (modifiers & Qt::ControlModifier) || m_selectionMode; // Keeping selectionMode similar to pressing control will hopefully
1529 // simplify the overall logic and possibilities both for users and devs.
1530 const bool leftClick = buttons & Qt::LeftButton;
1531 const bool rightClick = buttons & Qt::RightButton;
1532
1533 // The previous selection is cleared if either
1534 // 1. The selection mode is SingleSelection, or
1535 // 2. the selection mode is MultiSelection, and *none* of the following conditions are met:
1536 // a) Shift or Control are pressed.
1537 // b) The clicked item is selected already. In that case, the user might want to:
1538 // - start dragging multiple items, or
1539 // - open the context menu and perform an action for all selected items.
1540 const bool shiftOrControlPressed = shiftPressed || controlPressed;
1541 const bool pressedItemAlreadySelected = m_pressedIndex.has_value() && m_selectionManager->isSelected(m_pressedIndex.value());
1542 const bool clearSelection = m_selectionBehavior == SingleSelection || (!shiftOrControlPressed && !pressedItemAlreadySelected);
1543
1544 // When this method returns false, a rubberBand selection is created using KItemListController::startRubberBand via the caller.
1545 if (clearSelection) {
1546 const int selectedItemsCount = m_selectionManager->selectedItems().count();
1547 m_selectionManager->clearSelection();
1548 // clear and bail when we got an existing multi-selection
1549 if (selectedItemsCount > 1 && m_pressedIndex.has_value()) {
1550 const auto row = m_view->m_visibleItems.value(m_pressedIndex.value());
1551 const auto mappedPos = row->mapFromItem(m_view, pos);
1552 if (pressedItemAlreadySelected || row->iconRect().contains(mappedPos) || row->textRect().contains(mappedPos)) {
1553 // we are indeed inside the text/icon rect, keep m_pressedIndex what it is
1554 // and short-circuit for single-click activation (it will then propagate to onRelease and activate the item)
1555 // or we just keep going for double-click activation
1556 if (m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced) {
1557 if (!pressedItemAlreadySelected) {
1558 // An unselected item was clicked directly while deselecting multiple other items so we select it.
1559 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
1560 m_selectionManager->setCurrentItem(m_pressedIndex.value());
1561 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1562 }
1563 return true; // event handled, don't create rubber band
1564 }
1565 } else {
1566 // we're not inside the text/icon rect, as we've already cleared the selection
1567 // we can just stop here and make sure handlers down the line (i.e. onRelease) don't activate
1568 m_pressedIndex.reset();
1569 // we don't stop event propagation and proceed to create a rubber band and let onRelease
1570 // decide (based on m_pressedIndex) whether we're in a drag (drag => new rubber band, click => don't select the item)
1571 return false;
1572 }
1573 }
1574 } else if (pressedItemAlreadySelected && !shiftOrControlPressed && leftClick) {
1575 // The user might want to start dragging multiple items, but if he clicks the item
1576 // in order to trigger it instead, the other selected items must be deselected.
1577 // However, we do not know yet what the user is going to do.
1578 // -> remember that the user pressed an item which had been selected already and
1579 // clear the selection in mouseReleaseEvent(), unless the items are dragged.
1580 m_clearSelectionIfItemsAreNotDragged = true;
1581
1582 if (m_selectionManager->selectedItems().count() == 1 && m_view->isAboveText(m_pressedIndex.value_or(-1), m_pressedMousePos)) {
1583 Q_EMIT selectedItemTextPressed(m_pressedIndex.value_or(-1));
1584 }
1585 }
1586
1587 if (!shiftPressed) {
1588 // Finish the anchored selection before the current index is changed
1589 m_selectionManager->endAnchoredSelection();
1590 }
1591
1592 if (rightClick) {
1593 // Do header hit check and short circuit before commencing any state changing effects
1594 if (m_view->headerBoundaries().contains(pos)) {
1595 Q_EMIT headerContextMenuRequested(screenPos);
1596 return true;
1597 }
1598
1599 // Stop rubber band from persisting after right-clicks
1600 KItemListRubberBand *rubberBand = m_view->rubberBand();
1601 if (rubberBand->isActive()) {
1602 disconnect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged);
1603 rubberBand->setActive(false);
1604 m_view->setAutoScroll(false);
1605 }
1606 }
1607
1608 if (m_pressedIndex.has_value()) {
1609 // The hover highlight area of an item is being pressed.
1610 m_selectionManager->setCurrentItem(m_pressedIndex.value());
1611 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
1612 const bool hitTargetIsRowEmptyRegion = !row->contains(row->mapFromItem(m_view, pos));
1613 // again, when this method returns false, a rubberBand selection is created as the event is not consumed;
1614 // createRubberBand here tells us whether to return true or false.
1615 bool createRubberBand = (hitTargetIsRowEmptyRegion && m_selectionManager->selectedItems().isEmpty());
1616
1617 if (rightClick && hitTargetIsRowEmptyRegion) {
1618 // We have a right click outside the icon and text rect but within the hover highlight area
1619 // but it is unclear if this means that a selection rectangle for an item was clicked or the background of the view.
1620 if (m_selectionManager->selectedItems().contains(m_pressedIndex.value())) {
1621 // The selection rectangle for an item was clicked
1622 Q_EMIT itemContextMenuRequested(m_pressedIndex.value(), screenPos);
1623 } else {
1624 row->setHovered(false); // Removes the hover highlight so the context menu doesn't look like it applies to the row.
1625 Q_EMIT viewContextMenuRequested(screenPos);
1626 }
1627 return true;
1628 }
1629
1630 switch (m_selectionBehavior) {
1631 case NoSelection:
1632 break;
1633
1634 case SingleSelection:
1635 m_selectionManager->setSelected(m_pressedIndex.value());
1636 break;
1637
1638 case MultiSelection:
1639 if (controlPressed && !shiftPressed && leftClick) {
1640 // A left mouse button press is happening on an item while control is pressed. This either means a user wants to:
1641 // - toggle the selection of item(s) or
1642 // - they want to begin a drag on the item(s) to copy them.
1643 // We rule out the latter, if the item is not clicked directly and was unselected previously.
1644 const auto row = m_view->m_visibleItems.value(m_pressedIndex.value());
1645 const auto mappedPos = row->mapFromItem(m_view, pos);
1646 if (!row->iconRect().contains(mappedPos) && !row->textRect().contains(mappedPos) && !pressedItemAlreadySelected) {
1647 createRubberBand = true;
1648 } else {
1649 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
1650 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1651 createRubberBand = false; // multi selection, don't propagate any further
1652 // This will be the start of an item drag-to-copy operation if the user now moves the mouse before releasing the mouse button.
1653 }
1654 } else if (!shiftPressed || !m_selectionManager->isAnchoredSelectionActive()) {
1655 // Select the pressed item and start a new anchored selection
1656 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Select);
1657 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1658 }
1659 break;
1660
1661 default:
1662 Q_ASSERT(false);
1663 break;
1664 }
1665
1666 if (rightClick) {
1667 Q_EMIT itemContextMenuRequested(m_pressedIndex.value(), screenPos);
1668 }
1669 return !createRubberBand;
1670 }
1671
1672 if (rightClick) {
1673 // header right click handling would have been done before this so just normal context
1674 // menu here is fine
1675 Q_EMIT viewContextMenuRequested(screenPos);
1676 return true;
1677 }
1678
1679 return false;
1680 }
1681
1682 bool KItemListController::onRelease(const QPointF &pos, const Qt::KeyboardModifiers modifiers, const Qt::MouseButtons buttons, bool touch)
1683 {
1684 const bool isAboveSelectionToggle = m_view->isAboveSelectionToggle(m_pressedIndex.value_or(-1), m_pressedMousePos);
1685 if (isAboveSelectionToggle) {
1686 m_selectionTogglePressed = false;
1687 return true;
1688 }
1689
1690 if (!isAboveSelectionToggle && m_selectionTogglePressed) {
1691 m_selectionManager->setSelected(m_pressedIndex.value_or(-1), 1, KItemListSelectionManager::Toggle);
1692 m_selectionTogglePressed = false;
1693 return true;
1694 }
1695
1696 const bool controlPressed = modifiers & Qt::ControlModifier;
1697 const bool shiftOrControlPressed = modifiers & Qt::ShiftModifier || controlPressed;
1698
1699 const std::optional<int> index = m_view->itemAt(pos);
1700
1701 KItemListRubberBand *rubberBand = m_view->rubberBand();
1702 bool rubberBandRelease = false;
1703 if (rubberBand->isActive()) {
1704 disconnect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged);
1705 rubberBand->setActive(false);
1706 m_oldSelection.clear();
1707 m_view->setAutoScroll(false);
1708 rubberBandRelease = true;
1709 // We check for actual rubber band drag here: if delta between start and end is less than drag threshold,
1710 // then we have a single click on one of the rows
1711 if ((rubberBand->endPosition() - rubberBand->startPosition()).manhattanLength() < QApplication::startDragDistance()) {
1712 rubberBandRelease = false; // since we're only selecting, unmark rubber band release flag
1713 // m_pressedIndex will have no value if we came from a multi-selection clearing onPress
1714 // in that case, we don't select anything
1715 if (index.has_value() && m_pressedIndex.has_value()) {
1716 if (controlPressed && m_selectionBehavior == MultiSelection) {
1717 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
1718 } else {
1719 m_selectionManager->setSelected(index.value());
1720 }
1721 if (!m_selectionManager->isAnchoredSelectionActive()) {
1722 m_selectionManager->beginAnchoredSelection(index.value());
1723 }
1724 }
1725 }
1726 }
1727
1728 if (index.has_value() && index == m_pressedIndex) {
1729 // The release event is done above the same item as the press event
1730
1731 if (m_clearSelectionIfItemsAreNotDragged) {
1732 // A selected item has been clicked, but no drag operation has been started
1733 // -> clear the rest of the selection.
1734 m_selectionManager->clearSelection();
1735 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Select);
1736 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1737 }
1738
1739 if (buttons & Qt::LeftButton) {
1740 bool emitItemActivated = true;
1741 if (m_view->isAboveExpansionToggle(index.value(), pos)) {
1742 const bool expanded = m_model->isExpanded(index.value());
1743 m_model->setExpanded(index.value(), !expanded);
1744
1745 Q_EMIT itemExpansionToggleClicked(index.value());
1746 emitItemActivated = false;
1747 } else if (shiftOrControlPressed && m_selectionBehavior != SingleSelection) {
1748 // The mouse click should only update the selection, not trigger the item, except when
1749 // we are in single selection mode
1750 emitItemActivated = false;
1751 } else {
1752 const bool singleClickActivation = m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced;
1753 if (!singleClickActivation) {
1754 emitItemActivated = touch && !m_selectionMode;
1755 } else {
1756 // activate on single click only if we didn't come from a rubber band release
1757 emitItemActivated = !rubberBandRelease;
1758 }
1759 }
1760 if (emitItemActivated) {
1761 Q_EMIT itemActivated(index.value());
1762 }
1763 } else if (buttons & Qt::MiddleButton) {
1764 Q_EMIT itemMiddleClicked(index.value());
1765 }
1766 }
1767
1768 m_pressedMousePos = QPointF();
1769 m_pressedIndex = std::nullopt;
1770 m_clearSelectionIfItemsAreNotDragged = false;
1771 return false;
1772 }
1773
1774 void KItemListController::startRubberBand()
1775 {
1776 if (m_selectionBehavior == MultiSelection) {
1777 QPointF startPos = m_pressedMousePos;
1778 if (m_view->scrollOrientation() == Qt::Vertical) {
1779 startPos.ry() += m_view->scrollOffset();
1780 if (m_view->itemSize().width() < 0) {
1781 // Use a special rubberband for views that have only one column and
1782 // expand the rubberband to use the whole width of the view.
1783 startPos.setX(0);
1784 }
1785 } else {
1786 startPos.rx() += m_view->scrollOffset();
1787 }
1788
1789 m_oldSelection = m_selectionManager->selectedItems();
1790 KItemListRubberBand *rubberBand = m_view->rubberBand();
1791 rubberBand->setStartPosition(startPos);
1792 rubberBand->setEndPosition(startPos);
1793 rubberBand->setActive(true);
1794 connect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged);
1795 m_view->setAutoScroll(true);
1796 }
1797 }
1798
1799 void KItemListController::slotStateChanged(QScroller::State newState)
1800 {
1801 if (newState == QScroller::Scrolling) {
1802 m_scrollerIsScrolling = true;
1803 } else if (newState == QScroller::Inactive) {
1804 m_scrollerIsScrolling = false;
1805 }
1806 }