diff --git a/src/kitemviews/kitemlistcontroller.cpp b/src/kitemviews/kitemlistcontroller.cpp index ad859c52a..8cc9529ef 100644 --- a/src/kitemviews/kitemlistcontroller.cpp +++ b/src/kitemviews/kitemlistcontroller.cpp @@ -1,1354 +1,1356 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * Copyright (C) 2012 by Frank Reininghaus * * * * Based on the Itemviews NG project from Trolltech Labs: * * http://qt.gitorious.org/qt-labs/itemviews-ng * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemlistcontroller.h" #include "kitemlistselectionmanager.h" #include "kitemlistview.h" #include "private/kitemlistkeyboardsearchmanager.h" #include "private/kitemlistrubberband.h" #include "views/draganddrophelper.h" #include #include #include #include #include #include #include #include KItemListController::KItemListController(KItemModelBase* model, KItemListView* view, QObject* parent) : QObject(parent), m_singleClickActivationEnforced(false), m_selectionTogglePressed(false), m_clearSelectionIfItemsAreNotDragged(false), m_selectionBehavior(NoSelection), m_autoActivationBehavior(ActivationAndExpansion), m_mouseDoubleClickAction(ActivateItemOnly), m_model(nullptr), m_view(nullptr), m_selectionManager(new KItemListSelectionManager(this)), m_keyboardManager(new KItemListKeyboardSearchManager(this)), m_pressedIndex(-1), m_pressedMousePos(), m_autoActivationTimer(nullptr), m_oldSelection(), m_keyboardAnchorIndex(-1), m_keyboardAnchorPos(0) { connect(m_keyboardManager, &KItemListKeyboardSearchManager::changeCurrentItem, this, &KItemListController::slotChangeCurrentItem); connect(m_selectionManager, &KItemListSelectionManager::currentChanged, m_keyboardManager, &KItemListKeyboardSearchManager::slotCurrentChanged); + connect(m_selectionManager, &KItemListSelectionManager::selectionChanged, + m_keyboardManager, &KItemListKeyboardSearchManager::slotSelectionChanged); m_autoActivationTimer = new QTimer(this); m_autoActivationTimer->setSingleShot(true); m_autoActivationTimer->setInterval(-1); connect(m_autoActivationTimer, &QTimer::timeout, this, &KItemListController::slotAutoActivationTimeout); setModel(model); setView(view); } KItemListController::~KItemListController() { setView(nullptr); Q_ASSERT(!m_view); setModel(nullptr); Q_ASSERT(!m_model); } void KItemListController::setModel(KItemModelBase* model) { if (m_model == model) { return; } KItemModelBase* oldModel = m_model; if (oldModel) { oldModel->deleteLater(); } m_model = model; if (m_model) { m_model->setParent(this); } if (m_view) { m_view->setModel(m_model); } m_selectionManager->setModel(m_model); emit modelChanged(m_model, oldModel); } KItemModelBase* KItemListController::model() const { return m_model; } KItemListSelectionManager* KItemListController::selectionManager() const { return m_selectionManager; } void KItemListController::setView(KItemListView* view) { if (m_view == view) { return; } KItemListView* oldView = m_view; if (oldView) { disconnect(oldView, &KItemListView::scrollOffsetChanged, this, &KItemListController::slotViewScrollOffsetChanged); oldView->deleteLater(); } m_view = view; if (m_view) { m_view->setParent(this); m_view->setController(this); m_view->setModel(m_model); connect(m_view, &KItemListView::scrollOffsetChanged, this, &KItemListController::slotViewScrollOffsetChanged); updateExtendedSelectionRegion(); } emit viewChanged(m_view, oldView); } KItemListView* KItemListController::view() const { return m_view; } void KItemListController::setSelectionBehavior(SelectionBehavior behavior) { m_selectionBehavior = behavior; updateExtendedSelectionRegion(); } KItemListController::SelectionBehavior KItemListController::selectionBehavior() const { return m_selectionBehavior; } void KItemListController::setAutoActivationBehavior(AutoActivationBehavior behavior) { m_autoActivationBehavior = behavior; } KItemListController::AutoActivationBehavior KItemListController::autoActivationBehavior() const { return m_autoActivationBehavior; } void KItemListController::setMouseDoubleClickAction(MouseDoubleClickAction action) { m_mouseDoubleClickAction = action; } KItemListController::MouseDoubleClickAction KItemListController::mouseDoubleClickAction() const { return m_mouseDoubleClickAction; } int KItemListController::indexCloseToMousePressedPosition() const { QHashIterator it(m_view->m_visibleGroups); while (it.hasNext()) { it.next(); KItemListGroupHeader *groupHeader = it.value(); const QPointF mappedToGroup = groupHeader->mapFromItem(nullptr, m_pressedMousePos); if (groupHeader->contains(mappedToGroup)) { return it.key()->index(); } } return -1; } void KItemListController::setAutoActivationDelay(int delay) { m_autoActivationTimer->setInterval(delay); } int KItemListController::autoActivationDelay() const { return m_autoActivationTimer->interval(); } void KItemListController::setSingleClickActivationEnforced(bool singleClick) { m_singleClickActivationEnforced = singleClick; } bool KItemListController::singleClickActivationEnforced() const { return m_singleClickActivationEnforced; } bool KItemListController::keyPressEvent(QKeyEvent* event) { int index = m_selectionManager->currentItem(); int key = event->key(); // Handle the expanding/collapsing of items if (m_view->supportsItemExpanding() && m_model->isExpandable(index)) { if (key == Qt::Key_Right) { if (m_model->setExpanded(index, true)) { return true; } } else if (key == Qt::Key_Left) { if (m_model->setExpanded(index, false)) { return true; } } } const bool shiftPressed = event->modifiers() & Qt::ShiftModifier; const bool controlPressed = event->modifiers() & Qt::ControlModifier; const bool shiftOrControlPressed = shiftPressed || controlPressed; const bool navigationPressed = key == Qt::Key_Home || key == Qt::Key_End || key == Qt::Key_PageUp || key == Qt::Key_PageDown || key == Qt::Key_Up || key == Qt::Key_Down || key == Qt::Key_Left || key == Qt::Key_Right; const int itemCount = m_model->count(); // For horizontal scroll orientation, transform // the arrow keys to simplify the event handling. if (m_view->scrollOrientation() == Qt::Horizontal) { switch (key) { case Qt::Key_Up: key = Qt::Key_Left; break; case Qt::Key_Down: key = Qt::Key_Right; break; case Qt::Key_Left: key = Qt::Key_Up; break; case Qt::Key_Right: key = Qt::Key_Down; break; default: break; } } const bool selectSingleItem = m_selectionBehavior != NoSelection && itemCount == 1 && navigationPressed; if (selectSingleItem) { const int current = m_selectionManager->currentItem(); m_selectionManager->setSelected(current); return true; } switch (key) { case Qt::Key_Home: index = 0; m_keyboardAnchorIndex = index; m_keyboardAnchorPos = keyboardAnchorPos(index); break; case Qt::Key_End: index = itemCount - 1; m_keyboardAnchorIndex = index; m_keyboardAnchorPos = keyboardAnchorPos(index); break; case Qt::Key_Left: if (index > 0) { const int expandedParentsCount = m_model->expandedParentsCount(index); if (expandedParentsCount == 0) { --index; } else { // Go to the parent of the current item. do { --index; } while (index > 0 && m_model->expandedParentsCount(index) == expandedParentsCount); } m_keyboardAnchorIndex = index; m_keyboardAnchorPos = keyboardAnchorPos(index); } break; case Qt::Key_Right: if (index < itemCount - 1) { ++index; m_keyboardAnchorIndex = index; m_keyboardAnchorPos = keyboardAnchorPos(index); } break; case Qt::Key_Up: updateKeyboardAnchor(); index = previousRowIndex(index); break; case Qt::Key_Down: updateKeyboardAnchor(); index = nextRowIndex(index); break; case Qt::Key_PageUp: if (m_view->scrollOrientation() == Qt::Horizontal) { // The new current index should correspond to the first item in the current column. int newIndex = qMax(index - 1, 0); while (newIndex != index && m_view->itemRect(newIndex).topLeft().y() < m_view->itemRect(index).topLeft().y()) { index = newIndex; newIndex = qMax(index - 1, 0); } m_keyboardAnchorIndex = index; m_keyboardAnchorPos = keyboardAnchorPos(index); } else { const qreal currentItemBottom = m_view->itemRect(index).bottomLeft().y(); const qreal height = m_view->geometry().height(); // The new current item should be the first item in the current // column whose itemRect's top coordinate is larger than targetY. const qreal targetY = currentItemBottom - height; updateKeyboardAnchor(); int newIndex = previousRowIndex(index); do { index = newIndex; updateKeyboardAnchor(); newIndex = previousRowIndex(index); } while (m_view->itemRect(newIndex).topLeft().y() > targetY && newIndex != index); } break; case Qt::Key_PageDown: if (m_view->scrollOrientation() == Qt::Horizontal) { // The new current index should correspond to the last item in the current column. int newIndex = qMin(index + 1, m_model->count() - 1); while (newIndex != index && m_view->itemRect(newIndex).topLeft().y() > m_view->itemRect(index).topLeft().y()) { index = newIndex; newIndex = qMin(index + 1, m_model->count() - 1); } m_keyboardAnchorIndex = index; m_keyboardAnchorPos = keyboardAnchorPos(index); } else { const qreal currentItemTop = m_view->itemRect(index).topLeft().y(); const qreal height = m_view->geometry().height(); // The new current item should be the last item in the current // column whose itemRect's bottom coordinate is smaller than targetY. const qreal targetY = currentItemTop + height; updateKeyboardAnchor(); int newIndex = nextRowIndex(index); do { index = newIndex; updateKeyboardAnchor(); newIndex = nextRowIndex(index); } while (m_view->itemRect(newIndex).bottomLeft().y() < targetY && newIndex != index); } break; case Qt::Key_Enter: case Qt::Key_Return: { const KItemSet selectedItems = m_selectionManager->selectedItems(); if (selectedItems.count() >= 2) { emit itemsActivated(selectedItems); } else if (selectedItems.count() == 1) { emit itemActivated(selectedItems.first()); } else { emit itemActivated(index); } break; } case Qt::Key_Menu: { // Emit the signal itemContextMenuRequested() in case if at least one // item is selected. Otherwise the signal viewContextMenuRequested() will be emitted. const KItemSet selectedItems = m_selectionManager->selectedItems(); int index = -1; if (selectedItems.count() >= 2) { const int currentItemIndex = m_selectionManager->currentItem(); index = selectedItems.contains(currentItemIndex) ? currentItemIndex : selectedItems.first(); } else if (selectedItems.count() == 1) { index = selectedItems.first(); } if (index >= 0) { const QRectF contextRect = m_view->itemContextRect(index); const QPointF pos(m_view->scene()->views().first()->mapToGlobal(contextRect.bottomRight().toPoint())); emit itemContextMenuRequested(index, pos); } else { emit viewContextMenuRequested(QCursor::pos()); } break; } case Qt::Key_Escape: if (m_selectionBehavior != SingleSelection) { m_selectionManager->clearSelection(); } m_keyboardManager->cancelSearch(); emit escapePressed(); break; case Qt::Key_Space: if (m_selectionBehavior == MultiSelection) { if (controlPressed) { // Toggle the selection state of the current item. m_selectionManager->endAnchoredSelection(); m_selectionManager->setSelected(index, 1, KItemListSelectionManager::Toggle); m_selectionManager->beginAnchoredSelection(index); break; } else { // Select the current item if it is not selected yet. const int current = m_selectionManager->currentItem(); if (!m_selectionManager->isSelected(current)) { m_selectionManager->setSelected(current); break; } } } Q_FALLTHROUGH(); // fall through to the default case and add the Space to the current search string. default: m_keyboardManager->addKeys(event->text()); // Make sure unconsumed events get propagated up the chain. #302329 event->ignore(); return false; } if (m_selectionManager->currentItem() != index) { switch (m_selectionBehavior) { case NoSelection: m_selectionManager->setCurrentItem(index); break; case SingleSelection: m_selectionManager->setCurrentItem(index); m_selectionManager->clearSelection(); m_selectionManager->setSelected(index, 1); break; case MultiSelection: if (controlPressed) { m_selectionManager->endAnchoredSelection(); } m_selectionManager->setCurrentItem(index); if (!shiftOrControlPressed) { m_selectionManager->clearSelection(); m_selectionManager->setSelected(index, 1); } if (!shiftPressed) { m_selectionManager->beginAnchoredSelection(index); } break; } } if (navigationPressed) { m_view->scrollToItem(index); } return true; } void KItemListController::slotChangeCurrentItem(const QString& text, bool searchFromNextItem) { if (!m_model || m_model->count() == 0) { return; } int index; if (searchFromNextItem) { const int currentIndex = m_selectionManager->currentItem(); index = m_model->indexForKeyboardSearch(text, (currentIndex + 1) % m_model->count()); } else { index = m_model->indexForKeyboardSearch(text, 0); } if (index >= 0) { m_selectionManager->setCurrentItem(index); if (m_selectionBehavior != NoSelection) { m_selectionManager->replaceSelection(index); m_selectionManager->beginAnchoredSelection(index); } m_view->scrollToItem(index); } } void KItemListController::slotAutoActivationTimeout() { if (!m_model || !m_view) { return; } const int index = m_autoActivationTimer->property("index").toInt(); if (index < 0 || index >= m_model->count()) { return; } /* m_view->isUnderMouse() fixes a bug in the Folder-View-Panel and in the * Places-Panel. * * Bug: When you drag a file onto a Folder-View-Item or a Places-Item and * then move away before the auto-activation timeout triggers, than the * item still becomes activated/expanded. * * See Bug 293200 and 305783 */ if (m_model->supportsDropping(index) && m_view->isUnderMouse()) { if (m_view->supportsItemExpanding() && m_model->isExpandable(index)) { const bool expanded = m_model->isExpanded(index); m_model->setExpanded(index, !expanded); } else if (m_autoActivationBehavior != ExpansionOnly) { emit itemActivated(index); } } } bool KItemListController::inputMethodEvent(QInputMethodEvent* event) { Q_UNUSED(event); return false; } bool KItemListController::mousePressEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform) { if (!m_view) { return false; } m_pressedMousePos = transform.map(event->pos()); m_pressedIndex = m_view->itemAt(m_pressedMousePos); emit mouseButtonPressed(m_pressedIndex, event->buttons()); if (event->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; } if (m_view->isAboveExpansionToggle(m_pressedIndex, m_pressedMousePos)) { m_selectionManager->endAnchoredSelection(); m_selectionManager->setCurrentItem(m_pressedIndex); m_selectionManager->beginAnchoredSelection(m_pressedIndex); return true; } m_selectionTogglePressed = m_view->isAboveSelectionToggle(m_pressedIndex, m_pressedMousePos); if (m_selectionTogglePressed) { m_selectionManager->setSelected(m_pressedIndex, 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); m_selectionManager->beginAnchoredSelection(m_pressedIndex); return true; } const bool shiftPressed = event->modifiers() & Qt::ShiftModifier; const bool controlPressed = event->modifiers() & Qt::ControlModifier; // 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 >= 0 && m_selectionManager->isSelected(m_pressedIndex); const bool clearSelection = m_selectionBehavior == SingleSelection || (!shiftOrControlPressed && !pressedItemAlreadySelected); if (clearSelection) { m_selectionManager->clearSelection(); } else if (pressedItemAlreadySelected && !shiftOrControlPressed && (event->buttons() & Qt::LeftButton)) { // 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, m_pressedMousePos)) { emit selectedItemTextPressed(m_pressedIndex); } } if (!shiftPressed) { // Finish the anchored selection before the current index is changed m_selectionManager->endAnchoredSelection(); } if (event->buttons() & Qt::RightButton) { // 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 >= 0) { m_selectionManager->setCurrentItem(m_pressedIndex); switch (m_selectionBehavior) { case NoSelection: break; case SingleSelection: m_selectionManager->setSelected(m_pressedIndex); break; case MultiSelection: if (controlPressed && !shiftPressed) { m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Toggle); m_selectionManager->beginAnchoredSelection(m_pressedIndex); } else if (!shiftPressed || !m_selectionManager->isAnchoredSelectionActive()) { // Select the pressed item and start a new anchored selection m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Select); m_selectionManager->beginAnchoredSelection(m_pressedIndex); } break; default: Q_ASSERT(false); break; } if (event->buttons() & Qt::RightButton) { emit itemContextMenuRequested(m_pressedIndex, event->screenPos()); } return true; } if (event->buttons() & Qt::RightButton) { const QRectF headerBounds = m_view->headerBoundaries(); if (headerBounds.contains(event->pos())) { emit headerContextMenuRequested(event->screenPos()); } else { emit viewContextMenuRequested(event->screenPos()); } return true; } if (m_selectionBehavior == MultiSelection) { QPointF startPos = m_pressedMousePos; if (m_view->scrollOrientation() == Qt::Vertical) { startPos.ry() += m_view->scrollOffset(); if (m_view->itemSize().width() < 0) { // Use a special rubberband for views that have only one column and // expand the rubberband to use the whole width of the view. startPos.setX(0); } } 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); } return false; } bool KItemListController::mouseMoveEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform) { if (!m_view) { return false; } if (m_pressedIndex >= 0) { // 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)) { // Always assure that the dragged item gets selected. Usually this is already // done on the mouse-press event, but when using the selection-toggle on a // selected item the dragged item is not selected yet. m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Toggle); } else { // A selected item has been clicked to drag all selected items // -> the selection should not be cleared when the mouse button is released. m_clearSelectionIfItemsAreNotDragged = false; } startDragging(); } } } else { KItemListRubberBand* rubberBand = m_view->rubberBand(); if (rubberBand->isActive()) { QPointF endPos = transform.map(event->pos()); // Update the current item. const int newCurrent = m_view->itemAt(endPos); if (newCurrent >= 0) { // It's expected that the new current index is also the new anchor (bug 163451). m_selectionManager->endAnchoredSelection(); m_selectionManager->setCurrentItem(newCurrent); m_selectionManager->beginAnchoredSelection(newCurrent); } if (m_view->scrollOrientation() == Qt::Vertical) { endPos.ry() += m_view->scrollOffset(); if (m_view->itemSize().width() < 0) { // Use a special rubberband for views that have only one column and // expand the rubberband to use the whole width of the view. endPos.setX(m_view->size().width()); } } else { endPos.rx() += m_view->scrollOffset(); } rubberBand->setEndPosition(endPos); } } return false; } bool KItemListController::mouseReleaseEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform) { if (!m_view) { return false; } emit mouseButtonReleased(m_pressedIndex, event->buttons()); const bool isAboveSelectionToggle = m_view->isAboveSelectionToggle(m_pressedIndex, m_pressedMousePos); if (isAboveSelectionToggle) { m_selectionTogglePressed = false; return true; } if (!isAboveSelectionToggle && m_selectionTogglePressed) { m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Toggle); m_selectionTogglePressed = false; return true; } const bool shiftOrControlPressed = event->modifiers() & Qt::ShiftModifier || event->modifiers() & Qt::ControlModifier; KItemListRubberBand* rubberBand = m_view->rubberBand(); if (rubberBand->isActive()) { disconnect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged); rubberBand->setActive(false); m_oldSelection.clear(); m_view->setAutoScroll(false); } const QPointF pos = transform.map(event->pos()); const int index = m_view->itemAt(pos); if (index >= 0 && 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, 1, KItemListSelectionManager::Select); m_selectionManager->beginAnchoredSelection(m_pressedIndex); } if (event->button() & Qt::LeftButton) { bool emitItemActivated = true; if (m_view->isAboveExpansionToggle(index, pos)) { const bool expanded = m_model->isExpanded(index); m_model->setExpanded(index, !expanded); emit itemExpansionToggleClicked(index); emitItemActivated = false; } else if (shiftOrControlPressed) { // The mouse click should only update the selection, not trigger the item emitItemActivated = false; } else if (!(m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced)) { emitItemActivated = false; } if (emitItemActivated) { emit itemActivated(index); } } else if (event->button() & Qt::MidButton) { emit itemMiddleClicked(index); } } m_pressedMousePos = QPointF(); m_pressedIndex = -1; m_clearSelectionIfItemsAreNotDragged = false; return false; } bool KItemListController::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform) { const QPointF pos = transform.map(event->pos()); const int index = m_view->itemAt(pos); // Expand item if desired - See Bug 295573 if (m_mouseDoubleClickAction != ActivateItemOnly) { if (m_view && m_model && m_view->supportsItemExpanding() && m_model->isExpandable(index)) { const bool expanded = m_model->isExpanded(index); m_model->setExpanded(index, !expanded); } } if (event->button() & Qt::RightButton) { m_selectionManager->clearSelection(); if (index >= 0) { m_selectionManager->setSelected(index); emit itemContextMenuRequested(index, event->screenPos()); } else { const QRectF headerBounds = m_view->headerBoundaries(); if (headerBounds.contains(event->pos())) { emit headerContextMenuRequested(event->screenPos()); } else { emit viewContextMenuRequested(event->screenPos()); } } return true; } bool emitItemActivated = !(m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced) && (event->button() & Qt::LeftButton) && index >= 0 && index < m_model->count(); if (emitItemActivated) { emit itemActivated(index); } return false; } bool KItemListController::dragEnterEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform) { Q_UNUSED(event); Q_UNUSED(transform); DragAndDropHelper::clearUrlListMatchesUrlCache(); return false; } bool KItemListController::dragLeaveEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform) { Q_UNUSED(event); Q_UNUSED(transform); m_autoActivationTimer->stop(); m_view->setAutoScroll(false); m_view->hideDropIndicator(); KItemListWidget* widget = hoveredWidget(); if (widget) { widget->setHovered(false); emit itemUnhovered(widget->index()); } return false; } bool KItemListController::dragMoveEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform) { if (!m_model || !m_view) { return false; } QUrl hoveredDir = m_model->directory(); KItemListWidget* oldHoveredWidget = hoveredWidget(); const QPointF pos = transform.map(event->pos()); KItemListWidget* newHoveredWidget = widgetForPos(pos); if (oldHoveredWidget != newHoveredWidget) { m_autoActivationTimer->stop(); if (oldHoveredWidget) { oldHoveredWidget->setHovered(false); emit itemUnhovered(oldHoveredWidget->index()); } } if (newHoveredWidget) { bool droppingBetweenItems = false; if (m_model->sortRole().isEmpty()) { // The model supports inserting items between other items. droppingBetweenItems = (m_view->showDropIndicator(pos) >= 0); } const int index = newHoveredWidget->index(); if (m_model->isDir(index)) { hoveredDir = m_model->url(index); } if (!droppingBetweenItems) { if (m_model->supportsDropping(index)) { // Something has been dragged on an item. m_view->hideDropIndicator(); if (!newHoveredWidget->isHovered()) { newHoveredWidget->setHovered(true); emit itemHovered(index); } if (!m_autoActivationTimer->isActive() && m_autoActivationTimer->interval() >= 0) { m_autoActivationTimer->setProperty("index", index); m_autoActivationTimer->start(); } } } else { m_autoActivationTimer->stop(); if (newHoveredWidget && newHoveredWidget->isHovered()) { newHoveredWidget->setHovered(false); emit itemUnhovered(index); } } } else { m_view->hideDropIndicator(); } event->setAccepted(!DragAndDropHelper::urlListMatchesUrl(event->mimeData()->urls(), hoveredDir)); return false; } bool KItemListController::dropEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform) { if (!m_view) { return false; } m_autoActivationTimer->stop(); m_view->setAutoScroll(false); const QPointF pos = transform.map(event->pos()); int dropAboveIndex = -1; if (m_model->sortRole().isEmpty()) { // The model supports inserting of items between other items. dropAboveIndex = m_view->showDropIndicator(pos); } if (dropAboveIndex >= 0) { // Something has been dropped between two items. m_view->hideDropIndicator(); 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. emit itemDropEvent(m_view->itemAt(pos), event); } QAccessibleEvent accessibilityEvent(view(), QAccessible::DragDropEnd); QAccessible::updateAccessibility(&accessibilityEvent); return true; } bool KItemListController::hoverEnterEvent(QGraphicsSceneHoverEvent* event, const QTransform& transform) { Q_UNUSED(event); Q_UNUSED(transform); return false; } bool KItemListController::hoverMoveEvent(QGraphicsSceneHoverEvent* event, const QTransform& transform) { Q_UNUSED(transform); if (!m_model || !m_view) { return false; } KItemListWidget* oldHoveredWidget = hoveredWidget(); const QPointF pos = transform.map(event->pos()); KItemListWidget* newHoveredWidget = widgetForPos(pos); if (oldHoveredWidget != newHoveredWidget) { if (oldHoveredWidget) { oldHoveredWidget->setHovered(false); emit itemUnhovered(oldHoveredWidget->index()); } if (newHoveredWidget) { newHoveredWidget->setHovered(true); const QPointF mappedPos = newHoveredWidget->mapFromItem(m_view, pos); newHoveredWidget->setHoverPosition(mappedPos); emit itemHovered(newHoveredWidget->index()); } } else if (oldHoveredWidget) { const QPointF mappedPos = oldHoveredWidget->mapFromItem(m_view, pos); oldHoveredWidget->setHoverPosition(mappedPos); } return false; } bool KItemListController::hoverLeaveEvent(QGraphicsSceneHoverEvent* event, const QTransform& transform) { Q_UNUSED(event); Q_UNUSED(transform); if (!m_model || !m_view) { return false; } foreach (KItemListWidget* widget, m_view->visibleItemListWidgets()) { if (widget->isHovered()) { widget->setHovered(false); emit itemUnhovered(widget->index()); } } return false; } bool KItemListController::wheelEvent(QGraphicsSceneWheelEvent* event, const QTransform& transform) { Q_UNUSED(event); Q_UNUSED(transform); return false; } bool KItemListController::resizeEvent(QGraphicsSceneResizeEvent* event, const QTransform& transform) { Q_UNUSED(event); Q_UNUSED(transform); return false; } bool KItemListController::processEvent(QEvent* event, const QTransform& transform) { if (!event) { return false; } switch (event->type()) { case QEvent::KeyPress: return keyPressEvent(static_cast(event)); case QEvent::InputMethod: return inputMethodEvent(static_cast(event)); case QEvent::GraphicsSceneMousePress: return mousePressEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneMouseMove: return mouseMoveEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneMouseRelease: return mouseReleaseEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneMouseDoubleClick: return mouseDoubleClickEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneWheel: return wheelEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneDragEnter: return dragEnterEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneDragLeave: return dragLeaveEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneDragMove: return dragMoveEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneDrop: return dropEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneHoverEnter: return hoverEnterEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneHoverMove: return hoverMoveEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneHoverLeave: return hoverLeaveEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneResize: return resizeEvent(static_cast(event), transform); default: break; } return false; } void KItemListController::slotViewScrollOffsetChanged(qreal current, qreal previous) { if (!m_view) { return; } KItemListRubberBand* rubberBand = m_view->rubberBand(); if (rubberBand->isActive()) { const qreal diff = current - previous; // TODO: Ideally just QCursor::pos() should be used as // new end-position but it seems there is no easy way // to have something like QWidget::mapFromGlobal() for QGraphicsWidget // (... or I just missed an easy way to do the mapping) QPointF endPos = rubberBand->endPosition(); if (m_view->scrollOrientation() == Qt::Vertical) { endPos.ry() += diff; } else { endPos.rx() += diff; } rubberBand->setEndPosition(endPos); } } void KItemListController::slotRubberBandChanged() { if (!m_view || !m_model || m_model->count() <= 0) { return; } const KItemListRubberBand* rubberBand = m_view->rubberBand(); const QPointF startPos = rubberBand->startPosition(); const QPointF endPos = rubberBand->endPosition(); QRectF rubberBandRect = QRectF(startPos, endPos).normalized(); const bool scrollVertical = (m_view->scrollOrientation() == Qt::Vertical); if (scrollVertical) { rubberBandRect.translate(0, -m_view->scrollOffset()); } else { rubberBandRect.translate(-m_view->scrollOffset(), 0); } if (!m_oldSelection.isEmpty()) { // Clear the old selection that was available before the rubberband has // 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) { m_oldSelection.clear(); } } KItemSet selectedItems; // Select all visible items that intersect with the rubberband foreach (const KItemListWidget* widget, m_view->visibleItemListWidgets()) { const int index = widget->index(); const QRectF widgetRect = m_view->itemRect(index); if (widgetRect.intersects(rubberBandRect)) { const QRectF iconRect = widget->iconRect().translated(widgetRect.topLeft()); const QRectF textRect = widget->textRect().translated(widgetRect.topLeft()); if (iconRect.intersects(rubberBandRect) || textRect.intersects(rubberBandRect)) { selectedItems.insert(index); } } } // Select all invisible items that intersect with the rubberband. Instead of // iterating all items only the area which might be touched by the rubberband // will be checked. const bool increaseIndex = scrollVertical ? startPos.y() > endPos.y(): startPos.x() > endPos.x(); int index = increaseIndex ? m_view->lastVisibleIndex() + 1 : m_view->firstVisibleIndex() - 1; bool selectionFinished = false; do { const QRectF widgetRect = m_view->itemRect(index); if (widgetRect.intersects(rubberBandRect)) { selectedItems.insert(index); } if (increaseIndex) { ++index; selectionFinished = (index >= m_model->count()) || ( scrollVertical && widgetRect.top() > rubberBandRect.bottom()) || (!scrollVertical && widgetRect.left() > rubberBandRect.right()); } else { --index; selectionFinished = (index < 0) || ( scrollVertical && widgetRect.bottom() < rubberBandRect.top()) || (!scrollVertical && widgetRect.right() < rubberBandRect.left()); } } while (!selectionFinished); if (QApplication::keyboardModifiers() & Qt::ControlModifier) { // 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 // 2. all items inside the rubberband which have not been selected previously. m_selectionManager->setSelectedItems(m_oldSelection ^ selectedItems); } else { m_selectionManager->setSelectedItems(selectedItems + m_oldSelection); } } void KItemListController::startDragging() { if (!m_view || !m_model) { return; } const KItemSet selectedItems = m_selectionManager->selectedItems(); if (selectedItems.isEmpty()) { return; } QMimeData* data = m_model->createMimeData(selectedItems); if (!data) { return; } // The created drag object will be owned and deleted // by QApplication::activeWindow(). QDrag* drag = new QDrag(QApplication::activeWindow()); drag->setMimeData(data); const QPixmap pixmap = m_view->createDragPixmap(selectedItems); drag->setPixmap(pixmap); const QPoint hotSpot((pixmap.width() / pixmap.devicePixelRatio()) / 2, 0); drag->setHotSpot(hotSpot); drag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction); QAccessibleEvent accessibilityEvent(view(), QAccessible::DragDropStart); QAccessible::updateAccessibility(&accessibilityEvent); } KItemListWidget* KItemListController::hoveredWidget() const { Q_ASSERT(m_view); foreach (KItemListWidget* widget, m_view->visibleItemListWidgets()) { if (widget->isHovered()) { return widget; } } return nullptr; } KItemListWidget* KItemListController::widgetForPos(const QPointF& pos) const { Q_ASSERT(m_view); foreach (KItemListWidget* widget, m_view->visibleItemListWidgets()) { const QPointF mappedPos = widget->mapFromItem(m_view, pos); const bool hovered = widget->contains(mappedPos) && !widget->expansionToggleRect().contains(mappedPos); if (hovered) { return widget; } } return nullptr; } void KItemListController::updateKeyboardAnchor() { const bool validAnchor = m_keyboardAnchorIndex >= 0 && m_keyboardAnchorIndex < m_model->count() && keyboardAnchorPos(m_keyboardAnchorIndex) == m_keyboardAnchorPos; if (!validAnchor) { const int index = m_selectionManager->currentItem(); m_keyboardAnchorIndex = index; m_keyboardAnchorPos = keyboardAnchorPos(index); } } int KItemListController::nextRowIndex(int index) const { if (m_keyboardAnchorIndex < 0) { return index; } const int maxIndex = m_model->count() - 1; if (index == maxIndex) { return index; } // Calculate the index of the last column inside the row of the current index int lastColumnIndex = index; while (keyboardAnchorPos(lastColumnIndex + 1) > keyboardAnchorPos(lastColumnIndex)) { ++lastColumnIndex; if (lastColumnIndex >= maxIndex) { return index; } } // Based on the last column index go to the next row and calculate the nearest index // that is below the current index int nextRowIndex = lastColumnIndex + 1; int searchIndex = nextRowIndex; qreal minDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(nextRowIndex)); while (searchIndex < maxIndex && keyboardAnchorPos(searchIndex + 1) > keyboardAnchorPos(searchIndex)) { ++searchIndex; const qreal searchDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(searchIndex)); if (searchDiff < minDiff) { minDiff = searchDiff; nextRowIndex = searchIndex; } } return nextRowIndex; } int KItemListController::previousRowIndex(int index) const { if (m_keyboardAnchorIndex < 0 || index == 0) { return index; } // Calculate the index of the first column inside the row of the current index int firstColumnIndex = index; while (keyboardAnchorPos(firstColumnIndex - 1) < keyboardAnchorPos(firstColumnIndex)) { --firstColumnIndex; if (firstColumnIndex <= 0) { return index; } } // Based on the first column index go to the previous row and calculate the nearest index // that is above the current index int previousRowIndex = firstColumnIndex - 1; int searchIndex = previousRowIndex; qreal minDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(previousRowIndex)); while (searchIndex > 0 && keyboardAnchorPos(searchIndex - 1) < keyboardAnchorPos(searchIndex)) { --searchIndex; const qreal searchDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(searchIndex)); if (searchDiff < minDiff) { minDiff = searchDiff; previousRowIndex = searchIndex; } } return previousRowIndex; } qreal KItemListController::keyboardAnchorPos(int index) const { const QRectF itemRect = m_view->itemRect(index); if (!itemRect.isEmpty()) { return (m_view->scrollOrientation() == Qt::Vertical) ? itemRect.x() : itemRect.y(); } return 0; } void KItemListController::updateExtendedSelectionRegion() { if (m_view) { const bool extend = (m_selectionBehavior != MultiSelection); KItemListStyleOption option = m_view->styleOption(); if (option.extendedSelectionRegion != extend) { option.extendedSelectionRegion = extend; m_view->setStyleOption(option); } } } diff --git a/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp b/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp index 82e8aa2ff..09b4eaf23 100644 --- a/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp +++ b/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp @@ -1,99 +1,112 @@ /*************************************************************************** * Copyright (C) 2011 by Tirtha Chatterjee * * * * Based on the Itemviews NG project from Trolltech Labs: * * http://qt.gitorious.org/qt-labs/itemviews-ng * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemlistkeyboardsearchmanager.h" - KItemListKeyboardSearchManager::KItemListKeyboardSearchManager(QObject* parent) : QObject(parent), + m_isSearchRestarted(false), m_timeout(1000) { m_keyboardInputTime.invalidate(); } KItemListKeyboardSearchManager::~KItemListKeyboardSearchManager() { } bool KItemListKeyboardSearchManager::shouldClearSearchIfInputTimeReached() { const bool keyboardTimeWasValid = m_keyboardInputTime.isValid(); const qint64 keyboardInputTimeElapsed = m_keyboardInputTime.restart(); return (keyboardInputTimeElapsed > m_timeout) || !keyboardTimeWasValid; } void KItemListKeyboardSearchManager::addKeys(const QString& keys) { if (shouldClearSearchIfInputTimeReached()) { m_searchedString.clear(); } const bool newSearch = m_searchedString.isEmpty(); // Do not start a new search if the user pressed Space. Only add // it to the search string if a search is in progress already. if (newSearch && keys == QLatin1Char(' ')) { return; } if (!keys.isEmpty()) { m_searchedString.append(keys); // Special case: // If the same key is pressed repeatedly, the next item matching that key should be highlighted const QChar firstKey = m_searchedString.length() > 0 ? m_searchedString.at(0) : QChar(); const bool sameKey = m_searchedString.length() > 1 && m_searchedString.count(firstKey) == m_searchedString.length(); // Searching for a matching item should start from the next item if either - // 1. a new search is started, or + // 1. a new search is started and a search has not been restarted or // 2. a 'repeated key' search is done. - const bool searchFromNextItem = newSearch || sameKey; + const bool searchFromNextItem = (!m_isSearchRestarted && newSearch) || sameKey; + + // to remember not to searchFromNextItem if selection was deselected + // loosing keyboard search context basically + m_isSearchRestarted = false; emit changeCurrentItem(sameKey ? firstKey : m_searchedString, searchFromNextItem); } m_keyboardInputTime.start(); } void KItemListKeyboardSearchManager::setTimeout(qint64 milliseconds) { m_timeout = milliseconds; } qint64 KItemListKeyboardSearchManager::timeout() const { return m_timeout; } void KItemListKeyboardSearchManager::cancelSearch() { + m_isSearchRestarted = true; m_searchedString.clear(); } void KItemListKeyboardSearchManager::slotCurrentChanged(int current, int previous) { Q_UNUSED(previous); if (current < 0) { // The current item has been removed. We should cancel the search. cancelSearch(); } } + +void KItemListKeyboardSearchManager::slotSelectionChanged(const KItemSet& current, const KItemSet& previous) +{ + if (!previous.isEmpty() && current.isEmpty() && previous.count() > 0 && current.count() == 0) { + // The selection has been emptied. We should cancel the search. + cancelSearch(); + } +} diff --git a/src/kitemviews/private/kitemlistkeyboardsearchmanager.h b/src/kitemviews/private/kitemlistkeyboardsearchmanager.h index 29bec1414..9995c16b0 100644 --- a/src/kitemviews/private/kitemlistkeyboardsearchmanager.h +++ b/src/kitemviews/private/kitemlistkeyboardsearchmanager.h @@ -1,88 +1,91 @@ /*************************************************************************** * Copyright (C) 2011 by Tirtha Chatterjee * * * * Based on the Itemviews NG project from Trolltech Labs: * * http://qt.gitorious.org/qt-labs/itemviews-ng * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #ifndef KITEMLISTKEYBOARDSEARCHMANAGER_H #define KITEMLISTKEYBOARDSEARCHMANAGER_H #include "dolphin_export.h" +#include "kitemviews/kitemset.h" #include #include #include /** * @brief Controls the keyboard searching ability for a KItemListController. * * @see KItemListController * @see KItemModelBase */ class DOLPHIN_EXPORT KItemListKeyboardSearchManager : public QObject { Q_OBJECT public: explicit KItemListKeyboardSearchManager(QObject* parent = nullptr); ~KItemListKeyboardSearchManager() override; /** * Add \a keys to the text buffer used for searching. */ void addKeys(const QString& keys); /** * Sets the delay after which the search is cancelled to \a milliseconds. * If the time interval between two calls of addKeys(const QString&) is * larger than this, the second call will start a new search, rather than * combining the keys received from both calls to a single search string. */ void setTimeout(qint64 milliseconds); qint64 timeout() const; void cancelSearch(); bool shouldClearSearchIfInputTimeReached(); public slots: void slotCurrentChanged(int current, int previous); + void slotSelectionChanged(const KItemSet& current, const KItemSet& previous); signals: /** * Is emitted if the current item should be changed corresponding * to \a text. * @param searchFromNextItem If true start searching from item next to the * current item. Otherwise, search from the * current item. */ // TODO: Think about getting rid of the bool parameter // (see http://doc.qt.nokia.com/qq/qq13-apis.html#thebooleanparametertrap) void changeCurrentItem(const QString& string, bool searchFromNextItem); private: QString m_searchedString; + bool m_isSearchRestarted; QElapsedTimer m_keyboardInputTime; qint64 m_timeout; }; #endif diff --git a/src/tests/kitemlistcontrollertest.cpp b/src/tests/kitemlistcontrollertest.cpp index 2fd71483e..4cb1256e3 100644 --- a/src/tests/kitemlistcontrollertest.cpp +++ b/src/tests/kitemlistcontrollertest.cpp @@ -1,655 +1,661 @@ /*************************************************************************** * Copyright (C) 2012 by Frank Reininghaus * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemviews/kitemlistcontainer.h" #include "kitemviews/kfileitemlistview.h" #include "kitemviews/kfileitemmodel.h" #include "kitemviews/kitemlistcontroller.h" #include "kitemviews/kitemlistselectionmanager.h" #include "kitemviews/private/kitemlistviewlayouter.h" #include "testdir.h" #include #include #include #include /** * \class KItemListControllerTestStyle is a proxy style for testing the * KItemListController with different style hint options, e.g. single/double * click activation. */ class KItemListControllerTestStyle : public QProxyStyle { Q_OBJECT public: KItemListControllerTestStyle(QStyle* style) : QProxyStyle(style), m_activateItemOnSingleClick((bool)style->styleHint(SH_ItemView_ActivateItemOnSingleClick)) { } void setActivateItemOnSingleClick(bool activateItemOnSingleClick) { m_activateItemOnSingleClick = activateItemOnSingleClick; } bool activateItemOnSingleClick() const { return m_activateItemOnSingleClick; } int styleHint(StyleHint hint, const QStyleOption* option = nullptr, const QWidget* widget = nullptr, QStyleHintReturn* returnData = nullptr) const override { switch (hint) { case QStyle::SH_ItemView_ActivateItemOnSingleClick: return (int)activateItemOnSingleClick(); default: return QProxyStyle::styleHint(hint, option, widget, returnData); } } private: bool m_activateItemOnSingleClick; }; Q_DECLARE_METATYPE(KFileItemListView::ItemLayout) Q_DECLARE_METATYPE(Qt::Orientation) Q_DECLARE_METATYPE(KItemListController::SelectionBehavior) Q_DECLARE_METATYPE(KItemSet) class KItemListControllerTest : public QObject { Q_OBJECT private slots: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); void testKeyboardNavigation_data(); void testKeyboardNavigation(); void testMouseClickActivation(); private: /** * Make sure that the number of columns in the view is equal to \a count * by changing the geometry of the container. */ void adjustGeometryForColumnCount(int count); private: KFileItemListView* m_view; KItemListController* m_controller; KItemListSelectionManager* m_selectionManager; KFileItemModel* m_model; TestDir* m_testDir; KItemListContainer* m_container; KItemListControllerTestStyle* m_testStyle; }; /** * This function initializes the member objects, creates the temporary files, and * shows the view on the screen on startup. This could also be done before every * single test, but this would make the time needed to run the test much larger. */ void KItemListControllerTest::initTestCase() { qRegisterMetaType("KItemSet"); m_testDir = new TestDir(); m_model = new KFileItemModel(); m_view = new KFileItemListView(); m_controller = new KItemListController(m_model, m_view, this); m_container = new KItemListContainer(m_controller); m_controller = m_container->controller(); m_controller->setSelectionBehavior(KItemListController::MultiSelection); m_selectionManager = m_controller->selectionManager(); m_testStyle = new KItemListControllerTestStyle(m_view->style()); m_view->setStyle(m_testStyle); QStringList files; files << "a1" << "a2" << "a3" << "b1" << "c1" << "c2" << "c3" << "c4" << "c5" << "d1" << "d2" << "d3" << "d4" << "e" << "e 2" << "e 3" << "e 4" << "e 5" << "e 6" << "e 7"; m_testDir->createFiles(files); m_model->loadDirectory(m_testDir->url()); QSignalSpy spyDirectoryLoadingCompleted(m_model, &KFileItemModel::directoryLoadingCompleted); QVERIFY(spyDirectoryLoadingCompleted.wait()); m_container->show(); QVERIFY(QTest::qWaitForWindowExposed(m_container)); } void KItemListControllerTest::cleanupTestCase() { delete m_container; m_container = nullptr; delete m_testDir; m_testDir = nullptr; } /** Before each test, the current item, selection, and item size are reset to the defaults. */ void KItemListControllerTest::init() { m_selectionManager->setCurrentItem(0); QCOMPARE(m_selectionManager->currentItem(), 0); m_selectionManager->clearSelection(); QVERIFY(!m_selectionManager->hasSelection()); const QSizeF itemSize(50, 50); m_view->setItemSize(itemSize); QCOMPARE(m_view->itemSize(), itemSize); } void KItemListControllerTest::cleanup() { } /** * \class KeyPress is a small helper struct that represents a key press event, * including the key and the keyboard modifiers. */ struct KeyPress { KeyPress(Qt::Key key, Qt::KeyboardModifiers modifier = Qt::NoModifier) : m_key(key), m_modifier(modifier) {} Qt::Key m_key; Qt::KeyboardModifiers m_modifier; }; /** * \class ViewState is a small helper struct that represents a certain state * of the view, including the current item, the selected items in MultiSelection * mode (in the other modes, the selection is either empty or equal to the * current item), and the information whether items were activated by the last * key press. */ struct ViewState { ViewState(int current, const KItemSet &selection, bool activated = false) : m_current(current), m_selection(selection), m_activated(activated) {} int m_current; KItemSet m_selection; bool m_activated; }; // We have to define a typedef for the pair in order to make the test compile. typedef QPair keyPressViewStatePair; Q_DECLARE_METATYPE(QList) /** * This function provides the data for the actual test function * KItemListControllerTest::testKeyboardNavigation(). * It tests all possible combinations of view layouts, selection behaviors, * and enabled/disabled groupings for different column counts, and * provides a list of key presses and the states that the view should be in * after the key press event. */ void KItemListControllerTest::testKeyboardNavigation_data() { QTest::addColumn("layout"); QTest::addColumn("scrollOrientation"); QTest::addColumn("columnCount"); QTest::addColumn("selectionBehavior"); QTest::addColumn("groupingEnabled"); QTest::addColumn > >("testList"); QList layoutList; QHash layoutNames; layoutList.append(KFileItemListView::IconsLayout); layoutNames[KFileItemListView::IconsLayout] = "Icons"; layoutList.append(KFileItemListView::CompactLayout); layoutNames[KFileItemListView::CompactLayout] = "Compact"; layoutList.append(KFileItemListView::DetailsLayout); layoutNames[KFileItemListView::DetailsLayout] = "Details"; QList selectionBehaviorList; QHash selectionBehaviorNames; selectionBehaviorList.append(KItemListController::NoSelection); selectionBehaviorNames[KItemListController::NoSelection] = "NoSelection"; selectionBehaviorList.append(KItemListController::SingleSelection); selectionBehaviorNames[KItemListController::SingleSelection] = "SingleSelection"; selectionBehaviorList.append(KItemListController::MultiSelection); selectionBehaviorNames[KItemListController::MultiSelection] = "MultiSelection"; QList groupingEnabledList; QHash groupingEnabledNames; groupingEnabledList.append(false); groupingEnabledNames[false] = "ungrouped"; groupingEnabledList.append(true); groupingEnabledNames[true] = "grouping enabled"; foreach (const KFileItemListView::ItemLayout& layout, layoutList) { // The following settings depend on the layout. // Note that 'columns' are actually 'rows' in // Compact layout. Qt::Orientation scrollOrientation; QList columnCountList; Qt::Key nextItemKey; Qt::Key previousItemKey; Qt::Key nextRowKey; Qt::Key previousRowKey; switch (layout) { case KFileItemListView::IconsLayout: scrollOrientation = Qt::Vertical; columnCountList << 1 << 3 << 5; nextItemKey = Qt::Key_Right; previousItemKey = Qt::Key_Left; nextRowKey = Qt::Key_Down; previousRowKey = Qt::Key_Up; break; case KFileItemListView::CompactLayout: scrollOrientation = Qt::Horizontal; columnCountList << 1 << 3 << 5; nextItemKey = Qt::Key_Down; previousItemKey = Qt::Key_Up; nextRowKey = Qt::Key_Right; previousRowKey = Qt::Key_Left; break; case KFileItemListView::DetailsLayout: scrollOrientation = Qt::Vertical; columnCountList << 1; nextItemKey = Qt::Key_Down; previousItemKey = Qt::Key_Up; nextRowKey = Qt::Key_Down; previousRowKey = Qt::Key_Up; break; } foreach (int columnCount, columnCountList) { foreach (const KItemListController::SelectionBehavior& selectionBehavior, selectionBehaviorList) { foreach (bool groupingEnabled, groupingEnabledList) { // krazy:exclude=foreach QList > testList; // First, key presses which should have the same effect // for any layout and any number of columns. testList << qMakePair(KeyPress(nextItemKey), ViewState(1, KItemSet() << 1)) << qMakePair(KeyPress(Qt::Key_Return), ViewState(1, KItemSet() << 1, true)) << qMakePair(KeyPress(Qt::Key_Enter), ViewState(1, KItemSet() << 1, true)) << qMakePair(KeyPress(nextItemKey), ViewState(2, KItemSet() << 2)) << qMakePair(KeyPress(nextItemKey, Qt::ShiftModifier), ViewState(3, KItemSet() << 2 << 3)) << qMakePair(KeyPress(Qt::Key_Return), ViewState(3, KItemSet() << 2 << 3, true)) << qMakePair(KeyPress(previousItemKey, Qt::ShiftModifier), ViewState(2, KItemSet() << 2)) << qMakePair(KeyPress(nextItemKey, Qt::ShiftModifier), ViewState(3, KItemSet() << 2 << 3)) << qMakePair(KeyPress(nextItemKey, Qt::ControlModifier), ViewState(4, KItemSet() << 2 << 3)) << qMakePair(KeyPress(Qt::Key_Return), ViewState(4, KItemSet() << 2 << 3, true)) << qMakePair(KeyPress(previousItemKey), ViewState(3, KItemSet() << 3)) << qMakePair(KeyPress(Qt::Key_Home, Qt::ShiftModifier), ViewState(0, KItemSet() << 0 << 1 << 2 << 3)) << qMakePair(KeyPress(nextItemKey, Qt::ControlModifier), ViewState(1, KItemSet() << 0 << 1 << 2 << 3)) << qMakePair(KeyPress(Qt::Key_Space, Qt::ControlModifier), ViewState(1, KItemSet() << 0 << 2 << 3)) << qMakePair(KeyPress(Qt::Key_Space, Qt::ControlModifier), ViewState(1, KItemSet() << 0 << 1 << 2 << 3)) << qMakePair(KeyPress(Qt::Key_End), ViewState(19, KItemSet() << 19)) << qMakePair(KeyPress(previousItemKey, Qt::ShiftModifier), ViewState(18, KItemSet() << 18 << 19)) << qMakePair(KeyPress(Qt::Key_Home), ViewState(0, KItemSet() << 0)) << qMakePair(KeyPress(Qt::Key_Space, Qt::ControlModifier), ViewState(0, KItemSet())) << qMakePair(KeyPress(Qt::Key_Enter), ViewState(0, KItemSet(), true)) << qMakePair(KeyPress(Qt::Key_Space, Qt::ControlModifier), ViewState(0, KItemSet() << 0)) << qMakePair(KeyPress(Qt::Key_Space, Qt::ControlModifier), ViewState(0, KItemSet())) << qMakePair(KeyPress(Qt::Key_Space), ViewState(0, KItemSet() << 0)) << qMakePair(KeyPress(Qt::Key_E), ViewState(13, KItemSet() << 13)) << qMakePair(KeyPress(Qt::Key_Space), ViewState(14, KItemSet() << 14)) << qMakePair(KeyPress(Qt::Key_3), ViewState(15, KItemSet() << 15)) + << qMakePair(KeyPress(Qt::Key_Escape), ViewState(15, KItemSet())) + << qMakePair(KeyPress(Qt::Key_E), ViewState(13, KItemSet() << 13)) + << qMakePair(KeyPress(Qt::Key_E), ViewState(14, KItemSet() << 14)) + << qMakePair(KeyPress(Qt::Key_E), ViewState(15, KItemSet() << 15)) + << qMakePair(KeyPress(Qt::Key_Escape), ViewState(15, KItemSet())) + << qMakePair(KeyPress(Qt::Key_E), ViewState(13, KItemSet() << 13)) << qMakePair(KeyPress(Qt::Key_Home), ViewState(0, KItemSet() << 0)) << qMakePair(KeyPress(Qt::Key_Escape), ViewState(0, KItemSet())); // Next, we test combinations of key presses which only work for a // particular number of columns and either enabled or disabled grouping. // One column. if (columnCount == 1) { testList << qMakePair(KeyPress(nextRowKey), ViewState(1, KItemSet() << 1)) << qMakePair(KeyPress(nextRowKey, Qt::ShiftModifier), ViewState(2, KItemSet() << 1 << 2)) << qMakePair(KeyPress(nextRowKey, Qt::ControlModifier), ViewState(3, KItemSet() << 1 << 2)) << qMakePair(KeyPress(previousRowKey), ViewState(2, KItemSet() << 2)) << qMakePair(KeyPress(previousItemKey), ViewState(1, KItemSet() << 1)) << qMakePair(KeyPress(Qt::Key_Home), ViewState(0, KItemSet() << 0)); } // Multiple columns: we test both 3 and 5 columns with grouping // enabled or disabled. For each case, the layout of the items // in the view is shown (both using file names and indices) to // make it easier to understand what the tests do. if (columnCount == 3 && !groupingEnabled) { // 3 columns, no grouping: // // a1 a2 a3 | 0 1 2 // b1 c1 c2 | 3 4 5 // c3 c4 c5 | 6 7 8 // d1 d2 d3 | 9 10 11 // d4 e1 e2 | 12 13 14 // e3 e4 e5 | 15 16 17 // e6 e7 | 18 19 testList << qMakePair(KeyPress(nextRowKey), ViewState(3, KItemSet() << 3)) << qMakePair(KeyPress(nextItemKey, Qt::ControlModifier), ViewState(4, KItemSet() << 3)) << qMakePair(KeyPress(nextRowKey), ViewState(7, KItemSet() << 7)) << qMakePair(KeyPress(nextItemKey, Qt::ShiftModifier), ViewState(8, KItemSet() << 7 << 8)) << qMakePair(KeyPress(nextItemKey, Qt::ShiftModifier), ViewState(9, KItemSet() << 7 << 8 << 9)) << qMakePair(KeyPress(previousItemKey, Qt::ShiftModifier), ViewState(8, KItemSet() << 7 << 8)) << qMakePair(KeyPress(previousItemKey, Qt::ShiftModifier), ViewState(7, KItemSet() << 7)) << qMakePair(KeyPress(previousItemKey, Qt::ShiftModifier), ViewState(6, KItemSet() << 6 << 7)) << qMakePair(KeyPress(previousItemKey, Qt::ShiftModifier), ViewState(5, KItemSet() << 5 << 6 << 7)) << qMakePair(KeyPress(nextItemKey, Qt::ShiftModifier), ViewState(6, KItemSet() << 6 << 7)) << qMakePair(KeyPress(nextItemKey, Qt::ShiftModifier), ViewState(7, KItemSet() << 7)) << qMakePair(KeyPress(nextRowKey), ViewState(10, KItemSet() << 10)) << qMakePair(KeyPress(nextItemKey), ViewState(11, KItemSet() << 11)) << qMakePair(KeyPress(nextRowKey), ViewState(14, KItemSet() << 14)) << qMakePair(KeyPress(nextRowKey), ViewState(17, KItemSet() << 17)) << qMakePair(KeyPress(nextRowKey), ViewState(19, KItemSet() << 19)) << qMakePair(KeyPress(previousRowKey), ViewState(17, KItemSet() << 17)) << qMakePair(KeyPress(Qt::Key_End), ViewState(19, KItemSet() << 19)) << qMakePair(KeyPress(previousRowKey), ViewState(16, KItemSet() << 16)) << qMakePair(KeyPress(Qt::Key_Home), ViewState(0, KItemSet() << 0)); } if (columnCount == 5 && !groupingEnabled) { // 5 columns, no grouping: // // a1 a2 a3 b1 c1 | 0 1 2 3 4 // c2 c3 c4 c5 d1 | 5 6 7 8 9 // d2 d3 d4 e1 e2 | 10 11 12 13 14 // e3 e4 e5 e6 e7 | 15 16 17 18 19 testList << qMakePair(KeyPress(nextRowKey), ViewState(5, KItemSet() << 5)) << qMakePair(KeyPress(nextItemKey, Qt::ControlModifier), ViewState(6, KItemSet() << 5)) << qMakePair(KeyPress(nextRowKey), ViewState(11, KItemSet() << 11)) << qMakePair(KeyPress(nextItemKey), ViewState(12, KItemSet() << 12)) << qMakePair(KeyPress(nextRowKey, Qt::ShiftModifier), ViewState(17, KItemSet() << 12 << 13 << 14 << 15 << 16 << 17)) << qMakePair(KeyPress(previousRowKey, Qt::ShiftModifier), ViewState(12, KItemSet() << 12)) << qMakePair(KeyPress(previousRowKey, Qt::ShiftModifier), ViewState(7, KItemSet() << 7 << 8 << 9 << 10 << 11 << 12)) << qMakePair(KeyPress(nextRowKey, Qt::ShiftModifier), ViewState(12, KItemSet() << 12)) << qMakePair(KeyPress(Qt::Key_End, Qt::ControlModifier), ViewState(19, KItemSet() << 12)) << qMakePair(KeyPress(previousRowKey), ViewState(14, KItemSet() << 14)) << qMakePair(KeyPress(Qt::Key_Home), ViewState(0, KItemSet() << 0)); } if (columnCount == 3 && groupingEnabled) { // 3 columns, with grouping: // // a1 a2 a3 | 0 1 2 // b1 | 3 // c1 c2 c3 | 4 5 6 // c4 c5 | 7 8 // d1 d2 d3 | 9 10 11 // d4 | 12 // e1 e2 e3 | 13 14 15 // e4 e5 e6 | 16 17 18 // e7 | 19 testList << qMakePair(KeyPress(nextItemKey), ViewState(1, KItemSet() << 1)) << qMakePair(KeyPress(nextItemKey), ViewState(2, KItemSet() << 2)) << qMakePair(KeyPress(nextRowKey, Qt::ShiftModifier), ViewState(3, KItemSet() << 2 << 3)) << qMakePair(KeyPress(nextRowKey, Qt::ShiftModifier), ViewState(6, KItemSet() << 2 << 3 << 4 << 5 << 6)) << qMakePair(KeyPress(nextRowKey), ViewState(8, KItemSet() << 8)) << qMakePair(KeyPress(nextRowKey), ViewState(11, KItemSet() << 11)) << qMakePair(KeyPress(nextItemKey, Qt::ControlModifier), ViewState(12, KItemSet() << 11)) << qMakePair(KeyPress(nextRowKey), ViewState(13, KItemSet() << 13)) << qMakePair(KeyPress(nextRowKey), ViewState(16, KItemSet() << 16)) << qMakePair(KeyPress(nextItemKey), ViewState(17, KItemSet() << 17)) << qMakePair(KeyPress(nextRowKey), ViewState(19, KItemSet() << 19)) << qMakePair(KeyPress(previousRowKey), ViewState(17, KItemSet() << 17)) << qMakePair(KeyPress(Qt::Key_Home), ViewState(0, KItemSet() << 0)); } if (columnCount == 5 && groupingEnabled) { // 5 columns, with grouping: // // a1 a2 a3 | 0 1 2 // b1 | 3 // c1 c2 c3 c4 c5 | 4 5 6 7 8 // d1 d2 d3 d4 | 9 10 11 12 // e1 e2 e3 e4 e5 | 13 14 15 16 17 // e6 e7 | 18 19 testList << qMakePair(KeyPress(nextItemKey), ViewState(1, KItemSet() << 1)) << qMakePair(KeyPress(nextRowKey, Qt::ShiftModifier), ViewState(3, KItemSet() << 1 << 2 << 3)) << qMakePair(KeyPress(nextRowKey, Qt::ShiftModifier), ViewState(5, KItemSet() << 1 << 2 << 3 << 4 << 5)) << qMakePair(KeyPress(nextItemKey), ViewState(6, KItemSet() << 6)) << qMakePair(KeyPress(nextItemKey, Qt::ControlModifier), ViewState(7, KItemSet() << 6)) << qMakePair(KeyPress(nextItemKey, Qt::ControlModifier), ViewState(8, KItemSet() << 6)) << qMakePair(KeyPress(nextRowKey), ViewState(12, KItemSet() << 12)) << qMakePair(KeyPress(nextRowKey), ViewState(17, KItemSet() << 17)) << qMakePair(KeyPress(nextRowKey), ViewState(19, KItemSet() << 19)) << qMakePair(KeyPress(previousRowKey), ViewState(17, KItemSet() << 17)) << qMakePair(KeyPress(Qt::Key_End, Qt::ShiftModifier), ViewState(19, KItemSet() << 17 << 18 << 19)) << qMakePair(KeyPress(previousRowKey, Qt::ShiftModifier), ViewState(14, KItemSet() << 14 << 15 << 16 << 17)) << qMakePair(KeyPress(Qt::Key_Home), ViewState(0, KItemSet() << 0)); } const QString testName = layoutNames[layout] + ", " + QString("%1 columns, ").arg(columnCount) + selectionBehaviorNames[selectionBehavior] + ", " + groupingEnabledNames[groupingEnabled]; const QByteArray testNameAscii = testName.toLatin1(); QTest::newRow(testNameAscii.data()) << layout << scrollOrientation << columnCount << selectionBehavior << groupingEnabled << testList; } } } } } /** * This function sets the view's properties according to the data provided by * KItemListControllerTest::testKeyboardNavigation_data(). * * The list \a testList contains pairs of key presses, which are sent to the * container, and expected view states, which are verified then. */ void KItemListControllerTest::testKeyboardNavigation() { QFETCH(KFileItemListView::ItemLayout, layout); QFETCH(Qt::Orientation, scrollOrientation); QFETCH(int, columnCount); QFETCH(KItemListController::SelectionBehavior, selectionBehavior); QFETCH(bool, groupingEnabled); QFETCH(QList, testList); m_view->setItemLayout(layout); QCOMPARE(m_view->itemLayout(), layout); m_view->setScrollOrientation(scrollOrientation); QCOMPARE(m_view->scrollOrientation(), scrollOrientation); m_controller->setSelectionBehavior(selectionBehavior); QCOMPARE(m_controller->selectionBehavior(), selectionBehavior); m_model->setGroupedSorting(groupingEnabled); QCOMPARE(m_model->groupedSorting(), groupingEnabled); adjustGeometryForColumnCount(columnCount); QCOMPARE(m_view->m_layouter->m_columnCount, columnCount); QSignalSpy spySingleItemActivated(m_controller, &KItemListController::itemActivated); QSignalSpy spyMultipleItemsActivated(m_controller, &KItemListController::itemsActivated); while (!testList.isEmpty()) { const QPair test = testList.takeFirst(); const Qt::Key key = test.first.m_key; const Qt::KeyboardModifiers modifier = test.first.m_modifier; const int current = test.second.m_current; const KItemSet selection = test.second.m_selection; const bool activated = test.second.m_activated; QTest::keyClick(m_container, key, modifier); QCOMPARE(m_selectionManager->currentItem(), current); switch (selectionBehavior) { case KItemListController::NoSelection: QVERIFY(m_selectionManager->selectedItems().isEmpty()); break; case KItemListController::SingleSelection: QCOMPARE(m_selectionManager->selectedItems(), KItemSet() << current); break; case KItemListController::MultiSelection: QCOMPARE(m_selectionManager->selectedItems(), selection); break; } if (activated) { switch (selectionBehavior) { case KItemListController::MultiSelection: if (!selection.isEmpty()) { // The selected items should be activated. if (selection.count() == 1) { QVERIFY(!spySingleItemActivated.isEmpty()); QCOMPARE(qvariant_cast(spySingleItemActivated.takeFirst().at(0)), selection.first()); QVERIFY(spyMultipleItemsActivated.isEmpty()); } else { QVERIFY(spySingleItemActivated.isEmpty()); QVERIFY(!spyMultipleItemsActivated.isEmpty()); QCOMPARE(qvariant_cast(spyMultipleItemsActivated.takeFirst().at(0)), selection); } break; } // No items are selected. Therefore, the current item should be activated. // This is handled by falling through to the NoSelection/SingleSelection case. Q_FALLTHROUGH(); case KItemListController::NoSelection: case KItemListController::SingleSelection: // In NoSelection and SingleSelection mode, the current item should be activated. QVERIFY(!spySingleItemActivated.isEmpty()); QCOMPARE(qvariant_cast(spySingleItemActivated.takeFirst().at(0)), current); QVERIFY(spyMultipleItemsActivated.isEmpty()); break; } } } } void KItemListControllerTest::testMouseClickActivation() { m_view->setItemLayout(KFileItemListView::IconsLayout); // Make sure that we have a large window, such that // the items are visible and clickable. adjustGeometryForColumnCount(5); // Make sure that the first item is visible in the view. m_view->setScrollOffset(0); QCOMPARE(m_view->firstVisibleIndex(), 0); const QPointF pos = m_view->itemContextRect(0).center(); // Save the "single click" setting. const bool restoreSettingsSingleClick = m_testStyle->activateItemOnSingleClick(); QGraphicsSceneMouseEvent mousePressEvent(QEvent::GraphicsSceneMousePress); mousePressEvent.setPos(pos); mousePressEvent.setButton(Qt::LeftButton); mousePressEvent.setButtons(Qt::LeftButton); QGraphicsSceneMouseEvent mouseReleaseEvent(QEvent::GraphicsSceneMouseRelease); mouseReleaseEvent.setPos(pos); mouseReleaseEvent.setButton(Qt::LeftButton); mouseReleaseEvent.setButtons(Qt::NoButton); QSignalSpy spyItemActivated(m_controller, &KItemListController::itemActivated); // Default setting: single click activation. m_testStyle->setActivateItemOnSingleClick(true); m_view->event(&mousePressEvent); m_view->event(&mouseReleaseEvent); QCOMPARE(spyItemActivated.count(), 1); spyItemActivated.clear(); // Set the global setting to "double click activation". m_testStyle->setActivateItemOnSingleClick(false); m_view->event(&mousePressEvent); m_view->event(&mouseReleaseEvent); QCOMPARE(spyItemActivated.count(), 0); spyItemActivated.clear(); // Enforce single click activation in the controller. m_controller->setSingleClickActivationEnforced(true); m_view->event(&mousePressEvent); m_view->event(&mouseReleaseEvent); QCOMPARE(spyItemActivated.count(), 1); spyItemActivated.clear(); // Do not enforce single click activation in the controller. m_controller->setSingleClickActivationEnforced(false); m_view->event(&mousePressEvent); m_view->event(&mouseReleaseEvent); QCOMPARE(spyItemActivated.count(), 0); spyItemActivated.clear(); // Set the global setting back to "single click activation". m_testStyle->setActivateItemOnSingleClick(true); m_view->event(&mousePressEvent); m_view->event(&mouseReleaseEvent); QCOMPARE(spyItemActivated.count(), 1); spyItemActivated.clear(); // Enforce single click activation in the controller. m_controller->setSingleClickActivationEnforced(true); m_view->event(&mousePressEvent); m_view->event(&mouseReleaseEvent); QCOMPARE(spyItemActivated.count(), 1); spyItemActivated.clear(); // Restore previous settings. m_controller->setSingleClickActivationEnforced(true); m_testStyle->setActivateItemOnSingleClick(restoreSettingsSingleClick); } void KItemListControllerTest::adjustGeometryForColumnCount(int count) { const QSize size = m_view->itemSize().toSize(); QRect rect = m_container->geometry(); rect.setSize(size * count); m_container->setGeometry(rect); // Increase the size of the container until the correct column count is reached. while (m_view->m_layouter->m_columnCount < count) { rect = m_container->geometry(); rect.setSize(rect.size() + size); m_container->setGeometry(rect); } } QTEST_MAIN(KItemListControllerTest) #include "kitemlistcontrollertest.moc" diff --git a/src/tests/kitemlistkeyboardsearchmanagertest.cpp b/src/tests/kitemlistkeyboardsearchmanagertest.cpp index c14ce87ac..53ef9ec3c 100644 --- a/src/tests/kitemlistkeyboardsearchmanagertest.cpp +++ b/src/tests/kitemlistkeyboardsearchmanagertest.cpp @@ -1,157 +1,164 @@ /*************************************************************************** * Copyright (C) 2011 by Frank Reininghaus * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemviews/private/kitemlistkeyboardsearchmanager.h" #include #include class KItemListKeyboardSearchManagerTest : public QObject { Q_OBJECT private slots: void init(); void testBasicKeyboardSearch(); void testAbortedKeyboardSearch(); void testRepeatedKeyPress(); void testPressShift(); private: KItemListKeyboardSearchManager m_keyboardSearchManager; }; void KItemListKeyboardSearchManagerTest::init() { // Make sure that the previous search string is cleared m_keyboardSearchManager.cancelSearch(); } void KItemListKeyboardSearchManagerTest::testBasicKeyboardSearch() { QSignalSpy spy(&m_keyboardSearchManager, &KItemListKeyboardSearchManager::changeCurrentItem); QVERIFY(spy.isValid()); m_keyboardSearchManager.addKeys("f"); QCOMPARE(spy.count(), 1); - QCOMPARE(spy.takeFirst(), QList() << "f" << true); + QCOMPARE(spy.takeFirst(), QList() << "f" << false); m_keyboardSearchManager.addKeys("i"); QCOMPARE(spy.count(), 1); QCOMPARE(spy.takeFirst(), QList() << "fi" << false); m_keyboardSearchManager.addKeys("l"); QCOMPARE(spy.count(), 1); QCOMPARE(spy.takeFirst(), QList() << "fil" << false); m_keyboardSearchManager.addKeys("e"); QCOMPARE(spy.count(), 1); QCOMPARE(spy.takeFirst(), QList() << "file" << false); } void KItemListKeyboardSearchManagerTest::testAbortedKeyboardSearch() { // Set the timeout to a small value (the default is 5000 milliseconds) // to save time when running this test. m_keyboardSearchManager.setTimeout(100); QSignalSpy spy(&m_keyboardSearchManager, &KItemListKeyboardSearchManager::changeCurrentItem); QVERIFY(spy.isValid()); m_keyboardSearchManager.addKeys("f"); QCOMPARE(spy.count(), 1); - QCOMPARE(spy.takeFirst(), QList() << "f" << true); + QCOMPARE(spy.takeFirst(), QList() << "f" << false); m_keyboardSearchManager.addKeys("i"); QCOMPARE(spy.count(), 1); QCOMPARE(spy.takeFirst(), QList() << "fi" << false); // If the delay between two key presses is larger than the chosen timeout, // a new search is started. We add a small safety margin to avoid race conditions. QTest::qWait(m_keyboardSearchManager.timeout() + 10); m_keyboardSearchManager.addKeys("l"); QCOMPARE(spy.count(), 1); QCOMPARE(spy.takeFirst(), QList() << "l" << true); m_keyboardSearchManager.addKeys("e"); QCOMPARE(spy.count(), 1); QCOMPARE(spy.takeFirst(), QList() << "le" << false); + + // the selection was deselected, for instance with Esc or a click outside the selection + m_keyboardSearchManager.slotSelectionChanged(KItemSet(), KItemSet() << 1); + + m_keyboardSearchManager.addKeys("a"); + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.takeFirst(), QList() << "a" << false); } void KItemListKeyboardSearchManagerTest::testRepeatedKeyPress() { // If the same key is pressed repeatedly, the next matching item should be highlighted after // each key press. To achieve, that, the manager emits the changeCurrentItem(QString,bool) // signal, where // 1. the string contains the repeated key only once, and // 2. the bool searchFromNextItem is true. QSignalSpy spy(&m_keyboardSearchManager, &KItemListKeyboardSearchManager::changeCurrentItem); QVERIFY(spy.isValid()); m_keyboardSearchManager.addKeys("p"); QCOMPARE(spy.count(), 1); - QCOMPARE(spy.takeFirst(), QList() << "p" << true); + QCOMPARE(spy.takeFirst(), QList() << "p" << false); m_keyboardSearchManager.addKeys("p"); QCOMPARE(spy.count(), 1); QCOMPARE(spy.takeFirst(), QList() << "p" << true); m_keyboardSearchManager.addKeys("p"); QCOMPARE(spy.count(), 1); QCOMPARE(spy.takeFirst(), QList() << "p" << true); // Now press another key -> the search string contains all pressed keys m_keyboardSearchManager.addKeys("q"); QCOMPARE(spy.count(), 1); QCOMPARE(spy.takeFirst(), QList() << "pppq" << false); } void KItemListKeyboardSearchManagerTest::testPressShift() { // If the user presses Shift, i.e., to get a character like '_', // KItemListController calls the addKeys(QString) method with an empty // string. Make sure that this does not reset the current search. See // https://bugs.kde.org/show_bug.cgi?id=321286 QSignalSpy spy(&m_keyboardSearchManager, &KItemListKeyboardSearchManager::changeCurrentItem); QVERIFY(spy.isValid()); // Simulate that the user enters "a_b". m_keyboardSearchManager.addKeys("a"); QCOMPARE(spy.count(), 1); - QCOMPARE(spy.takeFirst(), QList() << "a" << true); + QCOMPARE(spy.takeFirst(), QList() << "a" << false); m_keyboardSearchManager.addKeys(""); QCOMPARE(spy.count(), 0); m_keyboardSearchManager.addKeys("_"); QCOMPARE(spy.count(), 1); QCOMPARE(spy.takeFirst(), QList() << "a_" << false); m_keyboardSearchManager.addKeys("b"); QCOMPARE(spy.count(), 1); QCOMPARE(spy.takeFirst(), QList() << "a_b" << false); } QTEST_GUILESS_MAIN(KItemListKeyboardSearchManagerTest) #include "kitemlistkeyboardsearchmanagertest.moc"