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