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