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