+bool KItemListController::onPress(const QPointF &pos, const Qt::KeyboardModifiers modifiers, const Qt::MouseButtons buttons)
+{
+ Q_EMIT mouseButtonPressed(m_pressedIndex.value_or(-1), buttons);
+
+ if (buttons & (Qt::BackButton | Qt::ForwardButton)) {
+ // Do not select items when clicking the back/forward buttons, see
+ // https://bugs.kde.org/show_bug.cgi?id=327412.
+ return true;
+ }
+
+ const QPointF pressedMousePos = m_view->transform().map(pos);
+
+ if (m_view->isAboveExpansionToggle(m_pressedIndex.value_or(-1), pressedMousePos)) {
+ m_selectionManager->endAnchoredSelection();
+ m_selectionManager->setCurrentItem(m_pressedIndex.value());
+ m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
+ return true;
+ }
+
+ m_selectionTogglePressed = m_view->isAboveSelectionToggle(m_pressedIndex.value_or(-1), pressedMousePos);
+ if (m_selectionTogglePressed) {
+ m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
+ // The previous anchored selection has been finished already in
+ // KItemListSelectionManager::setSelected(). We can safely change
+ // the current item and start a new anchored selection now.
+ m_selectionManager->setCurrentItem(m_pressedIndex.value());
+ m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
+ return true;
+ }
+
+ const bool shiftPressed = modifiers & Qt::ShiftModifier;
+ const bool controlPressed = (modifiers & Qt::ControlModifier) || m_selectionMode; // Keeping selectionMode similar to pressing control will hopefully
+ // simplify the overall logic and possibilities both for users and devs.
+ const bool leftClick = buttons & Qt::LeftButton;
+ const bool rightClick = buttons & Qt::RightButton;
+ const bool singleClickActivation = m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick);
+
+ // The previous selection is cleared if either
+ // 1. The selection mode is SingleSelection, or
+ // 2. the selection mode is MultiSelection, and *none* of the following conditions are met:
+ // a) Shift or Control are pressed.
+ // b) The clicked item is selected already. In that case, the user might want to:
+ // - start dragging multiple items, or
+ // - open the context menu and perform an action for all selected items.
+ const bool shiftOrControlPressed = shiftPressed || controlPressed;
+ const bool pressedItemAlreadySelected = m_pressedIndex.has_value() && m_selectionManager->isSelected(m_pressedIndex.value());
+ const bool clearSelection = m_selectionBehavior == SingleSelection || (!shiftOrControlPressed && !pressedItemAlreadySelected);
+
+ // When this method returns false, a rubberBand selection is created using KItemListController::startRubberBand via the caller.
+ if (clearSelection) {
+ const int selectedItemsCount = m_selectionManager->selectedItems().count();
+ m_selectionManager->clearSelection();
+ // clear and bail when we got an existing multi-selection
+ if (selectedItemsCount > 1 && m_pressedIndex.has_value()) {
+ const auto row = m_view->m_visibleItems.value(m_pressedIndex.value());
+ const auto mappedPos = row->mapFromItem(m_view, pos);
+ if (row->selectionRectCore().contains(mappedPos)) {
+ // we are indeed inside the text/icon rect, keep m_pressedIndex what it is
+ // and short-circuit for single-click activation (it will then propagate to onRelease and activate the item)
+ // or we just keep going for double-click activation
+ if (singleClickActivation || m_singleClickActivationEnforced) {
+ if (!pressedItemAlreadySelected) {
+ // An unselected item was clicked directly while deselecting multiple other items so we mark it "current".
+ m_selectionManager->setCurrentItem(m_pressedIndex.value());
+ m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
+ if (!leftClick) {
+ // We select the item here because this press is not meant to directly activate the item.
+ // We do not want to select items unless the user wants to edit them.
+ m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
+ }
+ }
+ if (leftClick) {
+ row->setPressed(true);
+ }
+ return true; // event handled, don't create rubber band
+ }
+ } else {
+ // we're not inside the text/icon rect, as we've already cleared the selection
+ // we can just stop here and make sure handlers down the line (i.e. onRelease) don't activate
+ m_pressedIndex.reset();
+ // we don't stop event propagation and proceed to create a rubber band and let onRelease
+ // decide (based on m_pressedIndex) whether we're in a drag (drag => new rubber band, click => don't select the item)
+ return false;
+ }
+ }
+ } else if (pressedItemAlreadySelected && !shiftOrControlPressed && leftClick) {
+ // The user might want to start dragging multiple items, but if he clicks the item
+ // in order to trigger it instead, the other selected items must be deselected.
+ // However, we do not know yet what the user is going to do.
+ // -> remember that the user pressed an item which had been selected already and
+ // clear the selection in mouseReleaseEvent(), unless the items are dragged.
+ m_clearSelectionIfItemsAreNotDragged = true;
+
+ if (m_selectionManager->selectedItems().count() == 1 && m_view->isAboveText(m_pressedIndex.value_or(-1), pressedMousePos)) {
+ Q_EMIT selectedItemTextPressed(m_pressedIndex.value_or(-1));
+ }
+ }
+
+ if (!shiftPressed) {
+ // Finish the anchored selection before the current index is changed
+ m_selectionManager->endAnchoredSelection();
+ }
+
+ if (rightClick) {
+ // Stop rubber band from persisting after right-clicks
+ KItemListRubberBand *rubberBand = m_view->rubberBand();
+ if (rubberBand->isActive()) {
+ disconnect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged);
+ rubberBand->setActive(false);
+ m_view->setAutoScroll(false);
+ }
+
+ if (!m_pressedIndex.has_value()) {
+ // We have a right-click in an empty region, don't create rubber band.
+ return true;
+ }
+ }
+
+ if (m_pressedIndex.has_value()) {
+ // The hover highlight area of an item is being pressed.
+ 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
+
+ const bool hitTargetIsRowEmptyRegion = !row->selectionRectCore().contains(row->mapFromItem(m_view, pos));
+ // again, when this method returns false, a rubberBand selection is created as the event is not consumed;
+ // createRubberBand here tells us whether to return true or false.
+ bool createRubberBand = (hitTargetIsRowEmptyRegion && m_selectionManager->selectedItems().isEmpty());
+
+ if (leftClick) {
+ row->setPressed(true);
+ }
+
+ if (rightClick && hitTargetIsRowEmptyRegion) {
+ // We have a right click outside the icon and text rect but within the hover highlight area.
+ // We don't want items to get selected through this, so we return now.
+ return true;
+ }
+
+ m_selectionManager->setCurrentItem(m_pressedIndex.value());
+
+ switch (m_selectionBehavior) {
+ case NoSelection:
+ break;
+
+ case SingleSelection:
+ if (!leftClick || shiftOrControlPressed || (!singleClickActivation && !m_singleClickActivationEnforced)) {
+ m_selectionManager->setSelected(m_pressedIndex.value());
+ }
+ break;
+
+ case MultiSelection:
+ if (controlPressed && !shiftPressed && leftClick) {
+ // A left mouse button press is happening on an item while control is pressed. This either means a user wants to:
+ // - toggle the selection of item(s) or
+ // - they want to begin a drag on the item(s) to copy them.
+ // We rule out the latter, if the item is not clicked directly and was unselected previously.
+ const auto row = m_view->m_visibleItems.value(m_pressedIndex.value());
+ const auto mappedPos = row->mapFromItem(m_view, pos);
+ if (!row->selectionRectCore().contains(mappedPos)) {
+ createRubberBand = true;
+ } else {
+ m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
+ m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
+ createRubberBand = false; // multi selection, don't propagate any further
+ // This will be the start of an item drag-to-copy operation if the user now moves the mouse before releasing the mouse button.
+ }
+ } else if (!shiftPressed || !m_selectionManager->isAnchoredSelectionActive()) {
+ // Select the pressed item and start a new anchored selection
+ if (!leftClick || shiftOrControlPressed || (!singleClickActivation && !m_singleClickActivationEnforced)) {
+ m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Select);
+ }
+ m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
+ }
+ break;
+
+ default:
+ Q_ASSERT(false);
+ break;
+ }
+
+ return !createRubberBand;
+ }
+
+ return false;
+}
+
+bool KItemListController::onRelease(const QPointF &pos, const Qt::KeyboardModifiers modifiers, const Qt::MouseButtons buttons, bool touch)
+{
+ const QPointF pressedMousePos = pos;
+ const bool isAboveSelectionToggle = m_view->isAboveSelectionToggle(m_pressedIndex.value_or(-1), pressedMousePos);
+ if (isAboveSelectionToggle) {
+ m_selectionTogglePressed = false;
+ return true;
+ }
+
+ if (!isAboveSelectionToggle && m_selectionTogglePressed) {
+ m_selectionManager->setSelected(m_pressedIndex.value_or(-1), 1, KItemListSelectionManager::Toggle);
+ m_selectionTogglePressed = false;
+ return true;
+ }
+
+ const bool controlPressed = modifiers & Qt::ControlModifier;
+ const bool shiftOrControlPressed = modifiers & Qt::ShiftModifier || controlPressed;
+
+ const std::optional<int> index = m_view->itemAt(pos);
+
+ KItemListRubberBand *rubberBand = m_view->rubberBand();
+ bool rubberBandRelease = false;
+ if (rubberBand->isActive()) {
+ disconnect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged);
+ rubberBand->setActive(false);
+ m_oldSelection.clear();
+ m_view->setAutoScroll(false);
+ rubberBandRelease = true;
+ // We check for actual rubber band drag here: if delta between start and end is less than drag threshold,
+ // then we have a single click on one of the rows
+ if ((rubberBand->endPosition() - rubberBand->startPosition()).manhattanLength() < QApplication::startDragDistance()) {
+ rubberBandRelease = false; // since we're only selecting, unmark rubber band release flag
+ // m_pressedIndex will have no value if we came from a multi-selection clearing onPress
+ // in that case, we don't select anything
+ if (index.has_value() && m_pressedIndex.has_value()) {
+ if (controlPressed && m_selectionBehavior == MultiSelection) {
+ m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle);
+ } else {
+ m_selectionManager->setSelected(index.value());
+ }
+ if (!m_selectionManager->isAnchoredSelectionActive()) {
+ m_selectionManager->beginAnchoredSelection(index.value());
+ }
+ }
+ }
+ }
+
+ if (index.has_value() && index == m_pressedIndex) {
+ // The release event is done above the same item as the press event
+
+ if (m_clearSelectionIfItemsAreNotDragged) {
+ // A selected item has been clicked, but no drag operation has been started
+ // -> clear the rest of the selection.
+ m_selectionManager->clearSelection();
+ m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Select);
+ m_selectionManager->beginAnchoredSelection(m_pressedIndex.value());
+ }
+
+ if (buttons & Qt::LeftButton) {
+ bool emitItemActivated = true;
+ if (m_view->isAboveExpansionToggle(index.value(), pos)) {
+ const bool expanded = m_model->isExpanded(index.value());
+ m_model->setExpanded(index.value(), !expanded);
+
+ Q_EMIT itemExpansionToggleClicked(index.value());
+ emitItemActivated = false;
+ } else if (shiftOrControlPressed && m_selectionBehavior != SingleSelection) {
+ // The mouse click should only update the selection, not trigger the item, except when
+ // we are in single selection mode
+ emitItemActivated = false;
+ } else {
+ const bool singleClickActivation = m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced;
+ if (!singleClickActivation) {
+ emitItemActivated = touch && !m_selectionMode;
+ } else {
+ // activate on single click only if we didn't come from a rubber band release
+ emitItemActivated = !rubberBandRelease;
+ }
+ }
+ if (emitItemActivated) {
+ Q_EMIT itemActivated(index.value());
+ }
+ } else if (buttons & Qt::MiddleButton) {
+ Q_EMIT itemMiddleClicked(index.value());
+ }
+ }
+
+ m_pressedMouseGlobalPos = QPointF();
+ m_pressedIndex = std::nullopt;
+ m_clearSelectionIfItemsAreNotDragged = false;
+ return false;
+}
+
+void KItemListController::startRubberBand()
+{
+ if (m_selectionBehavior == MultiSelection) {
+ QPoint startPos = m_view->transform().map(m_view->scene()->views().first()->mapFromGlobal(m_pressedMouseGlobalPos.toPoint()));
+ if (m_view->scrollOrientation() == Qt::Vertical) {
+ startPos.ry() += m_view->scrollOffset();
+ } else {
+ startPos.rx() += m_view->scrollOffset();
+ }
+
+ m_oldSelection = m_selectionManager->selectedItems();
+ KItemListRubberBand *rubberBand = m_view->rubberBand();
+ rubberBand->setStartPosition(startPos);
+ rubberBand->setEndPosition(startPos);
+ rubberBand->setActive(true);
+ connect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged);
+ m_view->setAutoScroll(true);
+ }
+}
+
+void KItemListController::slotStateChanged(QScroller::State newState)
+{
+ if (newState == QScroller::Scrolling) {
+ m_scrollerIsScrolling = true;
+ } else if (newState == QScroller::Inactive) {
+ m_scrollerIsScrolling = false;
+ }
+}
+
+#include "moc_kitemlistcontroller.cpp"