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