]> cloud.milkyroute.net Git - dolphin.git/commitdiff
Move the generic hover- and selection-adjustments from DolphinsDetailsView into the...
authorPeter Penz <peter.penz19@gmail.com>
Sat, 9 Oct 2010 09:45:56 +0000 (09:45 +0000)
committerPeter Penz <peter.penz19@gmail.com>
Sat, 9 Oct 2010 09:45:56 +0000 (09:45 +0000)
Background info: QTreeView does not respect the width of a cell for the hover-feedback and the selections. This has been adjusted in DolphinDetailsView already, but the required code for this is quite large. This made it tricky to maintain the really Dolphin-specific parts in DolphinDetailsView.

svn path=/trunk/KDE/kdebase/apps/; revision=1184152

src/CMakeLists.txt
src/views/dolphindetailsview.cpp
src/views/dolphindetailsview.h
src/views/dolphintreeview.cpp [new file with mode: 0644]
src/views/dolphintreeview.h [new file with mode: 0644]

index 0f51f29a1b3da1fe61c8086b91889b788bde589f..f8dfc831b3bc7ae8745104a47b4c9838b8cd3ab2 100644 (file)
@@ -36,6 +36,7 @@ set(dolphinprivate_LIB_SRCS
     views/dolphinnewfilemenuobserver.cpp
     views/dolphinremoteencoding.cpp
     views/dolphinsortfilterproxymodel.cpp
+    views/dolphintreeview.cpp
     views/dolphinviewactionhandler.cpp
     views/dolphinviewautoscroller.cpp
     views/dolphinviewcontroller.cpp
index 04ce06f6d3036247fac986afd4abe1b706181204..ef3126517eb1f8af33e1ceda4790dc7230b9e27b 100644 (file)
 #include <klocale.h>
 #include <kmenu.h>
 
-#include <QAction>
 #include <QApplication>
 #include <QHeaderView>
-#include <QRubberBand>
-#include <QPainter>
 #include <QScrollBar>
 
 DolphinDetailsView::DolphinDetailsView(QWidget* parent,
                                        DolphinViewController* dolphinViewController,
                                        const ViewModeController* viewModeController,
                                        DolphinSortFilterProxyModel* proxyModel) :
-    QTreeView(parent),
+    DolphinTreeView(parent),
     m_autoResize(true),
-    m_expandingTogglePressed(false),
-    m_keyPressed(false),
-    m_useDefaultIndexAt(true),
-    m_ignoreScrollTo(false),
     m_dolphinViewController(dolphinViewController),
     m_viewModeController(viewModeController),
     m_extensionsFactory(0),
     m_expandableFoldersAction(0),
     m_expandedUrls(),
     m_font(),
-    m_decorationSize(),
-    m_band()
+    m_decorationSize()
 {
     const DetailsModeSettings* settings = DolphinSettings::instance().detailsModeSettings();
     Q_ASSERT(settings != 0);
@@ -152,8 +144,6 @@ DolphinDetailsView::DolphinDetailsView(QWidget* parent,
     connect(KGlobalSettings::self(), SIGNAL(settingsChanged(int)),
             this, SLOT(slotGlobalSettingsChanged(int)));
 
-    m_useDefaultIndexAt = false;
-
     m_expandableFoldersAction = new QAction(i18nc("@option:check", "Expandable Folders"), this);
     m_expandableFoldersAction->setCheckable(true);
     connect(m_expandableFoldersAction, SIGNAL(toggled(bool)),
@@ -181,46 +171,19 @@ QSet<KUrl> DolphinDetailsView::expandedUrls() const
     return m_expandedUrls;
 }
 
-QRegion DolphinDetailsView::visualRegionForSelection(const QItemSelection& selection) const
-{
-    // We have to make sure that the visualRect of each model index is inside the region.
-    // QTreeView::visualRegionForSelection does not do it right because it assumes implicitly
-    // that all visualRects have the same width, which is in general not the case here.
-    QRegion selectionRegion;
-    const QModelIndexList indexes = selection.indexes();
-
-    foreach(const QModelIndex& index, indexes) {
-        selectionRegion += visualRect(index);
-    }
-
-    return selectionRegion;
-}
-
 bool DolphinDetailsView::event(QEvent* event)
 {
-    switch (event->type()) {
-    case QEvent::Polish:
+    if (event->type() == QEvent::Polish) {
         header()->setResizeMode(QHeaderView::Interactive);
         updateColumnVisibility();
-        break;
-
-    case QEvent::FocusOut:
-        // If a key-press triggers an action that e. g. opens a dialog, the
-        // widget gets no key-release event. Assure that the pressed state
-        // is reset to prevent accidently setting the current index during a selection.
-        m_keyPressed = false;
-        break;
-
-    default:
-        break;
     }
 
-    return QTreeView::event(event);
+    return DolphinTreeView::event(event);
 }
 
 QStyleOptionViewItem DolphinDetailsView::viewOptions() const
 {
-    QStyleOptionViewItem viewOptions = QTreeView::viewOptions();
+    QStyleOptionViewItem viewOptions = DolphinTreeView::viewOptions();
     viewOptions.font = m_font;
     viewOptions.fontMetrics = QFontMetrics(m_font);
     viewOptions.showDecorationSelected = true;
@@ -230,7 +193,7 @@ QStyleOptionViewItem DolphinDetailsView::viewOptions() const
 
 void DolphinDetailsView::contextMenuEvent(QContextMenuEvent* event)
 {
-    QTreeView::contextMenuEvent(event);
+    DolphinTreeView::contextMenuEvent(event);
 
     DetailsModeSettings* settings = DolphinSettings::instance().detailsModeSettings();
     m_expandableFoldersAction->setChecked(settings->expandableFolders());
@@ -242,111 +205,21 @@ void DolphinDetailsView::mousePressEvent(QMouseEvent* event)
 {
     m_dolphinViewController->requestActivation();
 
-    const QModelIndex current = currentIndex();
-    QTreeView::mousePressEvent(event);
-
-    m_expandingTogglePressed = isAboveExpandingToggle(event->pos());
+    DolphinTreeView::mousePressEvent(event);
 
     const QModelIndex index = indexAt(event->pos());
-    const bool updateState = index.isValid() &&
-                             (index.column() == DolphinModel::Name) &&
-                             (event->button() == Qt::LeftButton);
-    if (updateState) {
-        setState(QAbstractItemView::DraggingState);
-    }
-
     if (!index.isValid() || (index.column() != DolphinModel::Name)) {
-        // the mouse press is done somewhere outside the filename column
+        // The mouse press is done somewhere outside the filename column
         if (QApplication::mouseButtons() & Qt::MidButton) {
             m_dolphinViewController->replaceUrlByClipboard();
         }
-
-        const Qt::KeyboardModifiers mod = QApplication::keyboardModifiers();
-        if (!m_expandingTogglePressed && !(mod & Qt::ShiftModifier) && !(mod & Qt::ControlModifier)) {
-            clearSelection();
-        }
-
-        // restore the current index, other columns are handled as viewport area.
-        // setCurrentIndex(...) implicitly calls scrollTo(...), which we want to ignore.
-        m_ignoreScrollTo = true;
-        selectionModel()->setCurrentIndex(current, QItemSelectionModel::Current);
-        m_ignoreScrollTo = false;
-
-        if ((event->button() == Qt::LeftButton) && !m_expandingTogglePressed) {
-            // Inform Qt about what we are doing - otherwise it starts dragging items around!
-            setState(DragSelectingState);
-            m_band.show = true;
-            // Incremental update data will not be useful - start from scratch.
-            m_band.ignoreOldInfo = true;
-            const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
-            m_band.origin = event->pos()  + scrollPos;
-            m_band.destination = m_band.origin;
-            m_band.originalSelection = selectionModel()->selection();
-        }
-    }
-}
-
-void DolphinDetailsView::mouseMoveEvent(QMouseEvent* event)
-{
-    if (m_expandingTogglePressed) {
-        // Per default QTreeView starts either a selection or a drag operation when dragging
-        // the expanding toggle button (Qt-issue - see TODO comment in DolphinIconsView::mousePressEvent()).
-        // Turn off this behavior in Dolphin to stay predictable:
-        setState(QAbstractItemView::NoState);
-        return;
-    }
-
-    if (m_band.show) {
-        const QPoint mousePos = event->pos();
-        const QModelIndex index = indexAt(mousePos);
-        if (!index.isValid()) {
-            // the destination of the selection rectangle is above the viewport. In this
-            // case QTreeView does no selection at all, which is not the wanted behavior
-            // in Dolphin -> select all items within the elastic band rectangle
-            updateElasticBandSelection();
-        }
-
-        // TODO: enable QTreeView::mouseMoveEvent(event) again, as soon
-        // as the Qt-issue #199631 has been fixed.
-        // QTreeView::mouseMoveEvent(event);
-        QAbstractItemView::mouseMoveEvent(event);
-        updateElasticBand();
-    } else {
-        // TODO: enable QTreeView::mouseMoveEvent(event) again, as soon
-        // as the Qt-issue #199631 has been fixed.
-        // QTreeView::mouseMoveEvent(event);
-        QAbstractItemView::mouseMoveEvent(event);
-    }
-}
-
-void DolphinDetailsView::mouseReleaseEvent(QMouseEvent* event)
-{
-    if (!m_expandingTogglePressed) {
-        const QModelIndex index = indexAt(event->pos());
-        if (index.isValid() && (index.column() == DolphinModel::Name)) {
-            QTreeView::mouseReleaseEvent(event);
-        } else {
-            // don't change the current index if the cursor is released
-            // above any other column than the name column, as the other
-            // columns act as viewport
-            const QModelIndex current = currentIndex();
-            QTreeView::mouseReleaseEvent(event);
-            selectionModel()->setCurrentIndex(current, QItemSelectionModel::Current);
-        }
-    }
-    m_expandingTogglePressed = false;
-
-    if (m_band.show) {
-        setState(NoState);
-        updateElasticBand();
-        m_band.show = false;
     }
 }
 
 void DolphinDetailsView::startDrag(Qt::DropActions supportedActions)
 {
     DragAndDropHelper::instance().startDrag(this, supportedActions, m_dolphinViewController);
-    m_band.show = false;
+    DolphinTreeView::startDrag(supportedActions);
 }
 
 void DolphinDetailsView::dragEnterEvent(QDragEnterEvent* event)
@@ -354,38 +227,15 @@ void DolphinDetailsView::dragEnterEvent(QDragEnterEvent* event)
     if (DragAndDropHelper::instance().isMimeDataSupported(event->mimeData())) {
         event->acceptProposedAction();
     }
-
-    if (m_band.show) {
-        updateElasticBand();
-        m_band.show = false;
-    }
-}
-
-void DolphinDetailsView::dragLeaveEvent(QDragLeaveEvent* event)
-{
-    QTreeView::dragLeaveEvent(event);
-    setDirtyRegion(m_dropRect);
+    DolphinTreeView::dragEnterEvent(event);
 }
 
 void DolphinDetailsView::dragMoveEvent(QDragMoveEvent* event)
 {
-    QTreeView::dragMoveEvent(event);
-
-    // TODO: remove this code when the issue #160611 is solved in Qt 4.4
-    setDirtyRegion(m_dropRect);
-    const QModelIndex index = indexAt(event->pos());
-    if (index.isValid() && (index.column() == DolphinModel::Name)) {
-        const KFileItem item = m_dolphinViewController->itemForIndex(index);
-        if (!item.isNull() && item.isDir()) {
-            m_dropRect = visualRect(index);
-        } else {
-            m_dropRect.setSize(QSize()); // set as invalid
-        }
-        setDirtyRegion(m_dropRect);
-    }
+    DolphinTreeView::dragMoveEvent(event);
 
     if (DragAndDropHelper::instance().isMimeDataSupported(event->mimeData())) {
-        // accept url drops, independently from the destination item
+        // Accept URL drops, independently from the destination item
         event->acceptProposedAction();
     }
 }
@@ -398,49 +248,18 @@ void DolphinDetailsView::dropEvent(QDropEvent* event)
         item = m_dolphinViewController->itemForIndex(index);
     }
     m_dolphinViewController->indicateDroppedUrls(item, m_viewModeController->url(), event);
-    QTreeView::dropEvent(event);
-}
-
-void DolphinDetailsView::paintEvent(QPaintEvent* event)
-{
-    QTreeView::paintEvent(event);
-    if (m_band.show) {
-        // The following code has been taken from QListView
-        // and adapted to DolphinDetailsView.
-        // (C) 1992-2007 Trolltech ASA
-        QStyleOptionRubberBand opt;
-        opt.initFrom(this);
-        opt.shape = QRubberBand::Rectangle;
-        opt.opaque = false;
-        opt.rect = elasticBandRect();
-
-        QPainter painter(viewport());
-        painter.save();
-        style()->drawControl(QStyle::CE_RubberBand, &opt, &painter);
-        painter.restore();
-    }
+    DolphinTreeView::dropEvent(event);
 }
 
 void DolphinDetailsView::keyPressEvent(QKeyEvent* event)
 {
-    // If the Control modifier is pressed, a multiple selection
-    // is done and DolphinDetailsView::currentChanged() may not
-    // not change the selection in a custom way.
-    m_keyPressed = !(event->modifiers() & Qt::ControlModifier);
-
-    QTreeView::keyPressEvent(event);
+    DolphinTreeView::keyPressEvent(event);
     m_dolphinViewController->handleKeyPressEvent(event);
 }
 
-void DolphinDetailsView::keyReleaseEvent(QKeyEvent* event)
-{
-    QTreeView::keyReleaseEvent(event);
-    m_keyPressed = false;
-}
-
 void DolphinDetailsView::resizeEvent(QResizeEvent* event)
 {
-    QTreeView::resizeEvent(event);
+    DolphinTreeView::resizeEvent(event);
     if (m_autoResize) {
         resizeColumns();
     }
@@ -450,19 +269,13 @@ void DolphinDetailsView::wheelEvent(QWheelEvent* event)
 {
     const int step = m_decorationSize.height();
     verticalScrollBar()->setSingleStep(step);
-    QTreeView::wheelEvent(event);
+    DolphinTreeView::wheelEvent(event);
 }
 
 void DolphinDetailsView::currentChanged(const QModelIndex& current, const QModelIndex& previous)
 {
-    QTreeView::currentChanged(current, previous);
     m_extensionsFactory->handleCurrentIndexChange(current, previous);
-
-    // Stay consistent with QListView: When changing the current index by key presses,
-    // also change the selection.
-    if (m_keyPressed) {
-        setCurrentIndex(current);
-    }
+    DolphinTreeView::currentChanged(current, previous);
 
     // If folders are expanded, the width which is available for editing may have changed
     // because it depends on the level of the current item in the folder hierarchy.
@@ -472,27 +285,18 @@ void DolphinDetailsView::currentChanged(const QModelIndex& current, const QModel
 bool DolphinDetailsView::eventFilter(QObject* watched, QEvent* event)
 {
     if ((watched == viewport()) && (event->type() == QEvent::Leave)) {
-        // if the mouse is above an item and moved very fast outside the widget,
+        // If the mouse is above an item and moved very fast outside the widget,
         // no viewportEntered() signal might be emitted although the mouse has been moved
-        // above the viewport
+        // above the viewport.
         m_dolphinViewController->emitViewportEntered();
     }
 
-    return QTreeView::eventFilter(watched, event);
-}
-
-QModelIndex DolphinDetailsView::indexAt(const QPoint& point) const
-{
-    // the blank portion of the name column counts as empty space
-    const QModelIndex index = QTreeView::indexAt(point);
-    const bool isAboveEmptySpace  = !m_useDefaultIndexAt &&
-                                    (index.column() == KDirModel::Name) && !visualRect(index).contains(point);
-    return isAboveEmptySpace ? QModelIndex() : index;
+    return DolphinTreeView::eventFilter(watched, event);
 }
 
 QRect DolphinDetailsView::visualRect(const QModelIndex& index) const
 {
-    QRect rect = QTreeView::visualRect(index);
+    QRect rect = DolphinTreeView::visualRect(index);
     const KFileItem item = m_dolphinViewController->itemForIndex(index);
     if (!item.isNull()) {
         const int width = DolphinFileItemDelegate::nameColumnWidth(item.text(), viewOptions());
@@ -502,31 +306,21 @@ QRect DolphinDetailsView::visualRect(const QModelIndex& index) const
     return rect;
 }
 
-void DolphinDetailsView::setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags command)
+bool DolphinDetailsView::acceptsDrop(const QModelIndex& index) const
 {
-    // We must override setSelection() as Qt calls it internally and when this happens
-    // we must ensure that the default indexAt() is used.
-    if (!m_band.show) {
-        m_useDefaultIndexAt = true;
-        QTreeView::setSelection(rect, command);
-        m_useDefaultIndexAt = false;
-    } else {
-        // Use our own elastic band selection algorithm
-        updateElasticBandSelection();
+    if (index.isValid() && (index.column() == DolphinModel::Name)) {
+        // Accept drops above directories
+        const KFileItem item = m_dolphinViewController->itemForIndex(index);
+        return !item.isNull() && item.isDir();
     }
-}
 
-void DolphinDetailsView::scrollTo(const QModelIndex & index, ScrollHint hint)
-{
-    if (!m_ignoreScrollTo) {
-        QTreeView::scrollTo(index, hint);
-    }
+    return false;
 }
 
 void DolphinDetailsView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
 {
     removeExpandedIndexes(parent, start, end);
-    QTreeView::rowsAboutToBeRemoved(parent, start, end);
+    DolphinTreeView::rowsAboutToBeRemoved(parent, start, end);
 }
 
 void DolphinDetailsView::setSortIndicatorSection(DolphinView::Sorting sorting)
@@ -558,34 +352,6 @@ void DolphinDetailsView::slotEntered(const QModelIndex& index)
     }
 }
 
-void DolphinDetailsView::updateElasticBand()
-{
-    if (m_band.show) {
-        QRect dirtyRegion(elasticBandRect());
-        const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
-        m_band.destination = viewport()->mapFromGlobal(QCursor::pos()) + scrollPos;
-        // Going above the (logical) top-left of the view causes complications during selection;
-        // we may as well prevent it.
-        if (m_band.destination.y() < 0) {
-            m_band.destination.setY(0);
-        }
-        if (m_band.destination.x() < 0) {
-            m_band.destination.setX(0);
-        }
-        dirtyRegion = dirtyRegion.united(elasticBandRect());
-        setDirtyRegion(dirtyRegion);
-    }
-}
-
-QRect DolphinDetailsView::elasticBandRect() const
-{
-    const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
-
-    const QPoint topLeft = m_band.origin - scrollPos;
-    const QPoint bottomRight = m_band.destination - scrollPos;
-    return QRect(topLeft, bottomRight).normalized();
-}
-
 void DolphinDetailsView::setZoomLevel(int level)
 {
     const int size = ZoomLevelInfo::iconSizeForZoomLevel(level);
@@ -612,7 +378,7 @@ void DolphinDetailsView::configureSettings(const QPoint& pos)
     KMenu popup(this);
     popup.addTitle(i18nc("@title:menu", "Columns"));
 
-    // add checkbox items for each column
+    // Add checkbox items for each column
     QHeaderView* headerView = header();
     const int columns = model()->columnCount();
     for (int i = 0; i < columns; ++i) {
@@ -819,7 +585,7 @@ void DolphinDetailsView::slotGlobalSettingsChanged(int category)
     if (settings->useSystemFont()) {
         m_font = KGlobalSettings::generalFont();
     }
-    //Disconnect then reconnect, since the settings have been changed, the connection requirements may have also.
+    // Disconnect then reconnect, since the settings have been changed, the connection requirements may have also.
     disconnect(this, SIGNAL(clicked(QModelIndex)), m_dolphinViewController, SLOT(triggerItem(QModelIndex)));
     disconnect(this, SIGNAL(doubleClicked(QModelIndex)), m_dolphinViewController, SLOT(triggerItem(QModelIndex)));
     if (KGlobalSettings::singleClick()) {
@@ -829,193 +595,11 @@ void DolphinDetailsView::slotGlobalSettingsChanged(int category)
     }
 }
 
-void DolphinDetailsView::updateElasticBandSelection()
-{
-    if (!m_band.show) {
-        return;
-    }
-
-    // Ensure the elastic band itself is up-to-date, in
-    // case we are being called due to e.g. a drag event.
-    updateElasticBand();
-
-    // Clip horizontally to the name column, as some filenames will be
-    // longer than the column.  We don't clip vertically as origin
-    // may be above or below the current viewport area.
-    const int nameColumnX = header()->sectionPosition(DolphinModel::Name);
-    const int nameColumnWidth = header()->sectionSize(DolphinModel::Name);
-    QRect selRect = elasticBandRect().normalized();
-    QRect nameColumnArea(nameColumnX, selRect.y(), nameColumnWidth, selRect.height());
-    selRect = nameColumnArea.intersect(selRect).normalized();
-    // Get the last elastic band rectangle, expressed in viewpoint coordinates.
-    const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
-    QRect oldSelRect = QRect(m_band.lastSelectionOrigin - scrollPos, m_band.lastSelectionDestination - scrollPos).normalized();
-
-    if (selRect.isNull()) {
-        selectionModel()->select(m_band.originalSelection, QItemSelectionModel::ClearAndSelect);
-        m_band.ignoreOldInfo = true;
-        return;
-    }
-
-    if (!m_band.ignoreOldInfo) {
-        // Do some quick checks to see if we can rule out the need to
-        // update the selection.
-        Q_ASSERT(uniformRowHeights());
-        QModelIndex dummyIndex = model()->index(0, 0);
-        if (!dummyIndex.isValid()) {
-            // No items in the model presumably.
-            return;
-        }
-
-        // If the elastic band does not cover the same rows as before, we'll
-        // need to re-check, and also invalidate the old item distances.
-        const int rowHeight = QTreeView::rowHeight(dummyIndex);
-        const bool coveringSameRows =
-            (selRect.top()    / rowHeight == oldSelRect.top()    / rowHeight) &&
-            (selRect.bottom() / rowHeight == oldSelRect.bottom() / rowHeight);
-        if (coveringSameRows) {
-            // Covering the same rows, but have we moved far enough horizontally
-            // that we might have (de)selected some other items?
-            const bool itemSelectionChanged =
-                ((selRect.left() > oldSelRect.left()) &&
-                 (selRect.left() > m_band.insideNearestLeftEdge)) ||
-                ((selRect.left() < oldSelRect.left()) &&
-                 (selRect.left() <= m_band.outsideNearestLeftEdge)) ||
-                ((selRect.right() < oldSelRect.right()) &&
-                 (selRect.left() >= m_band.insideNearestRightEdge)) ||
-                ((selRect.right() > oldSelRect.right()) &&
-                 (selRect.right() >= m_band.outsideNearestRightEdge));
-
-            if (!itemSelectionChanged) {
-                return;
-            }
-        }
-    } else {
-        // This is the only piece of optimization data that needs to be explicitly
-        // discarded.
-        m_band.lastSelectionOrigin = QPoint();
-        m_band.lastSelectionDestination = QPoint();
-        oldSelRect = selRect;
-    }
-
-    // Do the selection from scratch. Force a update of the horizontal distances info.
-    m_band.insideNearestLeftEdge   = nameColumnX + nameColumnWidth + 1;
-    m_band.insideNearestRightEdge  = nameColumnX - 1;
-    m_band.outsideNearestLeftEdge  = nameColumnX - 1;
-    m_band.outsideNearestRightEdge = nameColumnX + nameColumnWidth + 1;
-
-    // Include the old selection rect as well, so we can deselect
-    // items that were inside it but not in the new selRect.
-    const QRect boundingRect = selRect.united(oldSelRect).normalized();
-    if (boundingRect.isNull()) {
-        return;
-    }
-
-    // Get the index of the item in this row in the name column.
-    // TODO - would this still work if the columns could be re-ordered?
-    QModelIndex startIndex = QTreeView::indexAt(boundingRect.topLeft());
-    if (startIndex.parent().isValid()) {
-        startIndex = startIndex.parent().child(startIndex.row(), KDirModel::Name);
-    } else {
-        startIndex = model()->index(startIndex.row(), KDirModel::Name);
-    }
-    if (!startIndex.isValid()) {
-        selectionModel()->select(m_band.originalSelection, QItemSelectionModel::ClearAndSelect);
-        m_band.ignoreOldInfo = true;
-        return;
-    }
-
-   // Go through all indexes between the top and bottom of boundingRect, and
-   // update the selection.
-   const int verticalCutoff = boundingRect.bottom();
-   QModelIndex currIndex = startIndex;
-   QModelIndex lastIndex;
-   bool allItemsInBoundDone = false;
-
-   // Calling selectionModel()->select(...) for each item that needs to be
-   // toggled is slow as each call emits selectionChanged(...) so store them
-   // and do the selection toggle in one batch.
-   QItemSelection itemsToToggle;
-   // QItemSelection's deal with continuous ranges of indexes better than
-   // single indexes, so try to portion items that need to be toggled into ranges.
-   bool formingToggleIndexRange = false;
-   QModelIndex toggleIndexRangeBegin = QModelIndex();
-
-   do {
-       QRect currIndexRect = visualRect(currIndex);
-
-        // Update some optimization info as we go.
-       const int cr = currIndexRect.right();
-       const int cl = currIndexRect.left();
-       const int sl = selRect.left();
-       const int sr = selRect.right();
-       // "The right edge of the name is outside of the rect but nearer than m_outsideNearestLeft", etc
-       if ((cr < sl && cr > m_band.outsideNearestLeftEdge)) {
-           m_band.outsideNearestLeftEdge = cr;
-       }
-       if ((cl > sr && cl < m_band.outsideNearestRightEdge)) {
-           m_band.outsideNearestRightEdge = cl;
-       }
-       if ((cl >= sl && cl <= sr && cl > m_band.insideNearestRightEdge)) {
-           m_band.insideNearestRightEdge = cl;
-       }
-       if ((cr >= sl && cr <= sr && cr < m_band.insideNearestLeftEdge)) {
-           m_band.insideNearestLeftEdge = cr;
-       }
-
-       bool currentlySelected = selectionModel()->isSelected(currIndex);
-       bool originallySelected = m_band.originalSelection.contains(currIndex);
-       bool intersectsSelectedRect = currIndexRect.intersects(selRect);
-       bool shouldBeSelected = (intersectsSelectedRect && !originallySelected) || (!intersectsSelectedRect && originallySelected);
-       bool needToToggleItem = (currentlySelected && !shouldBeSelected) || (!currentlySelected && shouldBeSelected);
-       if (needToToggleItem && !formingToggleIndexRange) {
-            toggleIndexRangeBegin = currIndex;
-            formingToggleIndexRange = true;
-       }
-
-       // NOTE: indexBelow actually walks up and down expanded trees for us.
-       QModelIndex nextIndex = indexBelow(currIndex);
-       allItemsInBoundDone = !nextIndex.isValid() || currIndexRect.top() > verticalCutoff;
-
-       const bool commitToggleIndexRange = formingToggleIndexRange &&
-                                           (!needToToggleItem ||
-                                            allItemsInBoundDone ||
-                                            currIndex.parent() != toggleIndexRangeBegin.parent());
-       if (commitToggleIndexRange) {
-           formingToggleIndexRange = false;
-            // If this is the last item in the bounds and it is also the beginning of a range,
-            // don't toggle lastIndex - it will already have been dealt with.
-           if (!allItemsInBoundDone || toggleIndexRangeBegin != currIndex) {
-               itemsToToggle.select(toggleIndexRangeBegin, lastIndex);
-           }
-            // Need to start a new range immediately with currIndex?
-           if (needToToggleItem) {
-               toggleIndexRangeBegin = currIndex;
-               formingToggleIndexRange = true;
-           }
-           if (allItemsInBoundDone && needToToggleItem) {
-                // Toggle the very last item in the bounds.
-               itemsToToggle.select(currIndex, currIndex);
-           }
-       }
-
-       // next item
-       lastIndex = currIndex;
-       currIndex = nextIndex;
-    } while (!allItemsInBoundDone);
-
-
-    selectionModel()->select(itemsToToggle, QItemSelectionModel::Toggle);
-
-    m_band.lastSelectionOrigin = m_band.origin;
-    m_band.lastSelectionDestination = m_band.destination;
-    m_band.ignoreOldInfo = false;
-}
 
 void DolphinDetailsView::setFoldersExpandable(bool expandable)
 {
     if (!expandable) {
-        // collapse all expanded folders, as QTreeView::setItemsExpandable(false)
+        // Collapse all expanded folders, as QTreeView::setItemsExpandable(false)
         // does not do this task
         const int rowCount = model()->rowCount();
         for (int row = 0; row < rowCount; ++row) {
@@ -1090,52 +674,10 @@ KFileItemDelegate::Information DolphinDetailsView::infoForColumn(int columnIndex
     return AdditionalInfoAccessor::instance().keyForColumn(columnIndex);
 }
 
-bool DolphinDetailsView::isAboveExpandingToggle(const QPoint& pos) const
-{
-    // QTreeView offers no public API to get the information whether an index has an
-    // expanding toggle and what boundaries the toggle has. The following approach
-    // also assumes a toggle for file items.
-    if (itemsExpandable()) {
-        const QModelIndex index = QTreeView::indexAt(pos);
-        if (index.isValid() && (index.column() == KDirModel::Name)) {
-            QRect rect = visualRect(index);
-            const int toggleSize = rect.height();
-            if (isRightToLeft()) {
-                rect.moveRight(rect.right());
-            } else {
-                rect.moveLeft(rect.x() - toggleSize);
-            }
-            rect.setWidth(toggleSize);
-
-            QStyleOption opt;
-            opt.initFrom(this);
-            opt.rect = rect;
-            rect = style()->subElementRect(QStyle::SE_TreeViewDisclosureItem, &opt, this);
-
-            return rect.contains(pos);
-        }
-    }
-    return false;
-}
-
 void DolphinDetailsView::adjustMaximumSizeForEditing(const QModelIndex& index)
 {
     // Make sure that the full width of the "Name" column is available for "Rename Inline"
     m_extensionsFactory->fileItemDelegate()->setMaximumSize(QTreeView::visualRect(index).size());
 }
 
-DolphinDetailsView::ElasticBand::ElasticBand() :
-    show(false),
-    origin(),
-    destination(),
-    lastSelectionOrigin(),
-    lastSelectionDestination(),
-    ignoreOldInfo(true),
-    outsideNearestLeftEdge(0),
-    outsideNearestRightEdge(0),
-    insideNearestLeftEdge(0),
-    insideNearestRightEdge(0)
-{
-}
-
 #include "dolphindetailsview.moc"
index 8b76cbc363988a603445532b516329226ec6f257..67f7f2e3eda100b9b78d751553cd512a38560dd6 100644 (file)
@@ -1,6 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2006 by Peter Penz (peter.penz@gmx.at)                  *
- *   Copyright (C) 2008 by Simon St. James (kdedevel@etotheipiplusone.com) *
+ *   Copyright (C) 2006-2010 by Peter Penz <peter.penz19@gmail.com>        *
  *                                                                         *
  *   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  *
@@ -21,6 +20,7 @@
 #ifndef DOLPHINDETAILSVIEW_H
 #define DOLPHINDETAILSVIEW_H
 
+#include "dolphintreeview.h"
 #include <QTreeView>
 #include <libdolphin_export.h>
 #include <views/dolphinview.h>
@@ -37,7 +37,7 @@ class ViewExtensionsFactory;
  * that full available width of the view is used by stretching the width
  * of the name column.
  */
-class LIBDOLPHINPRIVATE_EXPORT DolphinDetailsView : public QTreeView
+class LIBDOLPHINPRIVATE_EXPORT DolphinDetailsView : public DolphinTreeView
 {
     Q_OBJECT
 
@@ -62,32 +62,22 @@ public:
      */
     QSet<KUrl> expandedUrls() const;
 
-    virtual QRegion visualRegionForSelection(const QItemSelection& selection) const;
-
 protected:
     virtual bool event(QEvent* event);
     virtual QStyleOptionViewItem viewOptions() const;
     virtual void contextMenuEvent(QContextMenuEvent* event);
     virtual void mousePressEvent(QMouseEvent* event);
-    virtual void mouseMoveEvent(QMouseEvent* event);
-    virtual void mouseReleaseEvent(QMouseEvent* event);
     virtual void startDrag(Qt::DropActions supportedActions);
     virtual void dragEnterEvent(QDragEnterEvent* event);
-    virtual void dragLeaveEvent(QDragLeaveEvent* event);
     virtual void dragMoveEvent(QDragMoveEvent* event);
     virtual void dropEvent(QDropEvent* event);
-    virtual void paintEvent(QPaintEvent* event);
     virtual void keyPressEvent(QKeyEvent* event);
-    virtual void keyReleaseEvent(QKeyEvent* event);
     virtual void resizeEvent(QResizeEvent* event);
     virtual void wheelEvent(QWheelEvent* event);
     virtual void currentChanged(const QModelIndex& current, const QModelIndex& previous);
     virtual bool eventFilter(QObject* watched, QEvent* event);
-    virtual QModelIndex indexAt (const QPoint& point) const;
     virtual QRect visualRect(const QModelIndex& index) const;
-    virtual void setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags command);
-    virtual void scrollTo(const QModelIndex& index, ScrollHint hint = EnsureVisible);
-
+    virtual bool acceptsDrop(const QModelIndex& index) const;
 
 protected slots:
     virtual void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end);
@@ -121,20 +111,6 @@ private slots:
      */
     void slotEntered(const QModelIndex& index);
 
-    /**
-     * Updates the destination \a destination from
-     * the elastic band to the current mouse position and triggers
-     * an update.
-     */
-    void updateElasticBand();
-
-    /**
-     * Returns the rectangle for the elastic band dependent from the
-     * origin \a origin, the current destination
-     * \a destination and the viewport position.
-     */
-    QRect elasticBandRect() const;
-
     void setZoomLevel(int level);
 
     void slotShowPreviewChanged();
@@ -183,13 +159,6 @@ private slots:
 
     void slotGlobalSettingsChanged(int category);
 
-    /**
-     * If the elastic band is currently shown, update the elastic band based on
-     * the current mouse position and ensure that the selection is the set of items
-     * intersecting it.
-     */
-    void updateElasticBandSelection();
-
     /**
      * If \a expandable is true, the details view acts as tree view.
      * The current expandable state is remembered in the settings.
@@ -219,22 +188,13 @@ private:
 
     KFileItemDelegate::Information infoForColumn(int columnIndex) const;
 
-    /**
-     * Returns true, if \a pos is within the expanding toggle of a tree.
-     */
-    bool isAboveExpandingToggle(const QPoint& pos) const;
-
     /**
      * Sets the maximum size available for editing in the delegate.
      */
     void adjustMaximumSizeForEditing(const QModelIndex& index);
 
 private:
-    bool m_autoResize : 1;        // if true, the columns are resized automatically to the available width
-    bool m_expandingTogglePressed : 1;
-    bool m_keyPressed : 1;        // true if a key is pressed currently; info used by currentChanged()
-    bool m_useDefaultIndexAt : 1; // true, if QTreeView::indexAt() should be used
-    bool m_ignoreScrollTo : 1;    // true if calls to scrollTo(...) should do nothing.
+    bool m_autoResize; // if true, the columns are resized automatically to the available width
 
     DolphinViewController* m_dolphinViewController;
     const ViewModeController* m_viewModeController;
@@ -249,39 +209,6 @@ private:
 
     QFont m_font;
     QSize m_decorationSize;
-
-    QRect m_dropRect;
-
-    struct ElasticBand
-    {
-        ElasticBand();
-
-        // Elastic band origin and destination coordinates are relative to t
-        // he origin of the view, not the viewport.
-        bool show;
-        QPoint origin;
-        QPoint destination;
-
-        // Optimization mechanisms for use with elastic band selection.
-        // Basically, allow "incremental" updates to the selection based
-        // on the last elastic band shape.
-        QPoint lastSelectionOrigin;
-        QPoint lastSelectionDestination;
-
-        // If true, compute the set of selected elements from scratch (slower)
-        bool ignoreOldInfo;
-
-        // Edges of the filenames that are closest to the edges of oldSelectionRect.
-        // Used to decide whether horizontal changes in the elastic band are likely
-        // to require us to re-check which items are selected.
-        int outsideNearestLeftEdge;
-        int outsideNearestRightEdge;
-        int insideNearestLeftEdge;
-        int insideNearestRightEdge;
-        // The set of items that were selected at the time this band was shown.
-        // NOTE: Unless CTRL was pressed when the band was created, this is always empty.
-        QItemSelection originalSelection;
-    } m_band;
 };
 
 #endif
diff --git a/src/views/dolphintreeview.cpp b/src/views/dolphintreeview.cpp
new file mode 100644 (file)
index 0000000..31c20cd
--- /dev/null
@@ -0,0 +1,552 @@
+/***************************************************************************
+ *   Copyright (C) 2010 by Peter Penz <peter.penz19@gmail.com>             *
+ *   Copyright (C) 2008 by Simon St. James <kdedevel@etotheipiplusone.com> *
+ *                                                                         *
+ *   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 "dolphintreeview.h"
+
+#include "dolphinmodel.h"
+
+#include <QApplication>
+#include <QEvent>
+#include <QHeaderView>
+#include <QMouseEvent>
+#include <QPainter>
+#include <QScrollBar>
+
+DolphinTreeView::DolphinTreeView(QWidget* parent) :
+    QTreeView(parent),
+    m_keyPressed(false),
+    m_expandingTogglePressed(false),
+    m_useDefaultIndexAt(true),
+    m_ignoreScrollTo(false),
+    m_dropRect(),
+    m_band()
+{
+}
+
+DolphinTreeView::~DolphinTreeView()
+{
+}
+
+QRegion DolphinTreeView::visualRegionForSelection(const QItemSelection& selection) const
+{
+    // We have to make sure that the visualRect of each model index is inside the region.
+    // QTreeView::visualRegionForSelection does not do it right because it assumes implicitly
+    // that all visualRects have the same width, which is in general not the case here.
+    QRegion selectionRegion;
+    const QModelIndexList indexes = selection.indexes();
+
+    foreach(const QModelIndex& index, indexes) {
+        selectionRegion += visualRect(index);
+    }
+
+    return selectionRegion;
+}
+
+bool DolphinTreeView::acceptsDrop(const QModelIndex& index) const
+{
+    Q_UNUSED(index);
+    return false;
+}
+
+bool DolphinTreeView::event(QEvent* event)
+{
+    switch (event->type()) {
+    case QEvent::Polish:
+        m_useDefaultIndexAt = false;
+        break;
+    case QEvent::FocusOut:
+        // If a key-press triggers an action that e. g. opens a dialog, the
+        // widget gets no key-release event. Assure that the pressed state
+        // is reset to prevent accidently setting the current index during a selection.
+        m_keyPressed = false;
+        break;
+    default:
+        break;
+    }
+    return QTreeView::event(event);
+}
+
+void DolphinTreeView::mousePressEvent(QMouseEvent* event)
+{
+    const QModelIndex current = currentIndex();
+    QTreeView::mousePressEvent(event);
+
+    m_expandingTogglePressed = isAboveExpandingToggle(event->pos());
+
+    const QModelIndex index = indexAt(event->pos());
+    const bool updateState = index.isValid() &&
+                             (index.column() == DolphinModel::Name) &&
+                             (event->button() == Qt::LeftButton);
+    if (updateState) {
+        setState(QAbstractItemView::DraggingState);
+    }
+
+    if (!index.isValid() || (index.column() != DolphinModel::Name)) {
+        const Qt::KeyboardModifiers mod = QApplication::keyboardModifiers();
+        if (!m_expandingTogglePressed && !(mod & Qt::ShiftModifier) && !(mod & Qt::ControlModifier)) {
+            clearSelection();
+        }
+
+        // Restore the current index, other columns are handled as viewport area.
+        // setCurrentIndex(...) implicitly calls scrollTo(...), which we want to ignore.
+        m_ignoreScrollTo = true;
+        selectionModel()->setCurrentIndex(current, QItemSelectionModel::Current);
+        m_ignoreScrollTo = false;
+
+        if ((event->button() == Qt::LeftButton) && !m_expandingTogglePressed) {
+            // Inform Qt about what we are doing - otherwise it starts dragging items around!
+            setState(DragSelectingState);
+            m_band.show = true;
+            // Incremental update data will not be useful - start from scratch.
+            m_band.ignoreOldInfo = true;
+            const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
+            m_band.origin = event->pos()  + scrollPos;
+            m_band.destination = m_band.origin;
+            m_band.originalSelection = selectionModel()->selection();
+        }
+    }
+}
+
+void DolphinTreeView::mouseMoveEvent(QMouseEvent* event)
+{
+    if (m_expandingTogglePressed) {
+        // Per default QTreeView starts either a selection or a drag operation when dragging
+        // the expanding toggle button (Qt-issue - see TODO comment in DolphinIconsView::mousePressEvent()).
+        // Turn off this behavior in Dolphin to stay predictable:
+        setState(QAbstractItemView::NoState);
+        return;
+    }
+
+    if (m_band.show) {
+        const QPoint mousePos = event->pos();
+        const QModelIndex index = indexAt(mousePos);
+        if (!index.isValid()) {
+            // The destination of the selection rectangle is above the viewport. In this
+            // case QTreeView does no selection at all, which is not the wanted behavior
+            // in Dolphin. Select all items within the elastic band rectangle.
+            updateElasticBandSelection();
+        }
+
+        // TODO: Enable QTreeView::mouseMoveEvent(event) again, as soon
+        // as the Qt-issue #199631 has been fixed.
+        // QTreeView::mouseMoveEvent(event);
+        QAbstractItemView::mouseMoveEvent(event);
+        updateElasticBand();
+    } else {
+        // TODO: Enable QTreeView::mouseMoveEvent(event) again, as soon
+        // as the Qt-issue #199631 has been fixed.
+        // QTreeView::mouseMoveEvent(event);
+        QAbstractItemView::mouseMoveEvent(event);
+    }
+}
+
+void DolphinTreeView::mouseReleaseEvent(QMouseEvent* event)
+{
+    if (!m_expandingTogglePressed) {
+        const QModelIndex index = indexAt(event->pos());
+        if (index.isValid() && (index.column() == DolphinModel::Name)) {
+            QTreeView::mouseReleaseEvent(event);
+        } else {
+            // don't change the current index if the cursor is released
+            // above any other column than the name column, as the other
+            // columns act as viewport
+            const QModelIndex current = currentIndex();
+            QTreeView::mouseReleaseEvent(event);
+            selectionModel()->setCurrentIndex(current, QItemSelectionModel::Current);
+        }
+    }
+    m_expandingTogglePressed = false;
+
+    if (m_band.show) {
+        setState(NoState);
+        updateElasticBand();
+        m_band.show = false;
+    }
+}
+
+void DolphinTreeView::startDrag(Qt::DropActions supportedActions)
+{
+    Q_UNUSED(supportedActions);
+    m_band.show = false;
+}
+
+void DolphinTreeView::dragEnterEvent(QDragEnterEvent* event)
+{
+    Q_UNUSED(event);
+    if (m_band.show) {
+        updateElasticBand();
+        m_band.show = false;
+    }
+}
+
+void DolphinTreeView::dragMoveEvent(QDragMoveEvent* event)
+{
+    QTreeView::dragMoveEvent(event);
+
+    setDirtyRegion(m_dropRect);
+
+    const QModelIndex index = indexAt(event->pos());
+    if (acceptsDrop(index)) {
+        m_dropRect = visualRect(index);
+    } else {
+        m_dropRect.setSize(QSize()); // set invalid
+    }
+    setDirtyRegion(m_dropRect);
+}
+
+void DolphinTreeView::dragLeaveEvent(QDragLeaveEvent* event)
+{
+    QTreeView::dragLeaveEvent(event);
+    setDirtyRegion(m_dropRect);
+}
+
+void DolphinTreeView::paintEvent(QPaintEvent* event)
+{
+    QTreeView::paintEvent(event);
+    if (m_band.show) {
+        // The following code has been taken from QListView
+        // and adapted to DolphinDetailsView.
+        // (C) 1992-2007 Trolltech ASA
+        QStyleOptionRubberBand opt;
+        opt.initFrom(this);
+        opt.shape = QRubberBand::Rectangle;
+        opt.opaque = false;
+        opt.rect = elasticBandRect();
+
+        QPainter painter(viewport());
+        painter.save();
+        style()->drawControl(QStyle::CE_RubberBand, &opt, &painter);
+        painter.restore();
+    }
+}
+
+void DolphinTreeView::keyPressEvent(QKeyEvent* event)
+{
+    // If the Control modifier is pressed, a multiple selection
+    // is done and DolphinDetailsView::currentChanged() may not
+    // not change the selection in a custom way.
+    m_keyPressed = !(event->modifiers() & Qt::ControlModifier);
+
+    QTreeView::keyPressEvent(event);
+}
+
+void DolphinTreeView::keyReleaseEvent(QKeyEvent* event)
+{
+    QTreeView::keyReleaseEvent(event);
+    m_keyPressed = false;
+}
+
+void DolphinTreeView::currentChanged(const QModelIndex& current, const QModelIndex& previous)
+{
+    QTreeView::currentChanged(current, previous);
+
+    // Stay consistent with QListView: When changing the current index by key presses,
+    // also change the selection.
+    if (m_keyPressed) {
+        setCurrentIndex(current);
+    }
+}
+
+QModelIndex DolphinTreeView::indexAt(const QPoint& point) const
+{
+    // The blank portion of the name column counts as empty space
+    const QModelIndex index = QTreeView::indexAt(point);
+    const bool isAboveEmptySpace  = !m_useDefaultIndexAt &&
+                                    (index.column() == KDirModel::Name) &&
+                                    !visualRect(index).contains(point);
+    return isAboveEmptySpace ? QModelIndex() : index;
+}
+
+void DolphinTreeView::setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags command)
+{
+    // We must override setSelection() as Qt calls it internally and when this happens
+    // we must ensure that the default indexAt() is used.
+    if (!m_band.show) {
+        m_useDefaultIndexAt = true;
+        QTreeView::setSelection(rect, command);
+        m_useDefaultIndexAt = false;
+    } else {
+        // Use our own elastic band selection algorithm
+        updateElasticBandSelection();
+    }
+}
+
+
+void DolphinTreeView::scrollTo(const QModelIndex & index, ScrollHint hint)
+{
+    if (!m_ignoreScrollTo) {
+        QTreeView::scrollTo(index, hint);
+    }
+}
+
+void DolphinTreeView::updateElasticBandSelection()
+{
+    if (!m_band.show) {
+        return;
+    }
+
+    // Ensure the elastic band itself is up-to-date, in
+    // case we are being called due to e.g. a drag event.
+    updateElasticBand();
+
+    // Clip horizontally to the name column, as some filenames will be
+    // longer than the column.  We don't clip vertically as origin
+    // may be above or below the current viewport area.
+    const int nameColumnX = header()->sectionPosition(DolphinModel::Name);
+    const int nameColumnWidth = header()->sectionSize(DolphinModel::Name);
+    QRect selRect = elasticBandRect().normalized();
+    QRect nameColumnArea(nameColumnX, selRect.y(), nameColumnWidth, selRect.height());
+    selRect = nameColumnArea.intersect(selRect).normalized();
+    // Get the last elastic band rectangle, expressed in viewpoint coordinates.
+    const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
+    QRect oldSelRect = QRect(m_band.lastSelectionOrigin - scrollPos, m_band.lastSelectionDestination - scrollPos).normalized();
+
+    if (selRect.isNull()) {
+        selectionModel()->select(m_band.originalSelection, QItemSelectionModel::ClearAndSelect);
+        m_band.ignoreOldInfo = true;
+        return;
+    }
+
+    if (!m_band.ignoreOldInfo) {
+        // Do some quick checks to see if we can rule out the need to
+        // update the selection.
+        Q_ASSERT(uniformRowHeights());
+        QModelIndex dummyIndex = model()->index(0, 0);
+        if (!dummyIndex.isValid()) {
+            // No items in the model presumably.
+            return;
+        }
+
+        // If the elastic band does not cover the same rows as before, we'll
+        // need to re-check, and also invalidate the old item distances.
+        const int rowHeight = QTreeView::rowHeight(dummyIndex);
+        const bool coveringSameRows =
+            (selRect.top()    / rowHeight == oldSelRect.top()    / rowHeight) &&
+            (selRect.bottom() / rowHeight == oldSelRect.bottom() / rowHeight);
+        if (coveringSameRows) {
+            // Covering the same rows, but have we moved far enough horizontally
+            // that we might have (de)selected some other items?
+            const bool itemSelectionChanged =
+                ((selRect.left() > oldSelRect.left()) &&
+                 (selRect.left() > m_band.insideNearestLeftEdge)) ||
+                ((selRect.left() < oldSelRect.left()) &&
+                 (selRect.left() <= m_band.outsideNearestLeftEdge)) ||
+                ((selRect.right() < oldSelRect.right()) &&
+                 (selRect.left() >= m_band.insideNearestRightEdge)) ||
+                ((selRect.right() > oldSelRect.right()) &&
+                 (selRect.right() >= m_band.outsideNearestRightEdge));
+
+            if (!itemSelectionChanged) {
+                return;
+            }
+        }
+    } else {
+        // This is the only piece of optimization data that needs to be explicitly
+        // discarded.
+        m_band.lastSelectionOrigin = QPoint();
+        m_band.lastSelectionDestination = QPoint();
+        oldSelRect = selRect;
+    }
+
+    // Do the selection from scratch. Force a update of the horizontal distances info.
+    m_band.insideNearestLeftEdge   = nameColumnX + nameColumnWidth + 1;
+    m_band.insideNearestRightEdge  = nameColumnX - 1;
+    m_band.outsideNearestLeftEdge  = nameColumnX - 1;
+    m_band.outsideNearestRightEdge = nameColumnX + nameColumnWidth + 1;
+
+    // Include the old selection rect as well, so we can deselect
+    // items that were inside it but not in the new selRect.
+    const QRect boundingRect = selRect.united(oldSelRect).normalized();
+    if (boundingRect.isNull()) {
+        return;
+    }
+
+    // Get the index of the item in this row in the name column.
+    // TODO - would this still work if the columns could be re-ordered?
+    QModelIndex startIndex = QTreeView::indexAt(boundingRect.topLeft());
+    if (startIndex.parent().isValid()) {
+        startIndex = startIndex.parent().child(startIndex.row(), KDirModel::Name);
+    } else {
+        startIndex = model()->index(startIndex.row(), KDirModel::Name);
+    }
+    if (!startIndex.isValid()) {
+        selectionModel()->select(m_band.originalSelection, QItemSelectionModel::ClearAndSelect);
+        m_band.ignoreOldInfo = true;
+        return;
+    }
+
+   // Go through all indexes between the top and bottom of boundingRect, and
+   // update the selection.
+   const int verticalCutoff = boundingRect.bottom();
+   QModelIndex currIndex = startIndex;
+   QModelIndex lastIndex;
+   bool allItemsInBoundDone = false;
+
+   // Calling selectionModel()->select(...) for each item that needs to be
+   // toggled is slow as each call emits selectionChanged(...) so store them
+   // and do the selection toggle in one batch.
+   QItemSelection itemsToToggle;
+   // QItemSelection's deal with continuous ranges of indexes better than
+   // single indexes, so try to portion items that need to be toggled into ranges.
+   bool formingToggleIndexRange = false;
+   QModelIndex toggleIndexRangeBegin = QModelIndex();
+
+   do {
+       QRect currIndexRect = visualRect(currIndex);
+
+        // Update some optimization info as we go.
+       const int cr = currIndexRect.right();
+       const int cl = currIndexRect.left();
+       const int sl = selRect.left();
+       const int sr = selRect.right();
+       // "The right edge of the name is outside of the rect but nearer than m_outsideNearestLeft", etc
+       if ((cr < sl && cr > m_band.outsideNearestLeftEdge)) {
+           m_band.outsideNearestLeftEdge = cr;
+       }
+       if ((cl > sr && cl < m_band.outsideNearestRightEdge)) {
+           m_band.outsideNearestRightEdge = cl;
+       }
+       if ((cl >= sl && cl <= sr && cl > m_band.insideNearestRightEdge)) {
+           m_band.insideNearestRightEdge = cl;
+       }
+       if ((cr >= sl && cr <= sr && cr < m_band.insideNearestLeftEdge)) {
+           m_band.insideNearestLeftEdge = cr;
+       }
+
+       bool currentlySelected = selectionModel()->isSelected(currIndex);
+       bool originallySelected = m_band.originalSelection.contains(currIndex);
+       bool intersectsSelectedRect = currIndexRect.intersects(selRect);
+       bool shouldBeSelected = (intersectsSelectedRect && !originallySelected) || (!intersectsSelectedRect && originallySelected);
+       bool needToToggleItem = (currentlySelected && !shouldBeSelected) || (!currentlySelected && shouldBeSelected);
+       if (needToToggleItem && !formingToggleIndexRange) {
+            toggleIndexRangeBegin = currIndex;
+            formingToggleIndexRange = true;
+       }
+
+       // NOTE: indexBelow actually walks up and down expanded trees for us.
+       QModelIndex nextIndex = indexBelow(currIndex);
+       allItemsInBoundDone = !nextIndex.isValid() || currIndexRect.top() > verticalCutoff;
+
+       const bool commitToggleIndexRange = formingToggleIndexRange &&
+                                           (!needToToggleItem ||
+                                            allItemsInBoundDone ||
+                                            currIndex.parent() != toggleIndexRangeBegin.parent());
+       if (commitToggleIndexRange) {
+           formingToggleIndexRange = false;
+            // If this is the last item in the bounds and it is also the beginning of a range,
+            // don't toggle lastIndex - it will already have been dealt with.
+           if (!allItemsInBoundDone || toggleIndexRangeBegin != currIndex) {
+               itemsToToggle.select(toggleIndexRangeBegin, lastIndex);
+           }
+            // Need to start a new range immediately with currIndex?
+           if (needToToggleItem) {
+               toggleIndexRangeBegin = currIndex;
+               formingToggleIndexRange = true;
+           }
+           if (allItemsInBoundDone && needToToggleItem) {
+                // Toggle the very last item in the bounds.
+               itemsToToggle.select(currIndex, currIndex);
+           }
+       }
+
+       // Next item
+       lastIndex = currIndex;
+       currIndex = nextIndex;
+    } while (!allItemsInBoundDone);
+
+
+    selectionModel()->select(itemsToToggle, QItemSelectionModel::Toggle);
+
+    m_band.lastSelectionOrigin = m_band.origin;
+    m_band.lastSelectionDestination = m_band.destination;
+    m_band.ignoreOldInfo = false;
+}
+
+void DolphinTreeView::updateElasticBand()
+{
+    if (m_band.show) {
+        QRect dirtyRegion(elasticBandRect());
+        const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
+        m_band.destination = viewport()->mapFromGlobal(QCursor::pos()) + scrollPos;
+        // Going above the (logical) top-left of the view causes complications during selection;
+        // we may as well prevent it.
+        if (m_band.destination.y() < 0) {
+            m_band.destination.setY(0);
+        }
+        if (m_band.destination.x() < 0) {
+            m_band.destination.setX(0);
+        }
+        dirtyRegion = dirtyRegion.united(elasticBandRect());
+        setDirtyRegion(dirtyRegion);
+    }
+}
+
+QRect DolphinTreeView::elasticBandRect() const
+{
+    const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
+
+    const QPoint topLeft = m_band.origin - scrollPos;
+    const QPoint bottomRight = m_band.destination - scrollPos;
+    return QRect(topLeft, bottomRight).normalized();
+}
+
+bool DolphinTreeView::isAboveExpandingToggle(const QPoint& pos) const
+{
+    // QTreeView offers no public API to get the information whether an index has an
+    // expanding toggle and what boundaries the toggle has. The following approach
+    // also assumes a toggle for file items.
+    if (itemsExpandable()) {
+        const QModelIndex index = QTreeView::indexAt(pos);
+        if (index.isValid() && (index.column() == KDirModel::Name)) {
+            QRect rect = visualRect(index);
+            const int toggleSize = rect.height();
+            if (isRightToLeft()) {
+                rect.moveRight(rect.right());
+            } else {
+                rect.moveLeft(rect.x() - toggleSize);
+            }
+            rect.setWidth(toggleSize);
+
+            QStyleOption opt;
+            opt.initFrom(this);
+            opt.rect = rect;
+            rect = style()->subElementRect(QStyle::SE_TreeViewDisclosureItem, &opt, this);
+
+            return rect.contains(pos);
+        }
+    }
+    return false;
+}
+
+DolphinTreeView::ElasticBand::ElasticBand() :
+    show(false),
+    origin(),
+    destination(),
+    lastSelectionOrigin(),
+    lastSelectionDestination(),
+    ignoreOldInfo(true),
+    outsideNearestLeftEdge(0),
+    outsideNearestRightEdge(0),
+    insideNearestLeftEdge(0),
+    insideNearestRightEdge(0)
+{
+}
+
+#include "dolphintreeview.moc"
diff --git a/src/views/dolphintreeview.h b/src/views/dolphintreeview.h
new file mode 100644 (file)
index 0000000..67f8eed
--- /dev/null
@@ -0,0 +1,137 @@
+/***************************************************************************
+ *   Copyright (C) 2010 by Peter Penz <peter.penz19@gmail.com>             *
+ *   Copyright (C) 2008 by Simon St. James <kdedevel@etotheipiplusone.com> *
+ *                                                                         *
+ *   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 DOLPHINTREEVIEW_H
+#define DOLPHINTREEVIEW_H
+
+#include <QTreeView>
+#include <libdolphin_export.h>
+
+/**
+ * @brief Extends QTreeView by a custom selection- and hover-mechanism.
+ *
+ * QTreeView does not respect the width of a cell for the hover-feedback
+ * and when selecting items. DolphinTreeView improves this by respecting the
+ * content-width of the first column. The selection-mechanism also
+ * respects the content-width.
+ */
+class LIBDOLPHINPRIVATE_EXPORT DolphinTreeView : public QTreeView
+{
+    Q_OBJECT
+
+public:
+    explicit DolphinTreeView(QWidget* parent = 0);
+    virtual ~DolphinTreeView();
+
+    virtual QRegion visualRegionForSelection(const QItemSelection& selection) const;
+
+protected:   
+    /**
+     * @return True, if the item with the index \p index accepts a drop. In this
+     *         case a visual feedback for the user is given during dragging. Per
+     *         default false is returned.
+     */
+    virtual bool acceptsDrop(const QModelIndex& index) const;
+
+    virtual bool event(QEvent* event);
+    virtual void mousePressEvent(QMouseEvent* event);
+    virtual void mouseMoveEvent(QMouseEvent* event);
+    virtual void mouseReleaseEvent(QMouseEvent* event);
+    virtual void startDrag(Qt::DropActions supportedActions);
+    virtual void dragEnterEvent(QDragEnterEvent* event);
+    virtual void dragMoveEvent(QDragMoveEvent* event);
+    virtual void dragLeaveEvent(QDragLeaveEvent* event);
+    virtual void paintEvent(QPaintEvent* event);
+    virtual void keyPressEvent(QKeyEvent* event);
+    virtual void keyReleaseEvent(QKeyEvent* event);
+    virtual void currentChanged(const QModelIndex& current, const QModelIndex& previous);
+    virtual QModelIndex indexAt (const QPoint& point) const;
+    virtual void setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags command);
+    virtual void scrollTo(const QModelIndex& index, ScrollHint hint = EnsureVisible);
+
+private slots:
+    /**
+     * If the elastic band is currently shown, update the elastic band based on
+     * the current mouse position and ensure that the selection is the set of items
+     * intersecting it.
+     */
+    void updateElasticBandSelection();
+
+    /**
+     * Updates the destination \a destination from
+     * the elastic band to the current mouse position and triggers
+     * an update.
+     */
+    void updateElasticBand();
+
+    /**
+     * Returns the rectangle for the elastic band dependent from the
+     * origin \a origin, the current destination
+     * \a destination and the viewport position.
+     */
+    QRect elasticBandRect() const;
+
+private:
+    /**
+     * Returns true, if \a pos is within the expanding toggle of a tree.
+     */
+    bool isAboveExpandingToggle(const QPoint& pos) const;
+
+private:
+    bool m_keyPressed;        // true if a key is pressed currently; info used by currentChanged()
+    bool m_expandingTogglePressed;
+    bool m_useDefaultIndexAt; // true, if QTreeView::indexAt() should be used
+    bool m_ignoreScrollTo;    // true if calls to scrollTo(...) should do nothing.
+
+    QRect m_dropRect;
+
+    struct ElasticBand
+    {
+        ElasticBand();
+
+        // Elastic band origin and destination coordinates are relative to t
+        // he origin of the view, not the viewport.
+        bool show;
+        QPoint origin;
+        QPoint destination;
+
+        // Optimization mechanisms for use with elastic band selection.
+        // Basically, allow "incremental" updates to the selection based
+        // on the last elastic band shape.
+        QPoint lastSelectionOrigin;
+        QPoint lastSelectionDestination;
+
+        // If true, compute the set of selected elements from scratch (slower)
+        bool ignoreOldInfo;
+
+        // Edges of the filenames that are closest to the edges of oldSelectionRect.
+        // Used to decide whether horizontal changes in the elastic band are likely
+        // to require us to re-check which items are selected.
+        int outsideNearestLeftEdge;
+        int outsideNearestRightEdge;
+        int insideNearestLeftEdge;
+        int insideNearestRightEdge;
+        // The set of items that were selected at the time this band was shown.
+        // NOTE: Unless CTRL was pressed when the band was created, this is always empty.
+        QItemSelection originalSelection;
+    } m_band;
+};
+
+#endif