#include <KTwoFingerSwipe>
#include <KTwoFingerTap>
+#include <KUrlMimeData>
#include <QAccessible>
#include <QApplication>
#include <QGraphicsSceneEvent>
#include <QGraphicsView>
#include <QMimeData>
+#include <QStyleHints>
#include <QTimer>
#include <QTouchEvent>
KItemListController::KItemListController(KItemModelBase* model, KItemListView* view, QObject* parent) :
QObject(parent),
m_singleClickActivationEnforced(false),
+ m_selectionMode(false),
m_selectionTogglePressed(false),
m_clearSelectionIfItemsAreNotDragged(false),
m_isSwipeGesture(false),
m_pressedIndex(std::nullopt),
m_pressedMousePos(),
m_autoActivationTimer(nullptr),
+ m_longPressDetectionTimer(nullptr),
m_swipeGesture(Qt::CustomGesture),
m_twoFingerTapGesture(Qt::CustomGesture),
m_oldSelection(),
m_autoActivationTimer->setInterval(-1);
connect(m_autoActivationTimer, &QTimer::timeout, this, &KItemListController::slotAutoActivationTimeout);
+ m_longPressDetectionTimer = new QTimer(this);
+ m_longPressDetectionTimer->setSingleShot(true);
+ m_longPressDetectionTimer->setInterval(QGuiApplication::styleHints()->mousePressAndHoldInterval());
+ connect(m_longPressDetectionTimer, &QTimer::timeout, this, [this]() {
+ if (!m_selectionMode) {
+ Q_EMIT selectionModeChangeRequested(true);
+ }
+ });
+
setModel(model);
setView(view);
return m_singleClickActivationEnforced;
}
+void KItemListController::setSelectionModeEnabled(bool enabled)
+{
+ m_selectionMode = enabled;
+}
+
+bool KItemListController::selectionMode() const
+{
+ return m_selectionMode;
+}
+
bool KItemListController::keyPressEvent(QKeyEvent* event)
{
int index = m_selectionManager->currentItem();
}
case Qt::Key_Escape:
+ if (m_selectionMode && m_selectionManager->selectedItems().count() < 1) {
+ Q_EMIT selectionModeChangeRequested(false);
+ }
if (m_selectionBehavior != SingleSelection) {
m_selectionManager->clearSelection();
}
return false;
}
+ const QPointF pos = transform.map(event->pos());
+ if ((pos - m_pressedMousePos).manhattanLength() >= QApplication::startDragDistance()) {
+ m_longPressDetectionTimer->stop();
+ }
+
if (m_pressedIndex.has_value() && !m_view->rubberBand()->isActive()) {
// Check whether a dragging should be started
if (event->buttons() & Qt::LeftButton) {
- const QPointF pos = transform.map(event->pos());
if ((pos - m_pressedMousePos).manhattanLength() >= QApplication::startDragDistance()) {
if (!m_selectionManager->isSelected(m_pressedIndex.value())) {
// Always assure that the dragged item gets selected. Usually this is already
m_view->m_tapAndHoldIndicator->setActive(false);
}
+ m_longPressDetectionTimer->stop();
+
KItemListRubberBand* rubberBand = m_view->rubberBand();
if (event->source() == Qt::MouseEventSynthesizedByQt && !rubberBand->isActive() && m_isTouchEvent) {
return false;
KItemListWidget* oldHoveredWidget = hoveredWidget();
const QPointF pos = transform.map(event->pos());
- KItemListWidget* newHoveredWidget = widgetForPos(pos);
+ KItemListWidget* newHoveredWidget = widgetForDropPos(pos);
if (oldHoveredWidget != newHoveredWidget) {
m_autoActivationTimer->stop();
Q_EMIT aboveItemDropEvent(dropAboveIndex, event);
} else if (!event->mimeData()->hasFormat(m_model->blacklistItemDropEventMimeType())) {
// Something has been dropped on an item or on an empty part of the view.
- Q_EMIT itemDropEvent(m_view->itemAt(pos).value_or(-1), event);
+ const KItemListWidget *receivingWidget = widgetForDropPos(pos);
+ if (receivingWidget) {
+ Q_EMIT itemDropEvent(receivingWidget->index(), event);
+ } else {
+ Q_EMIT itemDropEvent(-1, event);
+ }
}
QAccessibleEvent accessibilityEvent(view(), QAccessible::DragDropEnd);
// been activated in case if no Shift- or Control-key are pressed
const bool shiftOrControlPressed = QApplication::keyboardModifiers() & Qt::ShiftModifier ||
QApplication::keyboardModifiers() & Qt::ControlModifier;
- if (!shiftOrControlPressed) {
+ if (!shiftOrControlPressed && !m_selectionMode) {
m_oldSelection.clear();
}
}
}
} while (!selectionFinished);
- if (QApplication::keyboardModifiers() & Qt::ControlModifier) {
+ if ((QApplication::keyboardModifiers() & Qt::ControlModifier) || m_selectionMode) {
// If Control is pressed, the selection state of all items in the rubberband is toggled.
// Therefore, the new selection contains:
// 1. All previously selected items which are not inside the rubberband, and
return;
}
- QMimeData* data = m_model->createMimeData(selectedItems);
+ QMimeData *data = m_model->createMimeData(selectedItems);
if (!data) {
return;
}
+ KUrlMimeData::exportUrlsToPortal(data);
// The created drag object will be owned and deleted
// by QApplication::activeWindow().
return nullptr;
}
+KItemListWidget* KItemListController::widgetForDropPos(const QPointF& pos) const
+{
+ Q_ASSERT(m_view);
+
+ const auto widgets = m_view->visibleItemListWidgets();
+ for (KItemListWidget* widget : widgets) {
+ const QPointF mappedPos = widget->mapFromItem(m_view, pos);
+ if (widget->contains(mappedPos)) {
+ return widget;
+ }
+ }
+
+ return nullptr;
+}
+
void KItemListController::updateKeyboardAnchor()
{
const bool validAnchor = m_keyboardAnchorIndex >= 0 &&
}
const bool shiftPressed = modifiers & Qt::ShiftModifier;
- const bool controlPressed = modifiers & Qt::ControlModifier;
+ 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;
+ if (leftClick) {
+ m_longPressDetectionTimer->start();
+ }
+
// 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:
return false;
}
}
- } else if (pressedItemAlreadySelected && !shiftOrControlPressed && (buttons & Qt::LeftButton)) {
+ } 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.
}
if (m_pressedIndex.has_value()) {
+ // The hover highlight area of an item is being pressed.
m_selectionManager->setCurrentItem(m_pressedIndex.value());
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->contains(row->mapFromItem(m_view, pos));
bool createRubberBand = (hitTargetIsRowEmptyRegion && m_selectionManager->selectedItems().isEmpty());
if (rightClick && hitTargetIsRowEmptyRegion) {
- // we got a right click outside the text rect, default to action on the current url and not the pressed item
- Q_EMIT itemContextMenuRequested(m_pressedIndex.value(), screenPos);
+ // We have a right click outside the icon and text rect but within the hover highlight area
+ // but it is unclear if this means that a selection rectangle for an item was clicked or the background of the view.
+ if (m_selectionManager->selectedItems().contains(m_pressedIndex.value())) {
+ // The selection rectangle for an item was clicked
+ Q_EMIT itemContextMenuRequested(m_pressedIndex.value(), screenPos);
+ } else {
+ row->setHovered(false); // Removes the hover highlight so the context menu doesn't look like it applies to the row.
+ Q_EMIT viewContextMenuRequested(screenPos);
+ }
return true;
}
break;
case MultiSelection:
- if (controlPressed && !shiftPressed) {
- // A mouse button press is happening on an item while control is pressed. This either means a user wants to:
+ 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.