]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kitemlistcontroller.cpp
Fix icon resize animation
[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 auto oldHoveredWidget = hoveredWidget();
875 if (oldHoveredWidget && oldHoveredWidget != newHoveredWidget) {
876 oldHoveredWidget->setHovered(false);
877 Q_EMIT itemUnhovered(oldHoveredWidget->index());
878 }
879 // 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)
880 unhoverOldExpansionWidget();
881
882 const bool isOverIconAndText = newHoveredWidget->iconRect().contains(mappedPos) || newHoveredWidget->textRect().contains(mappedPos);
883 const bool hasMultipleSelection = m_selectionManager->selectedItems().count() > 1;
884
885 if (hasMultipleSelection && !isOverIconAndText) {
886 // In case we have multiple selections, clicking on any row will deselect the selection.
887 // So, as a visual cue for signalling that clicking anywhere won't select, but clear current highlights,
888 // we disable hover of the *row*(i.e. blank space to the right of the icon+text)
889
890 // (no-op in this branch for masked hover)
891 } else {
892 newHoveredWidget->setHoverPosition(mappedPos);
893 if (oldHoveredWidget != newHoveredWidget) {
894 newHoveredWidget->setHovered(true);
895 Q_EMIT itemHovered(newHoveredWidget->index());
896 }
897 }
898 }
899 } else {
900 // unhover any currently hovered expansion and text+icon widgets
901 unhoverOldHoveredWidget();
902 unhoverOldExpansionWidget();
903 }
904 return false;
905 }
906
907 bool KItemListController::hoverLeaveEvent(QGraphicsSceneHoverEvent* event, const QTransform& transform)
908 {
909 Q_UNUSED(event)
910 Q_UNUSED(transform)
911
912 m_mousePress = false;
913 m_isTouchEvent = false;
914
915 if (!m_model || !m_view) {
916 return false;
917 }
918
919 const auto widgets = m_view->visibleItemListWidgets();
920 for (KItemListWidget* widget : widgets) {
921 if (widget->isHovered()) {
922 widget->setHovered(false);
923 Q_EMIT itemUnhovered(widget->index());
924 }
925 }
926 return false;
927 }
928
929 bool KItemListController::wheelEvent(QGraphicsSceneWheelEvent* event, const QTransform& transform)
930 {
931 Q_UNUSED(event)
932 Q_UNUSED(transform)
933 return false;
934 }
935
936 bool KItemListController::resizeEvent(QGraphicsSceneResizeEvent* event, const QTransform& transform)
937 {
938 Q_UNUSED(event)
939 Q_UNUSED(transform)
940 return false;
941 }
942
943 bool KItemListController::gestureEvent(QGestureEvent* event, const QTransform& transform)
944 {
945 if (!m_view) {
946 return false;
947 }
948
949 //you can touch on different views at the same time, but only one QWidget gets a mousePressEvent
950 //we use this to get the right QWidget
951 //the only exception is a tap gesture with state GestureStarted, we need to reset some variable
952 if (!m_mousePress) {
953 if (QGesture* tap = event->gesture(Qt::TapGesture)) {
954 QTapGesture* tapGesture = static_cast<QTapGesture*>(tap);
955 if (tapGesture->state() == Qt::GestureStarted) {
956 tapTriggered(tapGesture, transform);
957 }
958 }
959 return false;
960 }
961
962 bool accepted = false;
963
964 if (QGesture* tap = event->gesture(Qt::TapGesture)) {
965 tapTriggered(static_cast<QTapGesture*>(tap), transform);
966 accepted = true;
967 }
968 if (event->gesture(Qt::TapAndHoldGesture)) {
969 tapAndHoldTriggered(event, transform);
970 accepted = true;
971 }
972 if (event->gesture(Qt::PinchGesture)) {
973 pinchTriggered(event, transform);
974 accepted = true;
975 }
976 if (event->gesture(m_swipeGesture)) {
977 swipeTriggered(event, transform);
978 accepted = true;
979 }
980 if (event->gesture(m_twoFingerTapGesture)) {
981 twoFingerTapTriggered(event, transform);
982 accepted = true;
983 }
984 return accepted;
985 }
986
987 bool KItemListController::touchBeginEvent(QTouchEvent* event, const QTransform& transform)
988 {
989 Q_UNUSED(event)
990 Q_UNUSED(transform)
991
992 m_isTouchEvent = true;
993 return false;
994 }
995
996 void KItemListController::tapTriggered(QTapGesture* tap, const QTransform& transform)
997 {
998 static bool scrollerWasActive = false;
999
1000 if (tap->state() == Qt::GestureStarted) {
1001 m_dragActionOrRightClick = false;
1002 m_isSwipeGesture = false;
1003 m_pinchGestureInProgress = false;
1004 scrollerWasActive = m_scrollerIsScrolling;
1005 }
1006
1007 if (tap->state() == Qt::GestureFinished) {
1008 m_mousePress = false;
1009
1010 //if at the moment of the gesture start the QScroller was active, the user made the tap
1011 //to stop the QScroller and not to tap on an item
1012 if (scrollerWasActive) {
1013 return;
1014 }
1015
1016 if (m_view->m_tapAndHoldIndicator->isActive()) {
1017 m_view->m_tapAndHoldIndicator->setActive(false);
1018 }
1019
1020 m_pressedMousePos = transform.map(tap->position());
1021 m_pressedIndex = m_view->itemAt(m_pressedMousePos);
1022
1023 if (m_dragActionOrRightClick) {
1024 onPress(tap->hotSpot().toPoint(), tap->position().toPoint(), Qt::NoModifier, Qt::RightButton);
1025 onRelease(transform.map(tap->position()), Qt::NoModifier, Qt::RightButton, false);
1026 m_dragActionOrRightClick = false;
1027 }
1028 else {
1029 onPress(tap->hotSpot().toPoint(), tap->position().toPoint(), Qt::NoModifier, Qt::LeftButton);
1030 onRelease(transform.map(tap->position()), Qt::NoModifier, Qt::LeftButton, true);
1031 }
1032 m_isTouchEvent = false;
1033 }
1034 }
1035
1036 void KItemListController::tapAndHoldTriggered(QGestureEvent* event, const QTransform& transform)
1037 {
1038
1039 //the Qt TabAndHold gesture is triggerable with a mouse click, we don't want this
1040 if (!m_isTouchEvent) {
1041 return;
1042 }
1043
1044 const QTapAndHoldGesture* tap = static_cast<QTapAndHoldGesture*>(event->gesture(Qt::TapAndHoldGesture));
1045 if (tap->state() == Qt::GestureFinished) {
1046 //if a pinch gesture is in progress we don't want a TabAndHold gesture
1047 if (m_pinchGestureInProgress) {
1048 return;
1049 }
1050 m_pressedMousePos = transform.map(event->mapToGraphicsScene(tap->position()));
1051 m_pressedIndex = m_view->itemAt(m_pressedMousePos);
1052
1053 if (m_pressedIndex.has_value() && !m_selectionManager->isSelected(m_pressedIndex.value())) {
1054 m_selectionManager->clearSelection();
1055 m_selectionManager->setSelected(m_pressedIndex.value());
1056 } else if (!m_pressedIndex.has_value()) {
1057 m_selectionManager->clearSelection();
1058 startRubberBand();
1059 }
1060
1061 Q_EMIT scrollerStop();
1062
1063 m_view->m_tapAndHoldIndicator->setStartPosition(m_pressedMousePos);
1064 m_view->m_tapAndHoldIndicator->setActive(true);
1065
1066 m_dragActionOrRightClick = true;
1067 }
1068 }
1069
1070 void KItemListController::pinchTriggered(QGestureEvent* event, const QTransform& transform)
1071 {
1072 Q_UNUSED(transform)
1073
1074 const QPinchGesture* pinch = static_cast<QPinchGesture*>(event->gesture(Qt::PinchGesture));
1075 const qreal sensitivityModifier = 0.2;
1076 static qreal counter = 0;
1077
1078 if (pinch->state() == Qt::GestureStarted) {
1079 m_pinchGestureInProgress = true;
1080 counter = 0;
1081 }
1082 if (pinch->state() == Qt::GestureUpdated) {
1083 //if a swipe gesture was recognized or in progress, we don't want a pinch gesture to change the zoom
1084 if (m_isSwipeGesture) {
1085 return;
1086 }
1087 counter = counter + (pinch->scaleFactor() - 1);
1088 if (counter >= sensitivityModifier) {
1089 Q_EMIT increaseZoom();
1090 counter = 0;
1091 } else if (counter <= -sensitivityModifier) {
1092 Q_EMIT decreaseZoom();
1093 counter = 0;
1094 }
1095 }
1096 }
1097
1098 void KItemListController::swipeTriggered(QGestureEvent* event, const QTransform& transform)
1099 {
1100 Q_UNUSED(transform)
1101
1102 const KTwoFingerSwipe* swipe = static_cast<KTwoFingerSwipe*>(event->gesture(m_swipeGesture));
1103
1104 if (!swipe) {
1105 return;
1106 }
1107 if (swipe->state() == Qt::GestureStarted) {
1108 m_isSwipeGesture = true;
1109 }
1110
1111 if (swipe->state() == Qt::GestureCanceled) {
1112 m_isSwipeGesture = false;
1113 }
1114
1115 if (swipe->state() == Qt::GestureFinished) {
1116 Q_EMIT scrollerStop();
1117
1118 if (swipe->swipeAngle() <= 20 || swipe->swipeAngle() >= 340) {
1119 Q_EMIT mouseButtonPressed(m_pressedIndex.value_or(-1), Qt::BackButton);
1120 } else if (swipe->swipeAngle() <= 200 && swipe->swipeAngle() >= 160) {
1121 Q_EMIT mouseButtonPressed(m_pressedIndex.value_or(-1), Qt::ForwardButton);
1122 } else if (swipe->swipeAngle() <= 110 && swipe->swipeAngle() >= 60) {
1123 Q_EMIT swipeUp();
1124 }
1125 m_isSwipeGesture = true;
1126 }
1127 }
1128
1129 void KItemListController::twoFingerTapTriggered(QGestureEvent* event, const QTransform& transform)
1130 {
1131 const KTwoFingerTap* twoTap = static_cast<KTwoFingerTap*>(event->gesture(m_twoFingerTapGesture));
1132
1133 if (!twoTap) {
1134 return;
1135 }
1136
1137 if (twoTap->state() == Qt::GestureStarted) {
1138 m_pressedMousePos = transform.map(twoTap->pos());
1139 m_pressedIndex = m_view->itemAt(m_pressedMousePos);
1140 if (m_pressedIndex.has_value()) {
1141 onPress(twoTap->screenPos().toPoint(), twoTap->pos().toPoint(), Qt::ControlModifier, Qt::LeftButton);
1142 onRelease(transform.map(twoTap->pos()), Qt::ControlModifier, Qt::LeftButton, false);
1143 }
1144
1145 }
1146 }
1147
1148 bool KItemListController::processEvent(QEvent* event, const QTransform& transform)
1149 {
1150 if (!event) {
1151 return false;
1152 }
1153
1154 switch (event->type()) {
1155 case QEvent::KeyPress:
1156 return keyPressEvent(static_cast<QKeyEvent*>(event));
1157 case QEvent::InputMethod:
1158 return inputMethodEvent(static_cast<QInputMethodEvent*>(event));
1159 case QEvent::GraphicsSceneMousePress:
1160 return mousePressEvent(static_cast<QGraphicsSceneMouseEvent*>(event), QTransform());
1161 case QEvent::GraphicsSceneMouseMove:
1162 return mouseMoveEvent(static_cast<QGraphicsSceneMouseEvent*>(event), QTransform());
1163 case QEvent::GraphicsSceneMouseRelease:
1164 return mouseReleaseEvent(static_cast<QGraphicsSceneMouseEvent*>(event), QTransform());
1165 case QEvent::GraphicsSceneMouseDoubleClick:
1166 return mouseDoubleClickEvent(static_cast<QGraphicsSceneMouseEvent*>(event), QTransform());
1167 case QEvent::GraphicsSceneWheel:
1168 return wheelEvent(static_cast<QGraphicsSceneWheelEvent*>(event), QTransform());
1169 case QEvent::GraphicsSceneDragEnter:
1170 return dragEnterEvent(static_cast<QGraphicsSceneDragDropEvent*>(event), QTransform());
1171 case QEvent::GraphicsSceneDragLeave:
1172 return dragLeaveEvent(static_cast<QGraphicsSceneDragDropEvent*>(event), QTransform());
1173 case QEvent::GraphicsSceneDragMove:
1174 return dragMoveEvent(static_cast<QGraphicsSceneDragDropEvent*>(event), QTransform());
1175 case QEvent::GraphicsSceneDrop:
1176 return dropEvent(static_cast<QGraphicsSceneDragDropEvent*>(event), QTransform());
1177 case QEvent::GraphicsSceneHoverEnter:
1178 return hoverEnterEvent(static_cast<QGraphicsSceneHoverEvent*>(event), QTransform());
1179 case QEvent::GraphicsSceneHoverMove:
1180 return hoverMoveEvent(static_cast<QGraphicsSceneHoverEvent*>(event), QTransform());
1181 case QEvent::GraphicsSceneHoverLeave:
1182 return hoverLeaveEvent(static_cast<QGraphicsSceneHoverEvent*>(event), QTransform());
1183 case QEvent::GraphicsSceneResize:
1184 return resizeEvent(static_cast<QGraphicsSceneResizeEvent*>(event), transform);
1185 case QEvent::Gesture:
1186 return gestureEvent(static_cast<QGestureEvent*>(event), transform);
1187 case QEvent::TouchBegin:
1188 return touchBeginEvent(static_cast<QTouchEvent*>(event), transform);
1189 default:
1190 break;
1191 }
1192
1193 return false;
1194 }
1195
1196 void KItemListController::slotViewScrollOffsetChanged(qreal current, qreal previous)
1197 {
1198 if (!m_view) {
1199 return;
1200 }
1201
1202 KItemListRubberBand* rubberBand = m_view->rubberBand();
1203 if (rubberBand->isActive()) {
1204 const qreal diff = current - previous;
1205 // TODO: Ideally just QCursor::pos() should be used as
1206 // new end-position but it seems there is no easy way
1207 // to have something like QWidget::mapFromGlobal() for QGraphicsWidget
1208 // (... or I just missed an easy way to do the mapping)
1209 QPointF endPos = rubberBand->endPosition();
1210 if (m_view->scrollOrientation() == Qt::Vertical) {
1211 endPos.ry() += diff;
1212 } else {
1213 endPos.rx() += diff;
1214 }
1215
1216 rubberBand->setEndPosition(endPos);
1217 }
1218 }
1219
1220 void KItemListController::slotRubberBandChanged()
1221 {
1222 if (!m_view || !m_model || m_model->count() <= 0) {
1223 return;
1224 }
1225
1226 const KItemListRubberBand* rubberBand = m_view->rubberBand();
1227 const QPointF startPos = rubberBand->startPosition();
1228 const QPointF endPos = rubberBand->endPosition();
1229 QRectF rubberBandRect = QRectF(startPos, endPos).normalized();
1230
1231 const bool scrollVertical = (m_view->scrollOrientation() == Qt::Vertical);
1232 if (scrollVertical) {
1233 rubberBandRect.translate(0, -m_view->scrollOffset());
1234 } else {
1235 rubberBandRect.translate(-m_view->scrollOffset(), 0);
1236 }
1237
1238 if (!m_oldSelection.isEmpty()) {
1239 // Clear the old selection that was available before the rubberband has
1240 // been activated in case if no Shift- or Control-key are pressed
1241 const bool shiftOrControlPressed = QApplication::keyboardModifiers() & Qt::ShiftModifier ||
1242 QApplication::keyboardModifiers() & Qt::ControlModifier;
1243 if (!shiftOrControlPressed) {
1244 m_oldSelection.clear();
1245 }
1246 }
1247
1248 KItemSet selectedItems;
1249
1250 // Select all visible items that intersect with the rubberband
1251 const auto widgets = m_view->visibleItemListWidgets();
1252 for (const KItemListWidget* widget : widgets) {
1253 const int index = widget->index();
1254
1255 const QRectF widgetRect = m_view->itemRect(index);
1256 if (widgetRect.intersects(rubberBandRect)) {
1257 const QRectF iconRect = widget->iconRect().translated(widgetRect.topLeft());
1258 const QRectF textRect = widget->textRect().translated(widgetRect.topLeft());
1259 if (iconRect.intersects(rubberBandRect) || textRect.intersects(rubberBandRect)) {
1260 selectedItems.insert(index);
1261 }
1262 }
1263 }
1264
1265 // Select all invisible items that intersect with the rubberband. Instead of
1266 // iterating all items only the area which might be touched by the rubberband
1267 // will be checked.
1268 const bool increaseIndex = scrollVertical ?
1269 startPos.y() > endPos.y(): startPos.x() > endPos.x();
1270
1271 int index = increaseIndex ? m_view->lastVisibleIndex() + 1 : m_view->firstVisibleIndex() - 1;
1272 bool selectionFinished = false;
1273 do {
1274 const QRectF widgetRect = m_view->itemRect(index);
1275 if (widgetRect.intersects(rubberBandRect)) {
1276 selectedItems.insert(index);
1277 }
1278
1279 if (increaseIndex) {
1280 ++index;
1281 selectionFinished = (index >= m_model->count()) ||
1282 ( scrollVertical && widgetRect.top() > rubberBandRect.bottom()) ||
1283 (!scrollVertical && widgetRect.left() > rubberBandRect.right());
1284 } else {
1285 --index;
1286 selectionFinished = (index < 0) ||
1287 ( scrollVertical && widgetRect.bottom() < rubberBandRect.top()) ||
1288 (!scrollVertical && widgetRect.right() < rubberBandRect.left());
1289 }
1290 } while (!selectionFinished);
1291
1292 if (QApplication::keyboardModifiers() & Qt::ControlModifier) {
1293 // If Control is pressed, the selection state of all items in the rubberband is toggled.
1294 // Therefore, the new selection contains:
1295 // 1. All previously selected items which are not inside the rubberband, and
1296 // 2. all items inside the rubberband which have not been selected previously.
1297 m_selectionManager->setSelectedItems(m_oldSelection ^ selectedItems);
1298 }
1299 else {
1300 m_selectionManager->setSelectedItems(selectedItems + m_oldSelection);
1301 }
1302 }
1303
1304 void KItemListController::startDragging()
1305 {
1306 if (!m_view || !m_model) {
1307 return;
1308 }
1309
1310 const KItemSet selectedItems = m_selectionManager->selectedItems();
1311 if (selectedItems.isEmpty()) {
1312 return;
1313 }
1314
1315 QMimeData* data = m_model->createMimeData(selectedItems);
1316 if (!data) {
1317 return;
1318 }
1319
1320 // The created drag object will be owned and deleted
1321 // by QApplication::activeWindow().
1322 QDrag* drag = new QDrag(QApplication::activeWindow());
1323 drag->setMimeData(data);
1324
1325 const QPixmap pixmap = m_view->createDragPixmap(selectedItems);
1326 drag->setPixmap(pixmap);
1327
1328 const QPoint hotSpot((pixmap.width() / pixmap.devicePixelRatio()) / 2, 0);
1329 drag->setHotSpot(hotSpot);
1330
1331 drag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction);
1332
1333 QAccessibleEvent accessibilityEvent(view(), QAccessible::DragDropStart);
1334 QAccessible::updateAccessibility(&accessibilityEvent);
1335 }
1336
1337 KItemListWidget* KItemListController::hoveredWidget() const
1338 {
1339 Q_ASSERT(m_view);
1340
1341 const auto widgets = m_view->visibleItemListWidgets();
1342 for (KItemListWidget* widget : widgets) {
1343 if (widget->isHovered()) {
1344 return widget;
1345 }
1346 }
1347
1348 return nullptr;
1349 }
1350
1351 KItemListWidget* KItemListController::widgetForPos(const QPointF& pos) const
1352 {
1353 Q_ASSERT(m_view);
1354
1355 const auto widgets = m_view->visibleItemListWidgets();
1356 for (KItemListWidget* widget : widgets) {
1357 const QPointF mappedPos = widget->mapFromItem(m_view, pos);
1358 if (widget->contains(mappedPos) || widget->selectionRect().contains(mappedPos)) {
1359 return widget;
1360 }
1361 }
1362
1363 return nullptr;
1364 }
1365
1366 void KItemListController::updateKeyboardAnchor()
1367 {
1368 const bool validAnchor = m_keyboardAnchorIndex >= 0 &&
1369 m_keyboardAnchorIndex < m_model->count() &&
1370 keyboardAnchorPos(m_keyboardAnchorIndex) == m_keyboardAnchorPos;
1371 if (!validAnchor) {
1372 const int index = m_selectionManager->currentItem();
1373 m_keyboardAnchorIndex = index;
1374 m_keyboardAnchorPos = keyboardAnchorPos(index);
1375 }
1376 }
1377
1378 int KItemListController::nextRowIndex(int index) const
1379 {
1380 if (m_keyboardAnchorIndex < 0) {
1381 return index;
1382 }
1383
1384 const int maxIndex = m_model->count() - 1;
1385 if (index == maxIndex) {
1386 return index;
1387 }
1388
1389 // Calculate the index of the last column inside the row of the current index
1390 int lastColumnIndex = index;
1391 while (keyboardAnchorPos(lastColumnIndex + 1) > keyboardAnchorPos(lastColumnIndex)) {
1392 ++lastColumnIndex;
1393 if (lastColumnIndex >= maxIndex) {
1394 return index;
1395 }
1396 }
1397
1398 // Based on the last column index go to the next row and calculate the nearest index
1399 // that is below the current index
1400 int nextRowIndex = lastColumnIndex + 1;
1401 int searchIndex = nextRowIndex;
1402 qreal minDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(nextRowIndex));
1403 while (searchIndex < maxIndex && keyboardAnchorPos(searchIndex + 1) > keyboardAnchorPos(searchIndex)) {
1404 ++searchIndex;
1405 const qreal searchDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(searchIndex));
1406 if (searchDiff < minDiff) {
1407 minDiff = searchDiff;
1408 nextRowIndex = searchIndex;
1409 }
1410 }
1411
1412 return nextRowIndex;
1413 }
1414
1415 int KItemListController::previousRowIndex(int index) const
1416 {
1417 if (m_keyboardAnchorIndex < 0 || index == 0) {
1418 return index;
1419 }
1420
1421 // Calculate the index of the first column inside the row of the current index
1422 int firstColumnIndex = index;
1423 while (keyboardAnchorPos(firstColumnIndex - 1) < keyboardAnchorPos(firstColumnIndex)) {
1424 --firstColumnIndex;
1425 if (firstColumnIndex <= 0) {
1426 return index;
1427 }
1428 }
1429
1430 // Based on the first column index go to the previous row and calculate the nearest index
1431 // that is above the current index
1432 int previousRowIndex = firstColumnIndex - 1;
1433 int searchIndex = previousRowIndex;
1434 qreal minDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(previousRowIndex));
1435 while (searchIndex > 0 && keyboardAnchorPos(searchIndex - 1) < keyboardAnchorPos(searchIndex)) {
1436 --searchIndex;
1437 const qreal searchDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(searchIndex));
1438 if (searchDiff < minDiff) {
1439 minDiff = searchDiff;
1440 previousRowIndex = searchIndex;
1441 }
1442 }
1443
1444 return previousRowIndex;
1445 }
1446
1447 qreal KItemListController::keyboardAnchorPos(int index) const
1448 {
1449 const QRectF itemRect = m_view->itemRect(index);
1450 if (!itemRect.isEmpty()) {
1451 return (m_view->scrollOrientation() == Qt::Vertical) ? itemRect.x() : itemRect.y();
1452 }
1453
1454 return 0;
1455 }
1456
1457 void KItemListController::updateExtendedSelectionRegion()
1458 {
1459 if (m_view) {
1460 const bool extend = (m_selectionBehavior != MultiSelection);
1461 KItemListStyleOption option = m_view->styleOption();
1462 if (option.extendedSelectionRegion != extend) {
1463 option.extendedSelectionRegion = extend;
1464 m_view->setStyleOption(option);
1465 }
1466 }
1467 }
1468
1469 bool KItemListController::onPress(const QPoint& screenPos, const QPointF& pos, const Qt::KeyboardModifiers modifiers, const Qt::MouseButtons buttons)
1470 {
1471 Q_EMIT mouseButtonPressed(m_pressedIndex.value_or(-1), buttons);
1472
1473 if (buttons & (Qt::BackButton | Qt::ForwardButton)) {
1474 // Do not select items when clicking the back/forward buttons, see
1475 // https://bugs.kde.org/show_bug.cgi?id=327412.
1476 return true;
1477 }
1478
1479 if (m_view->isAboveExpansionToggle(m_pressedIndex.value_or(-1), m_pressedMousePos)) {
1480 m_selectionManager->endAnchoredSelection();
1481 m_selectionManager->setCurrentItem(m_pressedIndex.value());
1482 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1483 return true;
1484 }
1485
1486 m_selectionTogglePressed = m_view->isAboveSelectionToggle(m_pressedIndex.value_or(-1), m_pressedMousePos);
1487 if (m_selectionTogglePressed) {
1488 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
1489 // The previous anchored selection has been finished already in
1490 // KItemListSelectionManager::setSelected(). We can safely change
1491 // the current item and start a new anchored selection now.
1492 m_selectionManager->setCurrentItem(m_pressedIndex.value());
1493 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1494 return true;
1495 }
1496
1497 const bool shiftPressed = modifiers & Qt::ShiftModifier;
1498 const bool controlPressed = modifiers & Qt::ControlModifier;
1499 const bool rightClick = buttons & Qt::RightButton;
1500
1501 // The previous selection is cleared if either
1502 // 1. The selection mode is SingleSelection, or
1503 // 2. the selection mode is MultiSelection, and *none* of the following conditions are met:
1504 // a) Shift or Control are pressed.
1505 // b) The clicked item is selected already. In that case, the user might want to:
1506 // - start dragging multiple items, or
1507 // - open the context menu and perform an action for all selected items.
1508 const bool shiftOrControlPressed = shiftPressed || controlPressed;
1509 const bool pressedItemAlreadySelected = m_pressedIndex.has_value() && m_selectionManager->isSelected(m_pressedIndex.value());
1510 const bool clearSelection = m_selectionBehavior == SingleSelection ||
1511 (!shiftOrControlPressed && !pressedItemAlreadySelected);
1512
1513
1514 // When this method returns false, a rubberBand selection is created using KItemListController::startRubberBand via the caller.
1515 if (clearSelection) {
1516 const int selectedItemsCount = m_selectionManager->selectedItems().count();
1517 m_selectionManager->clearSelection();
1518 // clear and bail when we got an existing multi-selection
1519 if (selectedItemsCount > 1 && m_pressedIndex.has_value()) {
1520 const auto row = m_view->m_visibleItems.value(m_pressedIndex.value());
1521 const auto mappedPos = row->mapFromItem(m_view, pos);
1522 if (pressedItemAlreadySelected || row->iconRect().contains(mappedPos) || row->textRect().contains(mappedPos)) {
1523 // we are indeed inside the text/icon rect, keep m_pressedIndex what it is
1524 // and short-circuit for single-click activation (it will then propagate to onRelease and activate the item)
1525 // or we just keep going for double-click activation
1526 if (m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced) {
1527 if (!pressedItemAlreadySelected) {
1528 // An unselected item was clicked directly while deselecting multiple other items so we select it.
1529 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
1530 m_selectionManager->setCurrentItem(m_pressedIndex.value());
1531 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1532 }
1533 return true; // event handled, don't create rubber band
1534 }
1535 } else {
1536 // we're not inside the text/icon rect, as we've already cleared the selection
1537 // we can just stop here and make sure handlers down the line (i.e. onRelease) don't activate
1538 m_pressedIndex.reset();
1539 // we don't stop event propagation and proceed to create a rubber band and let onRelease
1540 // decide (based on m_pressedIndex) whether we're in a drag (drag => new rubber band, click => don't select the item)
1541 return false;
1542 }
1543 }
1544 } else if (pressedItemAlreadySelected && !shiftOrControlPressed && (buttons & Qt::LeftButton)) {
1545 // The user might want to start dragging multiple items, but if he clicks the item
1546 // in order to trigger it instead, the other selected items must be deselected.
1547 // However, we do not know yet what the user is going to do.
1548 // -> remember that the user pressed an item which had been selected already and
1549 // clear the selection in mouseReleaseEvent(), unless the items are dragged.
1550 m_clearSelectionIfItemsAreNotDragged = true;
1551
1552 if (m_selectionManager->selectedItems().count() == 1 && m_view->isAboveText(m_pressedIndex.value_or(-1), m_pressedMousePos)) {
1553 Q_EMIT selectedItemTextPressed(m_pressedIndex.value_or(-1));
1554 }
1555 }
1556
1557 if (!shiftPressed) {
1558 // Finish the anchored selection before the current index is changed
1559 m_selectionManager->endAnchoredSelection();
1560 }
1561
1562 if (rightClick) {
1563
1564 // Do header hit check and short circuit before commencing any state changing effects
1565 if (m_view->headerBoundaries().contains(pos)) {
1566 Q_EMIT headerContextMenuRequested(screenPos);
1567 return true;
1568 }
1569
1570 // Stop rubber band from persisting after right-clicks
1571 KItemListRubberBand* rubberBand = m_view->rubberBand();
1572 if (rubberBand->isActive()) {
1573 disconnect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged);
1574 rubberBand->setActive(false);
1575 m_view->setAutoScroll(false);
1576 }
1577 }
1578
1579 if (m_pressedIndex.has_value()) {
1580 m_selectionManager->setCurrentItem(m_pressedIndex.value());
1581 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
1582 const bool hitTargetIsRowEmptyRegion = !row->contains(row->mapFromItem(m_view, pos));
1583 // again, when this method returns false, a rubberBand selection is created as the event is not consumed;
1584 // createRubberBand here tells us whether to return true or false.
1585 bool createRubberBand = (hitTargetIsRowEmptyRegion && m_selectionManager->selectedItems().isEmpty());
1586
1587 if (rightClick && hitTargetIsRowEmptyRegion) {
1588 // we got a right click outside the text rect, default to action on the current url and not the pressed item
1589 Q_EMIT itemContextMenuRequested(m_pressedIndex.value(), screenPos);
1590 return true;
1591 }
1592
1593 switch (m_selectionBehavior) {
1594 case NoSelection:
1595 break;
1596
1597 case SingleSelection:
1598 m_selectionManager->setSelected(m_pressedIndex.value());
1599 break;
1600
1601 case MultiSelection:
1602 if (controlPressed && !shiftPressed) {
1603 // A mouse button press is happening on an item while control is pressed. This either means a user wants to:
1604 // - toggle the selection of item(s) or
1605 // - they want to begin a drag on the item(s) to copy them.
1606 // We rule out the latter, if the item is not clicked directly and was unselected previously.
1607 const auto row = m_view->m_visibleItems.value(m_pressedIndex.value());
1608 const auto mappedPos = row->mapFromItem(m_view, pos);
1609 if (!row->iconRect().contains(mappedPos) && !row->textRect().contains(mappedPos) && !pressedItemAlreadySelected) {
1610 createRubberBand = true;
1611 } else {
1612 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
1613 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1614 createRubberBand = false; // multi selection, don't propagate any further
1615 // This will be the start of an item drag-to-copy operation if the user now moves the mouse before releasing the mouse button.
1616 }
1617 } else if (!shiftPressed || !m_selectionManager->isAnchoredSelectionActive()) {
1618 // Select the pressed item and start a new anchored selection
1619 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Select);
1620 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1621 }
1622 break;
1623
1624 default:
1625 Q_ASSERT(false);
1626 break;
1627 }
1628
1629 if (rightClick) {
1630 Q_EMIT itemContextMenuRequested(m_pressedIndex.value(), screenPos);
1631 }
1632 return !createRubberBand;
1633 }
1634
1635 if (rightClick) {
1636 // header right click handling would have been done before this so just normal context
1637 // menu here is fine
1638 Q_EMIT viewContextMenuRequested(screenPos);
1639 return true;
1640 }
1641
1642 return false;
1643 }
1644
1645 bool KItemListController::onRelease(const QPointF& pos, const Qt::KeyboardModifiers modifiers, const Qt::MouseButtons buttons, bool touch)
1646 {
1647 const bool isAboveSelectionToggle = m_view->isAboveSelectionToggle(m_pressedIndex.value_or(-1), m_pressedMousePos);
1648 if (isAboveSelectionToggle) {
1649 m_selectionTogglePressed = false;
1650 return true;
1651 }
1652
1653 if (!isAboveSelectionToggle && m_selectionTogglePressed) {
1654 m_selectionManager->setSelected(m_pressedIndex.value_or(-1), 1, KItemListSelectionManager::Toggle);
1655 m_selectionTogglePressed = false;
1656 return true;
1657 }
1658
1659 const bool controlPressed = modifiers & Qt::ControlModifier;
1660 const bool shiftOrControlPressed = modifiers & Qt::ShiftModifier ||
1661 controlPressed;
1662
1663 const std::optional<int> index = m_view->itemAt(pos);
1664
1665 KItemListRubberBand* rubberBand = m_view->rubberBand();
1666 bool rubberBandRelease = false;
1667 if (rubberBand->isActive()) {
1668 disconnect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged);
1669 rubberBand->setActive(false);
1670 m_oldSelection.clear();
1671 m_view->setAutoScroll(false);
1672 rubberBandRelease = true;
1673 // We check for actual rubber band drag here: if delta between start and end is less than drag threshold,
1674 // then we have a single click on one of the rows
1675 if ((rubberBand->endPosition() - rubberBand->startPosition()).manhattanLength() < QApplication::startDragDistance()) {
1676 rubberBandRelease = false; // since we're only selecting, unmark rubber band release flag
1677 // m_pressedIndex will have no value if we came from a multi-selection clearing onPress
1678 // in that case, we don't select anything
1679 if (index.has_value() && m_pressedIndex.has_value()) {
1680 if (controlPressed && m_selectionBehavior == MultiSelection) {
1681 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
1682 } else {
1683 m_selectionManager->setSelected(index.value());
1684 }
1685 if (!m_selectionManager->isAnchoredSelectionActive()) {
1686 m_selectionManager->beginAnchoredSelection(index.value());
1687 }
1688 }
1689 }
1690 }
1691
1692 if (index.has_value() && index == m_pressedIndex) {
1693 // The release event is done above the same item as the press event
1694
1695 if (m_clearSelectionIfItemsAreNotDragged) {
1696 // A selected item has been clicked, but no drag operation has been started
1697 // -> clear the rest of the selection.
1698 m_selectionManager->clearSelection();
1699 m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Select);
1700 m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
1701 }
1702
1703 if (buttons & Qt::LeftButton) {
1704 bool emitItemActivated = true;
1705 if (m_view->isAboveExpansionToggle(index.value(), pos)) {
1706 const bool expanded = m_model->isExpanded(index.value());
1707 m_model->setExpanded(index.value(), !expanded);
1708
1709 Q_EMIT itemExpansionToggleClicked(index.value());
1710 emitItemActivated = false;
1711 } else if (shiftOrControlPressed && m_selectionBehavior != SingleSelection) {
1712 // The mouse click should only update the selection, not trigger the item, except when
1713 // we are in single selection mode
1714 emitItemActivated = false;
1715 } else {
1716 const bool singleClickActivation = m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced;
1717 if (!singleClickActivation) {
1718 emitItemActivated = touch;
1719 } else {
1720 // activate on single click only if we didn't come from a rubber band release
1721 emitItemActivated = !rubberBandRelease;
1722 }
1723 }
1724 if (emitItemActivated) {
1725 Q_EMIT itemActivated(index.value());
1726 }
1727 } else if (buttons & Qt::MiddleButton) {
1728 Q_EMIT itemMiddleClicked(index.value());
1729 }
1730 }
1731
1732 m_pressedMousePos = QPointF();
1733 m_pressedIndex = std::nullopt;
1734 m_clearSelectionIfItemsAreNotDragged = false;
1735 return false;
1736 }
1737
1738 void KItemListController::startRubberBand()
1739 {
1740 if (m_selectionBehavior == MultiSelection) {
1741 QPointF startPos = m_pressedMousePos;
1742 if (m_view->scrollOrientation() == Qt::Vertical) {
1743 startPos.ry() += m_view->scrollOffset();
1744 if (m_view->itemSize().width() < 0) {
1745 // Use a special rubberband for views that have only one column and
1746 // expand the rubberband to use the whole width of the view.
1747 startPos.setX(0);
1748 }
1749 } else {
1750 startPos.rx() += m_view->scrollOffset();
1751 }
1752
1753 m_oldSelection = m_selectionManager->selectedItems();
1754 KItemListRubberBand* rubberBand = m_view->rubberBand();
1755 rubberBand->setStartPosition(startPos);
1756 rubberBand->setEndPosition(startPos);
1757 rubberBand->setActive(true);
1758 connect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged);
1759 m_view->setAutoScroll(true);
1760 }
1761 }
1762
1763 void KItemListController::slotStateChanged(QScroller::State newState)
1764 {
1765 if (newState == QScroller::Scrolling) {
1766 m_scrollerIsScrolling = true;
1767 } else if (newState == QScroller::Inactive) {
1768 m_scrollerIsScrolling = false;
1769 }
1770 }