diff --git a/src/kitemviews/kitemlistcontroller.cpp b/src/kitemviews/kitemlistcontroller.cpp index a95969771..3731895d0 100644 --- a/src/kitemviews/kitemlistcontroller.cpp +++ b/src/kitemviews/kitemlistcontroller.cpp @@ -1,1328 +1,1328 @@ /*************************************************************************** * 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 "kitemlistview.h" #include "kitemlistselectionmanager.h" #include "private/kitemlistrubberband.h" #include "private/kitemlistkeyboardsearchmanager.h" #include #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(0), m_view(0), m_selectionManager(new KItemListSelectionManager(this)), m_keyboardManager(new KItemListKeyboardSearchManager(this)), m_pressedIndex(-1), m_pressedMousePos(), m_autoActivationTimer(0), 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); 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(0); Q_ASSERT(!m_view); setModel(0); 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; } 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::showEvent(QShowEvent* event) { Q_UNUSED(event); return false; } bool KItemListController::hideEvent(QHideEvent* event) { Q_UNUSED(event); return false; } 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 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 && (key == Qt::Key_Home || key == Qt::Key_End || key == Qt::Key_Up || key == Qt::Key_Down || key == Qt::Key_Left || key == Qt::Key_Right); 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; } } } // 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; } m_view->scrollToItem(index); } return true; } void KItemListController::slotChangeCurrentItem(const QString& text, bool searchFromNextItem) { if (!m_model || m_model->count() == 0) { return; } const int currentIndex = m_selectionManager->currentItem(); int index; if (searchFromNextItem) { index = m_model->indexForKeyboardSearch(text, (currentIndex + 1) % m_model->count()); } else { index = m_model->indexForKeyboardSearch(text, currentIndex); } if (index >= 0) { m_selectionManager->setCurrentItem(index); if (m_selectionBehavior != NoSelection) { m_selectionManager->clearSelection(); m_selectionManager->setSelected(index, 1); 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 (!shiftPressed) { // Finish the anchored selection before the current index is changed m_selectionManager->endAnchoredSelection(); } 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); return false; } bool KItemListController::dragLeaveEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform) { Q_UNUSED(event); Q_UNUSED(transform); 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; } event->acceptProposedAction(); 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 (!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(); } 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 { + } 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 0; } 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 0; } 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/kitemmodelbase.cpp b/src/kitemviews/kitemmodelbase.cpp index bf41b1c84..ee7e81084 100644 --- a/src/kitemviews/kitemmodelbase.cpp +++ b/src/kitemviews/kitemmodelbase.cpp @@ -1,161 +1,166 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * 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 "kitemmodelbase.h" KItemModelBase::KItemModelBase(QObject* parent) : QObject(parent), m_groupedSorting(false), m_sortRole(), m_sortOrder(Qt::AscendingOrder) { } KItemModelBase::KItemModelBase(const QByteArray& sortRole, QObject* parent) : QObject(parent), m_groupedSorting(false), m_sortRole(sortRole), m_sortOrder(Qt::AscendingOrder) { } KItemModelBase::~KItemModelBase() { } bool KItemModelBase::setData(int index, const QHash &values) { Q_UNUSED(index); Q_UNUSED(values); return false; } void KItemModelBase::setGroupedSorting(bool grouped) { if (m_groupedSorting != grouped) { m_groupedSorting = grouped; onGroupedSortingChanged(grouped); emit groupedSortingChanged(grouped); } } bool KItemModelBase::groupedSorting() const { return m_groupedSorting; } void KItemModelBase::setSortRole(const QByteArray& role) { if (role != m_sortRole) { const QByteArray previous = m_sortRole; m_sortRole = role; onSortRoleChanged(role, previous); emit sortRoleChanged(role, previous); } } QByteArray KItemModelBase::sortRole() const { return m_sortRole; } void KItemModelBase::setSortOrder(Qt::SortOrder order) { if (order != m_sortOrder) { const Qt::SortOrder previous = m_sortOrder; m_sortOrder = order; onSortOrderChanged(order, previous); emit sortOrderChanged(order, previous); } } QString KItemModelBase::roleDescription(const QByteArray& role) const { return role; } QList > KItemModelBase::groups() const { return QList >(); } bool KItemModelBase::setExpanded(int index, bool expanded) { Q_UNUSED(index); Q_UNUSED(expanded); return false; } bool KItemModelBase::isExpanded(int index) const { Q_UNUSED(index); return false; } bool KItemModelBase::isExpandable(int index) const { Q_UNUSED(index); return false; } int KItemModelBase::expandedParentsCount(int index) const { Q_UNUSED(index); return 0; } QMimeData* KItemModelBase::createMimeData(const KItemSet& indexes) const { Q_UNUSED(indexes); return 0; } int KItemModelBase::indexForKeyboardSearch(const QString& text, int startFromIndex) const { Q_UNUSED(text); Q_UNUSED(startFromIndex); return -1; } bool KItemModelBase::supportsDropping(int index) const { Q_UNUSED(index); return false; } +QString KItemModelBase::blacklistItemDropEventMimeType() const +{ + return QStringLiteral("application/x-dolphin-blacklist-drop"); +} + void KItemModelBase::onGroupedSortingChanged(bool current) { Q_UNUSED(current); } void KItemModelBase::onSortRoleChanged(const QByteArray& current, const QByteArray& previous) { Q_UNUSED(current); Q_UNUSED(previous); } void KItemModelBase::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) { Q_UNUSED(current); Q_UNUSED(previous); } diff --git a/src/kitemviews/kitemmodelbase.h b/src/kitemviews/kitemmodelbase.h index bd5ca1d65..45ad1f61a 100644 --- a/src/kitemviews/kitemmodelbase.h +++ b/src/kitemviews/kitemmodelbase.h @@ -1,271 +1,281 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * 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 KITEMMODELBASE_H #define KITEMMODELBASE_H #include "dolphin_export.h" #include #include #include #include #include class QMimeData; /** * @brief Base class for model implementations used by KItemListView and KItemListController. * * An item-model consists of a variable number of items. The number of items * is given by KItemModelBase::count(). The data of an item is accessed by a unique index * with KItemModelBase::data(). The indexes are integer-values counting from 0 to the * KItemModelBase::count() - 1. * * One item consists of a variable number of role/value-pairs. * * A model can optionally provide sorting- and grouping-capabilities. * * Also optionally it is possible to provide a tree of items by implementing the methods * setExpanded(), isExpanded(), isExpandable() and expandedParentsCount(). */ class DOLPHIN_EXPORT KItemModelBase : public QObject { Q_OBJECT public: KItemModelBase(QObject* parent = 0); explicit KItemModelBase(const QByteArray& sortRole, QObject* parent = 0); virtual ~KItemModelBase(); /** @return The number of items. */ virtual int count() const = 0; virtual QHash data(int index) const = 0; /** * Sets the data for the item at \a index to the given \a values. Returns true * if the data was set on the item; returns false otherwise. * * The default implementation does not set the data, and will always return * false. */ virtual bool setData(int index, const QHash& values); /** * Enables/disables the grouped sorting. The method KItemModelBase::onGroupedSortingChanged() will be * called so that model-implementations can react on the grouped-sorting change. Afterwards the * signal groupedSortingChanged() will be emitted. If the grouped sorting is enabled, the method * KItemModelBase::groups() must be implemented. */ void setGroupedSorting(bool grouped); bool groupedSorting() const; /** * Sets the sort-role to \a role. The method KItemModelBase::onSortRoleChanged() will be * called so that model-implementations can react on the sort-role change. Afterwards the * signal sortRoleChanged() will be emitted. */ void setSortRole(const QByteArray& role); QByteArray sortRole() const; /** * Sets the sort order to \a order. The method KItemModelBase::onSortOrderChanged() will be * called so that model-implementations can react on the sort order change. Afterwards the * signal sortOrderChanged() will be emitted. */ void setSortOrder(Qt::SortOrder order); Qt::SortOrder sortOrder() const; /** * @return Translated description for the \p role. The description is e.g. used * for the header in KItemListView. */ virtual QString roleDescription(const QByteArray& role) const; /** * @return List of group headers. Each list-item consists of the index of the item * that represents the first item of a group and a value represented * as QVariant. The value is shown by an instance of KItemListGroupHeader. * Per default an empty list is returned. */ virtual QList > groups() const; /** * Expands the item with the index \a index if \a expanded is true. * If \a expanded is false the item will be collapsed. * * Per default no expanding of items is implemented. When implementing * this method it is mandatory to overwrite KItemModelBase::isExpandable() * and KItemListView::supportsExpandableItems() to return true. * * @return True if the operation has been successful. */ virtual bool setExpanded(int index, bool expanded); /** * @return True if the item with the index \a index is expanded. * Per default no expanding of items is implemented. When implementing * this method it is mandatory to overwrite KItemModelBase::isExpandable() * and KItemListView::supportsExpandableItems() to return true. */ virtual bool isExpanded(int index) const; /** * @return True if expanding and collapsing of the item with the index \a index * is supported. Per default false is returned. */ virtual bool isExpandable(int index) const; /** * @return Number of expanded parent items for the item with the given index. * Per default 0 is returned. */ virtual int expandedParentsCount(int index) const; /** * @return MIME-data for the items given by \a indexes. The default implementation * returns 0. The ownership of the returned instance is in the hand of the * caller of this method. The method must be implemented if dragging of * items should be possible. */ virtual QMimeData* createMimeData(const KItemSet& indexes) const; /** * @return Reimplement this to return the index for the first item * beginning with string typed in through the keyboard, -1 if not found. * @param text the text which has been typed in through the keyboard * @param startFromIndex the index from which to start searching from */ virtual int indexForKeyboardSearch(const QString& text, int startFromIndex = 0) const; /** * @return True, if the item with the index \a index basically supports dropping. * Per default false is returned. * * The information is used only to give a visual feedback during a drag operation * and not to decide whether a drop event gets emitted. It is it is still up to * the receiver of KItemListController::itemDropEvent() to decide how to handle * the drop event. */ // TODO: Should the MIME-data be passed too so that the model can do a more specific // decision whether it accepts the drop? virtual bool supportsDropping(int index) const; + /** + * @return An internal mimetype to signal that an itemDropEvent() should be rejected by + * the receiving model. + * + * This mimeType can be used in createMimeData() to notify that the + * drop-onto-items events should be ignored, while the drop-between-items + * ones should be still accepted. + */ + QString blacklistItemDropEventMimeType() const; + signals: /** * Is emitted if one or more items have been inserted. Each item-range consists * of: * - an index where items have been inserted * - the number of inserted items. * The index of each item-range represents the index of the model * before the items have been inserted. * * For the item-ranges it is assured that: * - They don't overlap * - The index of item-range n is smaller than the index of item-range n + 1. */ void itemsInserted(const KItemRangeList& itemRanges); /** * Is emitted if one or more items have been removed. Each item-range consists * of: * - an index where items have been inserted * - the number of inserted items. * The index of each item-range represents the index of the model * before the items have been removed. * * For the item-ranges it is assured that: * - They don't overlap * - The index of item-range n is smaller than the index of item-range n + 1. */ void itemsRemoved(const KItemRangeList& itemRanges); /** * Is emitted if one ore more items get moved. * @param itemRange Item-range that gets moved to a new position. * @param movedToIndexes New positions for each element of the item-range. * * For example if the model has 10 items and the items 0 and 1 get exchanged * with the items 5 and 6 then the parameters look like this: * - itemRange: has the index 0 and a count of 7. * - movedToIndexes: Contains the seven values 5, 6, 2, 3, 4, 0, 1 * * This signal implies that the groups might have changed. Therefore, * gropusChanged() is not emitted if this signal is emitted. */ void itemsMoved(const KItemRange& itemRange, const QList& movedToIndexes); void itemsChanged(const KItemRangeList& itemRanges, const QSet& roles); /** * Is emitted if the groups have changed, even though the order of the * items has not been modified. */ void groupsChanged(); void groupedSortingChanged(bool current); void sortRoleChanged(const QByteArray& current, const QByteArray& previous); void sortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous); protected: /** * Is invoked if the grouped sorting has been changed by KItemModelBase::setGroupedSorting(). Allows * to react on the changed grouped sorting before the signal groupedSortingChanged() will be emitted. */ virtual void onGroupedSortingChanged(bool current); /** * Is invoked if the sort role has been changed by KItemModelBase::setSortRole(). Allows * to react on the changed sort role before the signal sortRoleChanged() will be emitted. * The implementation must assure that the items are sorted by the role given by \a current. * Usually the most efficient way is to emit a * itemsRemoved() signal for all items, reorder the items internally and to emit a * itemsInserted() signal afterwards. */ virtual void onSortRoleChanged(const QByteArray& current, const QByteArray& previous); /** * Is invoked if the sort order has been changed by KItemModelBase::setSortOrder(). Allows * to react on the changed sort order before the signal sortOrderChanged() will be emitted. * The implementation must assure that the items are sorted by the order given by \a current. * Usually the most efficient way is to emit a * itemsRemoved() signal for all items, reorder the items internally and to emit a * itemsInserted() signal afterwards. */ virtual void onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous); private: bool m_groupedSorting; QByteArray m_sortRole; Qt::SortOrder m_sortOrder; }; inline Qt::SortOrder KItemModelBase::sortOrder() const { return m_sortOrder; } #endif diff --git a/src/panels/places/placesitemmodel.cpp b/src/panels/places/placesitemmodel.cpp index a4741e746..04dac81b7 100644 --- a/src/panels/places/placesitemmodel.cpp +++ b/src/panels/places/placesitemmodel.cpp @@ -1,1209 +1,1212 @@ /*************************************************************************** * Copyright (C) 2012 by Peter Penz * * * * Based on KFilePlacesModel from kdelibs: * * Copyright (C) 2007 Kevin Ottens * * Copyright (C) 2007 David Faure * * * * 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 "placesitemmodel.h" #include "dolphin_generalsettings.h" #include #include #include "dolphindebug.h" #include #include #include #include #include #include "placesitem.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_BALOO #include #include #endif namespace { // As long as KFilePlacesView from kdelibs is available in parallel, the // system-bookmarks for "Recently Saved" and "Search For" should be // shown only inside the Places Panel. This is necessary as the stored // URLs needs to get translated to a Baloo-search-URL on-the-fly to // be independent from changes in the Baloo-search-URL-syntax. // Hence a prefix to the application-name of the stored bookmarks is // added, which is only read by PlacesItemModel. const char AppNamePrefix[] = "-places-panel"; } PlacesItemModel::PlacesItemModel(QObject* parent) : KStandardItemModel(parent), m_fileIndexingEnabled(false), m_hiddenItemsShown(false), m_availableDevices(), m_predicate(), m_bookmarkManager(0), m_systemBookmarks(), m_systemBookmarksIndexes(), m_bookmarkedItems(), m_hiddenItemToRemove(-1), m_updateBookmarksTimer(0), m_storageSetupInProgress() { #ifdef HAVE_BALOO Baloo::IndexerConfig config; m_fileIndexingEnabled = config.fileIndexingEnabled(); #endif const QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/user-places.xbel"; m_bookmarkManager = KBookmarkManager::managerForExternalFile(file); createSystemBookmarks(); initializeAvailableDevices(); loadBookmarks(); const int syncBookmarksTimeout = 100; m_updateBookmarksTimer = new QTimer(this); m_updateBookmarksTimer->setInterval(syncBookmarksTimeout); m_updateBookmarksTimer->setSingleShot(true); connect(m_updateBookmarksTimer, &QTimer::timeout, this, &PlacesItemModel::updateBookmarks); connect(m_bookmarkManager, &KBookmarkManager::changed, m_updateBookmarksTimer, static_cast(&QTimer::start)); } PlacesItemModel::~PlacesItemModel() { qDeleteAll(m_bookmarkedItems); m_bookmarkedItems.clear(); } PlacesItem* PlacesItemModel::createPlacesItem(const QString& text, const QUrl& url, const QString& iconName) { const KBookmark bookmark = PlacesItem::createBookmark(m_bookmarkManager, text, url, iconName); return new PlacesItem(bookmark); } PlacesItem* PlacesItemModel::placesItem(int index) const { return dynamic_cast(item(index)); } int PlacesItemModel::hiddenCount() const { int modelIndex = 0; int hiddenItemCount = 0; foreach (const PlacesItem* item, m_bookmarkedItems) { if (item) { ++hiddenItemCount; } else { if (placesItem(modelIndex)->isHidden()) { ++hiddenItemCount; } ++modelIndex; } } return hiddenItemCount; } void PlacesItemModel::setHiddenItemsShown(bool show) { if (m_hiddenItemsShown == show) { return; } m_hiddenItemsShown = show; if (show) { // Move all items that are part of m_bookmarkedItems to the model. QList itemsToInsert; QList insertPos; int modelIndex = 0; for (int i = 0; i < m_bookmarkedItems.count(); ++i) { if (m_bookmarkedItems[i]) { itemsToInsert.append(m_bookmarkedItems[i]); m_bookmarkedItems[i] = 0; insertPos.append(modelIndex); } ++modelIndex; } // Inserting the items will automatically insert an item // to m_bookmarkedItems in PlacesItemModel::onItemsInserted(). // The items are temporary saved in itemsToInsert, so // m_bookmarkedItems can be shrinked now. m_bookmarkedItems.erase(m_bookmarkedItems.begin(), m_bookmarkedItems.begin() + itemsToInsert.count()); for (int i = 0; i < itemsToInsert.count(); ++i) { insertItem(insertPos[i], itemsToInsert[i]); } Q_ASSERT(m_bookmarkedItems.count() == count()); } else { // Move all items of the model, where the "isHidden" property is true, to // m_bookmarkedItems. Q_ASSERT(m_bookmarkedItems.count() == count()); for (int i = count() - 1; i >= 0; --i) { if (placesItem(i)->isHidden()) { hideItem(i); } } } #ifdef PLACESITEMMODEL_DEBUG qCDebug(DolphinDebug) << "Changed visibility of hidden items"; showModelState(); #endif } bool PlacesItemModel::hiddenItemsShown() const { return m_hiddenItemsShown; } int PlacesItemModel::closestItem(const QUrl& url) const { int foundIndex = -1; int maxLength = 0; for (int i = 0; i < count(); ++i) { const QUrl itemUrl = placesItem(i)->url(); if (url == itemUrl) { // We can't find a closer one, so stop here. foundIndex = i; break; } else if (itemUrl.isParentOf(url)) { const int length = itemUrl.path().length(); if (length > maxLength) { foundIndex = i; maxLength = length; } } } return foundIndex; } void PlacesItemModel::appendItemToGroup(PlacesItem* item) { if (!item) { return; } int i = 0; while (i < count() && placesItem(i)->group() != item->group()) { ++i; } bool inserted = false; while (!inserted && i < count()) { if (placesItem(i)->group() != item->group()) { insertItem(i, item); inserted = true; } ++i; } if (!inserted) { appendItem(item); } } QAction* PlacesItemModel::ejectAction(int index) const { const PlacesItem* item = placesItem(index); if (item && item->device().is()) { return new QAction(QIcon::fromTheme(QStringLiteral("media-eject")), i18nc("@item", "Eject"), 0); } return 0; } QAction* PlacesItemModel::teardownAction(int index) const { const PlacesItem* item = placesItem(index); if (!item) { return 0; } Solid::Device device = item->device(); const bool providesTearDown = device.is() && device.as()->isAccessible(); if (!providesTearDown) { return 0; } Solid::StorageDrive* drive = device.as(); if (!drive) { drive = device.parent().as(); } bool hotPluggable = false; bool removable = false; if (drive) { hotPluggable = drive->isHotpluggable(); removable = drive->isRemovable(); } QString iconName; QString text; if (device.is()) { text = i18nc("@item", "Release"); } else if (removable || hotPluggable) { text = i18nc("@item", "Safely Remove"); iconName = QStringLiteral("media-eject"); } else { text = i18nc("@item", "Unmount"); iconName = QStringLiteral("media-eject"); } if (iconName.isEmpty()) { return new QAction(text, 0); } return new QAction(QIcon::fromTheme(iconName), text, 0); } void PlacesItemModel::requestEject(int index) { const PlacesItem* item = placesItem(index); if (item) { Solid::OpticalDrive* drive = item->device().parent().as(); if (drive) { connect(drive, &Solid::OpticalDrive::ejectDone, this, &PlacesItemModel::slotStorageTeardownDone); drive->eject(); } else { const QString label = item->text(); const QString message = i18nc("@info", "The device '%1' is not a disk and cannot be ejected.", label); emit errorMessage(message); } } } void PlacesItemModel::requestTeardown(int index) { const PlacesItem* item = placesItem(index); if (item) { Solid::StorageAccess* access = item->device().as(); if (access) { connect(access, &Solid::StorageAccess::teardownDone, this, &PlacesItemModel::slotStorageTeardownDone); access->teardown(); } } } bool PlacesItemModel::storageSetupNeeded(int index) const { const PlacesItem* item = placesItem(index); return item ? item->storageSetupNeeded() : false; } void PlacesItemModel::requestStorageSetup(int index) { const PlacesItem* item = placesItem(index); if (!item) { return; } Solid::Device device = item->device(); const bool setup = device.is() && !m_storageSetupInProgress.contains(device.as()) && !device.as()->isAccessible(); if (setup) { Solid::StorageAccess* access = device.as(); m_storageSetupInProgress[access] = index; connect(access, &Solid::StorageAccess::setupDone, this, &PlacesItemModel::slotStorageSetupDone); access->setup(); } } QMimeData* PlacesItemModel::createMimeData(const KItemSet& indexes) const { QList urls; QByteArray itemData; QDataStream stream(&itemData, QIODevice::WriteOnly); for (int index : indexes) { const QUrl itemUrl = placesItem(index)->url(); if (itemUrl.isValid()) { urls << itemUrl; } stream << index; } QMimeData* mimeData = new QMimeData(); if (!urls.isEmpty()) { mimeData->setUrls(urls); + } else { + // #378954: prevent itemDropEvent() drops if there isn't a source url. + mimeData->setData(blacklistItemDropEventMimeType(), QByteArrayLiteral("true")); } mimeData->setData(internalMimeType(), itemData); return mimeData; } bool PlacesItemModel::supportsDropping(int index) const { return index >= 0 && index < count(); } void PlacesItemModel::dropMimeDataBefore(int index, const QMimeData* mimeData) { if (mimeData->hasFormat(internalMimeType())) { // The item has been moved inside the view QByteArray itemData = mimeData->data(internalMimeType()); QDataStream stream(&itemData, QIODevice::ReadOnly); int oldIndex; stream >> oldIndex; if (oldIndex == index || oldIndex == index - 1) { // No moving has been done return; } PlacesItem* oldItem = placesItem(oldIndex); if (!oldItem) { return; } PlacesItem* newItem = new PlacesItem(oldItem->bookmark()); removeItem(oldIndex); if (oldIndex < index) { --index; } const int dropIndex = groupedDropIndex(index, newItem); insertItem(dropIndex, newItem); } else if (mimeData->hasFormat(QStringLiteral("text/uri-list"))) { // One or more items must be added to the model const QList urls = KUrlMimeData::urlsFromMimeData(mimeData); for (int i = urls.count() - 1; i >= 0; --i) { const QUrl& url = urls[i]; QString text = url.fileName(); if (text.isEmpty()) { text = url.host(); } if ((url.isLocalFile() && !QFileInfo(url.toLocalFile()).isDir()) || url.scheme() == QLatin1String("trash")) { // Only directories outside the trash are allowed continue; } PlacesItem* newItem = createPlacesItem(text, url); const int dropIndex = groupedDropIndex(index, newItem); insertItem(dropIndex, newItem); } } } QUrl PlacesItemModel::convertedUrl(const QUrl& url) { QUrl newUrl = url; if (url.scheme() == QLatin1String("timeline")) { newUrl = createTimelineUrl(url); } else if (url.scheme() == QLatin1String("search")) { newUrl = createSearchUrl(url); } return newUrl; } void PlacesItemModel::onItemInserted(int index) { const PlacesItem* insertedItem = placesItem(index); if (insertedItem) { // Take care to apply the PlacesItemModel-order of the inserted item // also to the bookmark-manager. const KBookmark insertedBookmark = insertedItem->bookmark(); const PlacesItem* previousItem = placesItem(index - 1); KBookmark previousBookmark; if (previousItem) { previousBookmark = previousItem->bookmark(); } m_bookmarkManager->root().moveBookmark(insertedBookmark, previousBookmark); } if (index == count() - 1) { // The item has been appended as last item to the list. In this // case assure that it is also appended after the hidden items and // not before (like done otherwise). m_bookmarkedItems.append(0); } else { int modelIndex = -1; int bookmarkIndex = 0; while (bookmarkIndex < m_bookmarkedItems.count()) { if (!m_bookmarkedItems[bookmarkIndex]) { ++modelIndex; if (modelIndex + 1 == index) { break; } } ++bookmarkIndex; } m_bookmarkedItems.insert(bookmarkIndex, 0); } #ifdef PLACESITEMMODEL_DEBUG qCDebug(DolphinDebug) << "Inserted item" << index; showModelState(); #endif } void PlacesItemModel::onItemRemoved(int index, KStandardItem* removedItem) { PlacesItem* placesItem = dynamic_cast(removedItem); if (placesItem) { const KBookmark bookmark = placesItem->bookmark(); m_bookmarkManager->root().deleteBookmark(bookmark); } const int boomarkIndex = bookmarkIndex(index); Q_ASSERT(!m_bookmarkedItems[boomarkIndex]); m_bookmarkedItems.removeAt(boomarkIndex); #ifdef PLACESITEMMODEL_DEBUG qCDebug(DolphinDebug) << "Removed item" << index; showModelState(); #endif } void PlacesItemModel::onItemChanged(int index, const QSet& changedRoles) { const PlacesItem* changedItem = placesItem(index); if (changedItem) { // Take care to apply the PlacesItemModel-order of the changed item // also to the bookmark-manager. const KBookmark insertedBookmark = changedItem->bookmark(); const PlacesItem* previousItem = placesItem(index - 1); KBookmark previousBookmark; if (previousItem) { previousBookmark = previousItem->bookmark(); } m_bookmarkManager->root().moveBookmark(insertedBookmark, previousBookmark); } if (changedRoles.contains("isHidden")) { if (!m_hiddenItemsShown && changedItem->isHidden()) { m_hiddenItemToRemove = index; QTimer::singleShot(0, this, static_cast(&PlacesItemModel::hideItem)); } } } void PlacesItemModel::slotDeviceAdded(const QString& udi) { const Solid::Device device(udi); if (!m_predicate.matches(device)) { return; } m_availableDevices << udi; const KBookmark bookmark = PlacesItem::createDeviceBookmark(m_bookmarkManager, udi); appendItem(new PlacesItem(bookmark)); } void PlacesItemModel::slotDeviceRemoved(const QString& udi) { if (!m_availableDevices.contains(udi)) { return; } for (int i = 0; i < m_bookmarkedItems.count(); ++i) { PlacesItem* item = m_bookmarkedItems[i]; if (item && item->udi() == udi) { m_bookmarkedItems.removeAt(i); delete item; return; } } for (int i = 0; i < count(); ++i) { if (placesItem(i)->udi() == udi) { removeItem(i); return; } } } void PlacesItemModel::slotStorageTeardownDone(Solid::ErrorType error, const QVariant& errorData) { if (error && errorData.isValid()) { emit errorMessage(errorData.toString()); } } void PlacesItemModel::slotStorageSetupDone(Solid::ErrorType error, const QVariant& errorData, const QString& udi) { Q_UNUSED(udi); const int index = m_storageSetupInProgress.take(sender()); const PlacesItem* item = placesItem(index); if (!item) { return; } if (error != Solid::NoError) { if (errorData.isValid()) { emit errorMessage(i18nc("@info", "An error occurred while accessing '%1', the system responded: %2", item->text(), errorData.toString())); } else { emit errorMessage(i18nc("@info", "An error occurred while accessing '%1'", item->text())); } emit storageSetupDone(index, false); } else { emit storageSetupDone(index, true); } } void PlacesItemModel::hideItem() { hideItem(m_hiddenItemToRemove); m_hiddenItemToRemove = -1; } void PlacesItemModel::updateBookmarks() { // Verify whether new bookmarks have been added or existing // bookmarks have been changed. KBookmarkGroup root = m_bookmarkManager->root(); KBookmark newBookmark = root.first(); while (!newBookmark.isNull()) { if (acceptBookmark(newBookmark, m_availableDevices)) { bool found = false; int modelIndex = 0; for (int i = 0; i < m_bookmarkedItems.count(); ++i) { PlacesItem* item = m_bookmarkedItems[i]; if (!item) { item = placesItem(modelIndex); ++modelIndex; } const KBookmark oldBookmark = item->bookmark(); if (equalBookmarkIdentifiers(newBookmark, oldBookmark)) { // The bookmark has been found in the model or as // a hidden item. The content of the bookmark might // have been changed, so an update is done. found = true; if (newBookmark.metaDataItem(QStringLiteral("UDI")).isEmpty()) { item->setBookmark(newBookmark); item->setText(i18nc("KFile System Bookmarks", newBookmark.text().toUtf8().constData())); } break; } } if (!found) { const QString udi = newBookmark.metaDataItem(QStringLiteral("UDI")); /* * See Bug 304878 * Only add a new places item, if the item text is not empty * and if the device is available. Fixes the strange behaviour - * add a places item without text in the Places section - when you * remove a device (e.g. a usb stick) without unmounting. */ if (udi.isEmpty() || Solid::Device(udi).isValid()) { PlacesItem* item = new PlacesItem(newBookmark); if (item->isHidden() && !m_hiddenItemsShown) { m_bookmarkedItems.append(item); } else { appendItemToGroup(item); } } } } newBookmark = root.next(newBookmark); } // Remove items that are not part of the bookmark-manager anymore int modelIndex = 0; for (int i = m_bookmarkedItems.count() - 1; i >= 0; --i) { PlacesItem* item = m_bookmarkedItems[i]; const bool itemIsPartOfModel = (item == 0); if (itemIsPartOfModel) { item = placesItem(modelIndex); } bool hasBeenRemoved = true; const KBookmark oldBookmark = item->bookmark(); KBookmark newBookmark = root.first(); while (!newBookmark.isNull()) { if (equalBookmarkIdentifiers(newBookmark, oldBookmark)) { hasBeenRemoved = false; break; } newBookmark = root.next(newBookmark); } if (hasBeenRemoved) { if (m_bookmarkedItems[i]) { delete m_bookmarkedItems[i]; m_bookmarkedItems.removeAt(i); } else { removeItem(modelIndex); --modelIndex; } } if (itemIsPartOfModel) { ++modelIndex; } } } void PlacesItemModel::saveBookmarks() { m_bookmarkManager->emitChanged(m_bookmarkManager->root()); } void PlacesItemModel::loadBookmarks() { KBookmarkGroup root = m_bookmarkManager->root(); KBookmark bookmark = root.first(); QSet devices = m_availableDevices; QSet missingSystemBookmarks; foreach (const SystemBookmarkData& data, m_systemBookmarks) { missingSystemBookmarks.insert(data.url); } // The bookmarks might have a mixed order of places, devices and search-groups due // to the compatibility with the KFilePlacesPanel. In Dolphin's places panel the // items should always be collected in one group so the items are collected first // in separate lists before inserting them. QList placesItems; QList recentlySavedItems; QList searchForItems; QList devicesItems; while (!bookmark.isNull()) { if (acceptBookmark(bookmark, devices)) { PlacesItem* item = new PlacesItem(bookmark); if (item->groupType() == PlacesItem::DevicesType) { devices.remove(item->udi()); devicesItems.append(item); } else { const QUrl url = bookmark.url(); if (missingSystemBookmarks.contains(url)) { missingSystemBookmarks.remove(url); // Try to retranslate the text of system bookmarks to have translated // items when changing the language. In case if the user has applied a custom // text, the retranslation will fail and the users custom text is still used. // It is important to use "KFile System Bookmarks" as context (see // createSystemBookmarks()). item->setText(i18nc("KFile System Bookmarks", bookmark.text().toUtf8().constData())); item->setSystemItem(true); } switch (item->groupType()) { case PlacesItem::PlacesType: placesItems.append(item); break; case PlacesItem::RecentlySavedType: recentlySavedItems.append(item); break; case PlacesItem::SearchForType: searchForItems.append(item); break; case PlacesItem::DevicesType: default: Q_ASSERT(false); break; } } } bookmark = root.next(bookmark); } if (!missingSystemBookmarks.isEmpty()) { // The current bookmarks don't contain all system-bookmarks. Add the missing // bookmarks. foreach (const SystemBookmarkData& data, m_systemBookmarks) { if (missingSystemBookmarks.contains(data.url)) { PlacesItem* item = createSystemPlacesItem(data); switch (item->groupType()) { case PlacesItem::PlacesType: placesItems.append(item); break; case PlacesItem::RecentlySavedType: recentlySavedItems.append(item); break; case PlacesItem::SearchForType: searchForItems.append(item); break; case PlacesItem::DevicesType: default: Q_ASSERT(false); break; } } } } // Create items for devices that have not been stored as bookmark yet devicesItems.reserve(devicesItems.count() + devices.count()); foreach (const QString& udi, devices) { const KBookmark bookmark = PlacesItem::createDeviceBookmark(m_bookmarkManager, udi); devicesItems.append(new PlacesItem(bookmark)); } QList items; items.append(placesItems); items.append(recentlySavedItems); items.append(searchForItems); items.append(devicesItems); foreach (PlacesItem* item, items) { if (!m_hiddenItemsShown && item->isHidden()) { m_bookmarkedItems.append(item); } else { appendItem(item); } } #ifdef PLACESITEMMODEL_DEBUG qCDebug(DolphinDebug) << "Loaded bookmarks"; showModelState(); #endif } bool PlacesItemModel::acceptBookmark(const KBookmark& bookmark, const QSet& availableDevices) const { const QString udi = bookmark.metaDataItem(QStringLiteral("UDI")); const QUrl url = bookmark.url(); const QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp")); const bool deviceAvailable = availableDevices.contains(udi); const bool allowedHere = (appName.isEmpty() || appName == KAboutData::applicationData().componentName() || appName == KAboutData::applicationData().componentName() + AppNamePrefix) && (m_fileIndexingEnabled || (url.scheme() != QLatin1String("timeline") && url.scheme() != QLatin1String("search"))); return (udi.isEmpty() && allowedHere) || deviceAvailable; } PlacesItem* PlacesItemModel::createSystemPlacesItem(const SystemBookmarkData& data) { KBookmark bookmark = PlacesItem::createBookmark(m_bookmarkManager, data.text, data.url, data.icon); const QString protocol = data.url.scheme(); if (protocol == QLatin1String("timeline") || protocol == QLatin1String("search")) { // As long as the KFilePlacesView from kdelibs is available, the system-bookmarks // for "Recently Saved" and "Search For" should be a setting available only // in the Places Panel (see description of AppNamePrefix for more details). const QString appName = KAboutData::applicationData().componentName() + AppNamePrefix; bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), appName); } PlacesItem* item = new PlacesItem(bookmark); item->setSystemItem(true); // Create default view-properties for all "Search For" and "Recently Saved" bookmarks // in case if the user has not already created custom view-properties for a corresponding // query yet. const bool createDefaultViewProperties = (item->groupType() == PlacesItem::SearchForType || item->groupType() == PlacesItem::RecentlySavedType) && !GeneralSettings::self()->globalViewProps(); if (createDefaultViewProperties) { ViewProperties props(convertedUrl(data.url)); if (!props.exist()) { const QString path = data.url.path(); if (path == QLatin1String("/documents")) { props.setViewMode(DolphinView::DetailsView); props.setPreviewsShown(false); props.setVisibleRoles({"text", "path"}); } else if (path == QLatin1String("/images")) { props.setViewMode(DolphinView::IconsView); props.setPreviewsShown(true); props.setVisibleRoles({"text", "imageSize"}); } else if (path == QLatin1String("/audio")) { props.setViewMode(DolphinView::DetailsView); props.setPreviewsShown(false); props.setVisibleRoles({"text", "artist", "album"}); } else if (path == QLatin1String("/videos")) { props.setViewMode(DolphinView::IconsView); props.setPreviewsShown(true); props.setVisibleRoles({"text"}); } else if (data.url.scheme() == QLatin1String("timeline")) { props.setViewMode(DolphinView::DetailsView); props.setVisibleRoles({"text", "modificationtime"}); } } } return item; } void PlacesItemModel::createSystemBookmarks() { Q_ASSERT(m_systemBookmarks.isEmpty()); Q_ASSERT(m_systemBookmarksIndexes.isEmpty()); // Note: The context of the I18N_NOOP2 must be "KFile System Bookmarks". The real // i18nc call is done after reading the bookmark. The reason why the i18nc call is not // done here is because otherwise switching the language would not result in retranslating the // bookmarks. m_systemBookmarks.append(SystemBookmarkData(QUrl::fromLocalFile(QDir::homePath()), QStringLiteral("user-home"), I18N_NOOP2("KFile System Bookmarks", "Home"))); m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("remote:/")), QStringLiteral("network-workgroup"), I18N_NOOP2("KFile System Bookmarks", "Network"))); m_systemBookmarks.append(SystemBookmarkData(QUrl::fromLocalFile(QStringLiteral("/")), QStringLiteral("folder-red"), I18N_NOOP2("KFile System Bookmarks", "Root"))); m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("trash:/")), QStringLiteral("user-trash"), I18N_NOOP2("KFile System Bookmarks", "Trash"))); if (m_fileIndexingEnabled) { m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("timeline:/today")), QStringLiteral("go-jump-today"), I18N_NOOP2("KFile System Bookmarks", "Today"))); m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("timeline:/yesterday")), QStringLiteral("view-calendar-day"), I18N_NOOP2("KFile System Bookmarks", "Yesterday"))); m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("timeline:/thismonth")), QStringLiteral("view-calendar-month"), I18N_NOOP2("KFile System Bookmarks", "This Month"))); m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("timeline:/lastmonth")), QStringLiteral("view-calendar-month"), I18N_NOOP2("KFile System Bookmarks", "Last Month"))); m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("search:/documents")), QStringLiteral("folder-text"), I18N_NOOP2("KFile System Bookmarks", "Documents"))); m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("search:/images")), QStringLiteral("folder-images"), I18N_NOOP2("KFile System Bookmarks", "Images"))); m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("search:/audio")), QStringLiteral("folder-sound"), I18N_NOOP2("KFile System Bookmarks", "Audio Files"))); m_systemBookmarks.append(SystemBookmarkData(QUrl(QStringLiteral("search:/videos")), QStringLiteral("folder-videos"), I18N_NOOP2("KFile System Bookmarks", "Videos"))); } for (int i = 0; i < m_systemBookmarks.count(); ++i) { m_systemBookmarksIndexes.insert(m_systemBookmarks[i].url, i); } } void PlacesItemModel::clear() { m_bookmarkedItems.clear(); KStandardItemModel::clear(); } void PlacesItemModel::initializeAvailableDevices() { QString predicate(QStringLiteral("[[[[ StorageVolume.ignored == false AND [ StorageVolume.usage == 'FileSystem' OR StorageVolume.usage == 'Encrypted' ]]" " OR " "[ IS StorageAccess AND StorageDrive.driveType == 'Floppy' ]]" " OR " "OpticalDisc.availableContent & 'Audio' ]" " OR " "StorageAccess.ignored == false ]")); if (KProtocolInfo::isKnownProtocol(QStringLiteral("mtp"))) { predicate.prepend("["); predicate.append(" OR PortableMediaPlayer.supportedProtocols == 'mtp']"); } m_predicate = Solid::Predicate::fromString(predicate); Q_ASSERT(m_predicate.isValid()); Solid::DeviceNotifier* notifier = Solid::DeviceNotifier::instance(); connect(notifier, &Solid::DeviceNotifier::deviceAdded, this, &PlacesItemModel::slotDeviceAdded); connect(notifier, &Solid::DeviceNotifier::deviceRemoved, this, &PlacesItemModel::slotDeviceRemoved); const QList& deviceList = Solid::Device::listFromQuery(m_predicate); foreach (const Solid::Device& device, deviceList) { m_availableDevices << device.udi(); } } int PlacesItemModel::bookmarkIndex(int index) const { int bookmarkIndex = 0; int modelIndex = 0; while (bookmarkIndex < m_bookmarkedItems.count()) { if (!m_bookmarkedItems[bookmarkIndex]) { if (modelIndex == index) { break; } ++modelIndex; } ++bookmarkIndex; } return bookmarkIndex >= m_bookmarkedItems.count() ? -1 : bookmarkIndex; } void PlacesItemModel::hideItem(int index) { PlacesItem* shownItem = placesItem(index); if (!shownItem) { return; } shownItem->setHidden(true); if (m_hiddenItemsShown) { // Removing items from the model is not allowed if all hidden // items should be shown. return; } const int newIndex = bookmarkIndex(index); if (newIndex >= 0) { const KBookmark hiddenBookmark = shownItem->bookmark(); PlacesItem* hiddenItem = new PlacesItem(hiddenBookmark); const PlacesItem* previousItem = placesItem(index - 1); KBookmark previousBookmark; if (previousItem) { previousBookmark = previousItem->bookmark(); } const bool updateBookmark = (m_bookmarkManager->root().indexOf(hiddenBookmark) >= 0); removeItem(index); if (updateBookmark) { // removeItem() also removed the bookmark from m_bookmarkManager in // PlacesItemModel::onItemRemoved(). However for hidden items the // bookmark should still be remembered, so readd it again: m_bookmarkManager->root().addBookmark(hiddenBookmark); m_bookmarkManager->root().moveBookmark(hiddenBookmark, previousBookmark); } m_bookmarkedItems.insert(newIndex, hiddenItem); } } QString PlacesItemModel::internalMimeType() const { return "application/x-dolphinplacesmodel-" + QString::number((qptrdiff)this); } int PlacesItemModel::groupedDropIndex(int index, const PlacesItem* item) const { Q_ASSERT(item); int dropIndex = index; const PlacesItem::GroupType type = item->groupType(); const int itemCount = count(); if (index < 0) { dropIndex = itemCount; } // Search nearest previous item with the same group int previousIndex = -1; for (int i = dropIndex - 1; i >= 0; --i) { if (placesItem(i)->groupType() == type) { previousIndex = i; break; } } // Search nearest next item with the same group int nextIndex = -1; for (int i = dropIndex; i < count(); ++i) { if (placesItem(i)->groupType() == type) { nextIndex = i; break; } } // Adjust the drop-index to be inserted to the // nearest item with the same group. if (previousIndex >= 0 && nextIndex >= 0) { dropIndex = (dropIndex - previousIndex < nextIndex - dropIndex) ? previousIndex + 1 : nextIndex; } else if (previousIndex >= 0) { dropIndex = previousIndex + 1; } else if (nextIndex >= 0) { dropIndex = nextIndex; } return dropIndex; } bool PlacesItemModel::equalBookmarkIdentifiers(const KBookmark& b1, const KBookmark& b2) { const QString udi1 = b1.metaDataItem(QStringLiteral("UDI")); const QString udi2 = b2.metaDataItem(QStringLiteral("UDI")); if (!udi1.isEmpty() && !udi2.isEmpty()) { return udi1 == udi2; } else { return b1.metaDataItem(QStringLiteral("ID")) == b2.metaDataItem(QStringLiteral("ID")); } } QUrl PlacesItemModel::createTimelineUrl(const QUrl& url) { // TODO: Clarify with the Baloo-team whether it makes sense // provide default-timeline-URLs like 'yesterday', 'this month' // and 'last month'. QUrl timelineUrl; const QString path = url.toDisplayString(QUrl::PreferLocalFile); if (path.endsWith(QLatin1String("yesterday"))) { const QDate date = QDate::currentDate().addDays(-1); const int year = date.year(); const int month = date.month(); const int day = date.day(); timelineUrl = QUrl("timeline:/" + timelineDateString(year, month) + '/' + timelineDateString(year, month, day)); } else if (path.endsWith(QLatin1String("thismonth"))) { const QDate date = QDate::currentDate(); timelineUrl = QUrl("timeline:/" + timelineDateString(date.year(), date.month())); } else if (path.endsWith(QLatin1String("lastmonth"))) { const QDate date = QDate::currentDate().addMonths(-1); timelineUrl = QUrl("timeline:/" + timelineDateString(date.year(), date.month())); } else { Q_ASSERT(path.endsWith(QLatin1String("today"))); timelineUrl = url; } return timelineUrl; } QString PlacesItemModel::timelineDateString(int year, int month, int day) { QString date = QString::number(year) + '-'; if (month < 10) { date += '0'; } date += QString::number(month); if (day >= 1) { date += '-'; if (day < 10) { date += '0'; } date += QString::number(day); } return date; } QUrl PlacesItemModel::createSearchUrl(const QUrl& url) { QUrl searchUrl; #ifdef HAVE_BALOO const QString path = url.toDisplayString(QUrl::PreferLocalFile); if (path.endsWith(QLatin1String("documents"))) { searchUrl = searchUrlForType(QStringLiteral("Document")); } else if (path.endsWith(QLatin1String("images"))) { searchUrl = searchUrlForType(QStringLiteral("Image")); } else if (path.endsWith(QLatin1String("audio"))) { searchUrl = searchUrlForType(QStringLiteral("Audio")); } else if (path.endsWith(QLatin1String("videos"))) { searchUrl = searchUrlForType(QStringLiteral("Video")); } else { Q_ASSERT(false); } #else Q_UNUSED(url); #endif return searchUrl; } #ifdef HAVE_BALOO QUrl PlacesItemModel::searchUrlForType(const QString& type) { Baloo::Query query; query.addType(type); return query.toSearchUrl(); } #endif #ifdef PLACESITEMMODEL_DEBUG void PlacesItemModel::showModelState() { qCDebug(DolphinDebug) << "================================="; qCDebug(DolphinDebug) << "Model:"; qCDebug(DolphinDebug) << "hidden-index model-index text"; int modelIndex = 0; for (int i = 0; i < m_bookmarkedItems.count(); ++i) { if (m_bookmarkedItems[i]) { qCDebug(DolphinDebug) << i << "(Hidden) " << " " << m_bookmarkedItems[i]->dataValue("text").toString(); } else { if (item(modelIndex)) { qCDebug(DolphinDebug) << i << " " << modelIndex << " " << item(modelIndex)->dataValue("text").toString(); } else { qCDebug(DolphinDebug) << i << " " << modelIndex << " " << "(not available yet)"; } ++modelIndex; } } qCDebug(DolphinDebug); qCDebug(DolphinDebug) << "Bookmarks:"; int bookmarkIndex = 0; KBookmarkGroup root = m_bookmarkManager->root(); KBookmark bookmark = root.first(); while (!bookmark.isNull()) { const QString udi = bookmark.metaDataItem("UDI"); const QString text = udi.isEmpty() ? bookmark.text() : udi; if (bookmark.metaDataItem("IsHidden") == QLatin1String("true")) { qCDebug(DolphinDebug) << bookmarkIndex << "(Hidden)" << text; } else { qCDebug(DolphinDebug) << bookmarkIndex << " " << text; } bookmark = root.next(bookmark); ++bookmarkIndex; } } #endif