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