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