1 /***************************************************************************
2 * Copyright (C) 2010 by Peter Penz <peter.penz19@gmail.com> *
3 * Copyright (C) 2008 by Simon St. James <kdedevel@etotheipiplusone.com> *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) any later version. *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, write to the *
17 * Free Software Foundation, Inc., *
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
19 ***************************************************************************/
21 #include "dolphintreeview.h"
23 #include "dolphinmodel.h"
25 #include <QApplication>
27 #include <QHeaderView>
28 #include <QMouseEvent>
32 DolphinTreeView::DolphinTreeView(QWidget
* parent
) :
34 m_expandingTogglePressed(false),
35 m_useDefaultIndexAt(true),
36 m_ignoreScrollTo(false),
40 setUniformRowHeights(true);
43 DolphinTreeView::~DolphinTreeView()
47 void DolphinTreeView::keyboardSearch(const QString
& search
)
49 const QModelIndex oldCurrent
= currentIndex();
50 QTreeView::keyboardSearch(search
);
51 if (currentIndex() != oldCurrent
) {
52 // The current index has changed, but it is not selected yet.
53 // To select it, we call setCurrentIndex(...).
54 setCurrentIndex(currentIndex());
58 QRegion
DolphinTreeView::visualRegionForSelection(const QItemSelection
& selection
) const
60 // We have to make sure that the visualRect of each model index is inside the region.
61 // QTreeView::visualRegionForSelection does not do it right because it assumes implicitly
62 // that all visualRects have the same width, which is in general not the case here.
63 QRegion selectionRegion
;
64 const QModelIndexList indexes
= selection
.indexes();
66 foreach(const QModelIndex
& index
, indexes
) {
67 selectionRegion
+= visualRect(index
);
70 return selectionRegion
;
73 bool DolphinTreeView::acceptsDrop(const QModelIndex
& index
) const
79 bool DolphinTreeView::event(QEvent
* event
)
81 switch (event
->type()) {
83 m_useDefaultIndexAt
= false;
88 return QTreeView::event(event
);
91 void DolphinTreeView::mousePressEvent(QMouseEvent
* event
)
93 const QModelIndex current
= currentIndex();
94 QTreeView::mousePressEvent(event
);
96 m_expandingTogglePressed
= isAboveExpandingToggle(event
->pos());
98 const QModelIndex index
= indexAt(event
->pos());
99 const bool updateState
= index
.isValid() &&
100 (index
.column() == DolphinModel::Name
) &&
101 (event
->button() == Qt::LeftButton
);
103 setState(QAbstractItemView::DraggingState
);
106 if (!index
.isValid() || (index
.column() != DolphinModel::Name
)) {
107 const Qt::KeyboardModifiers mod
= QApplication::keyboardModifiers();
108 if (!m_expandingTogglePressed
&& !(mod
& Qt::ShiftModifier
) && !(mod
& Qt::ControlModifier
)) {
112 // Restore the current index, other columns are handled as viewport area.
113 // setCurrentIndex(...) implicitly calls scrollTo(...), which we want to ignore.
114 m_ignoreScrollTo
= true;
115 selectionModel()->setCurrentIndex(current
, QItemSelectionModel::Current
);
116 m_ignoreScrollTo
= false;
118 if ((event
->button() == Qt::LeftButton
) && !m_expandingTogglePressed
) {
119 // Inform Qt about what we are doing - otherwise it starts dragging items around!
120 setState(DragSelectingState
);
122 // Incremental update data will not be useful - start from scratch.
123 m_band
.ignoreOldInfo
= true;
124 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
125 m_band
.origin
= event
->pos() + scrollPos
;
126 m_band
.destination
= m_band
.origin
;
127 m_band
.originalSelection
= selectionModel()->selection();
132 void DolphinTreeView::mouseMoveEvent(QMouseEvent
* event
)
134 if (m_expandingTogglePressed
) {
135 // Per default QTreeView starts either a selection or a drag operation when dragging
136 // the expanding toggle button (Qt-issue - see TODO comment in DolphinIconsView::mousePressEvent()).
137 // Turn off this behavior in Dolphin to stay predictable:
138 setState(QAbstractItemView::NoState
);
143 const QPoint mousePos
= event
->pos();
144 const QModelIndex index
= indexAt(mousePos
);
145 if (!index
.isValid()) {
146 // The destination of the selection rectangle is above the viewport. In this
147 // case QTreeView does no selection at all, which is not the wanted behavior
148 // in Dolphin. Select all items within the elastic band rectangle.
149 updateElasticBandSelection();
152 // TODO: Enable QTreeView::mouseMoveEvent(event) again, as soon
153 // as the Qt-issue #199631 has been fixed.
154 // QTreeView::mouseMoveEvent(event);
155 QAbstractItemView::mouseMoveEvent(event
);
158 // TODO: Enable QTreeView::mouseMoveEvent(event) again, as soon
159 // as the Qt-issue #199631 has been fixed.
160 // QTreeView::mouseMoveEvent(event);
161 QAbstractItemView::mouseMoveEvent(event
);
165 void DolphinTreeView::mouseReleaseEvent(QMouseEvent
* event
)
167 if (!m_expandingTogglePressed
) {
168 const QModelIndex index
= indexAt(event
->pos());
169 if (index
.isValid() && (index
.column() == DolphinModel::Name
)) {
170 QTreeView::mouseReleaseEvent(event
);
172 // don't change the current index if the cursor is released
173 // above any other column than the name column, as the other
174 // columns act as viewport
175 const QModelIndex current
= currentIndex();
176 QTreeView::mouseReleaseEvent(event
);
177 selectionModel()->setCurrentIndex(current
, QItemSelectionModel::Current
);
180 m_expandingTogglePressed
= false;
189 void DolphinTreeView::startDrag(Qt::DropActions supportedActions
)
191 Q_UNUSED(supportedActions
);
195 void DolphinTreeView::dragEnterEvent(QDragEnterEvent
* event
)
204 void DolphinTreeView::dragMoveEvent(QDragMoveEvent
* event
)
206 QTreeView::dragMoveEvent(event
);
208 setDirtyRegion(m_dropRect
);
210 const QModelIndex index
= indexAt(event
->pos());
211 if (acceptsDrop(index
)) {
212 m_dropRect
= visualRect(index
);
214 m_dropRect
.setSize(QSize()); // set invalid
216 setDirtyRegion(m_dropRect
);
219 void DolphinTreeView::dragLeaveEvent(QDragLeaveEvent
* event
)
221 QTreeView::dragLeaveEvent(event
);
222 setDirtyRegion(m_dropRect
);
225 void DolphinTreeView::paintEvent(QPaintEvent
* event
)
227 QTreeView::paintEvent(event
);
229 // The following code has been taken from QListView
230 // and adapted to DolphinDetailsView.
231 // (C) 1992-2007 Trolltech ASA
232 QStyleOptionRubberBand opt
;
234 opt
.shape
= QRubberBand::Rectangle
;
236 opt
.rect
= elasticBandRect();
238 QPainter
painter(viewport());
240 style()->drawControl(QStyle::CE_RubberBand
, &opt
, &painter
);
245 QModelIndex
DolphinTreeView::indexAt(const QPoint
& point
) const
247 // The blank portion of the name column counts as empty space
248 const QModelIndex index
= QTreeView::indexAt(point
);
249 const bool isAboveEmptySpace
= !m_useDefaultIndexAt
&&
250 (index
.column() == KDirModel::Name
) &&
251 !visualRect(index
).contains(point
);
252 return isAboveEmptySpace
? QModelIndex() : index
;
255 void DolphinTreeView::setSelection(const QRect
& rect
, QItemSelectionModel::SelectionFlags command
)
257 // We must override setSelection() as Qt calls it internally and when this happens
258 // we must ensure that the default indexAt() is used.
260 m_useDefaultIndexAt
= true;
261 QTreeView::setSelection(rect
, command
);
262 m_useDefaultIndexAt
= false;
264 // Use our own elastic band selection algorithm
265 updateElasticBandSelection();
270 void DolphinTreeView::scrollTo(const QModelIndex
& index
, ScrollHint hint
)
272 if (!m_ignoreScrollTo
) {
273 QTreeView::scrollTo(index
, hint
);
277 void DolphinTreeView::updateElasticBandSelection()
283 // Ensure the elastic band itself is up-to-date, in
284 // case we are being called due to e.g. a drag event.
287 // Clip horizontally to the name column, as some filenames will be
288 // longer than the column. We don't clip vertically as origin
289 // may be above or below the current viewport area.
290 const int nameColumnX
= header()->sectionPosition(DolphinModel::Name
);
291 const int nameColumnWidth
= header()->sectionSize(DolphinModel::Name
);
292 QRect selRect
= elasticBandRect().normalized();
293 QRect
nameColumnArea(nameColumnX
, selRect
.y(), nameColumnWidth
, selRect
.height());
294 selRect
= nameColumnArea
.intersect(selRect
).normalized();
295 // Get the last elastic band rectangle, expressed in viewpoint coordinates.
296 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
297 QRect oldSelRect
= QRect(m_band
.lastSelectionOrigin
- scrollPos
, m_band
.lastSelectionDestination
- scrollPos
).normalized();
299 if (selRect
.isNull()) {
300 selectionModel()->select(m_band
.originalSelection
, QItemSelectionModel::ClearAndSelect
);
301 m_band
.ignoreOldInfo
= true;
305 if (!m_band
.ignoreOldInfo
) {
306 // Do some quick checks to see if we can rule out the need to
307 // update the selection.
308 Q_ASSERT(uniformRowHeights());
309 QModelIndex dummyIndex
= model()->index(0, 0);
310 if (!dummyIndex
.isValid()) {
311 // No items in the model presumably.
315 // If the elastic band does not cover the same rows as before, we'll
316 // need to re-check, and also invalidate the old item distances.
317 const int rowHeight
= QTreeView::rowHeight(dummyIndex
);
318 const bool coveringSameRows
=
319 (selRect
.top() / rowHeight
== oldSelRect
.top() / rowHeight
) &&
320 (selRect
.bottom() / rowHeight
== oldSelRect
.bottom() / rowHeight
);
321 if (coveringSameRows
) {
322 // Covering the same rows, but have we moved far enough horizontally
323 // that we might have (de)selected some other items?
324 const bool itemSelectionChanged
=
325 ((selRect
.left() > oldSelRect
.left()) &&
326 (selRect
.left() > m_band
.insideNearestLeftEdge
)) ||
327 ((selRect
.left() < oldSelRect
.left()) &&
328 (selRect
.left() <= m_band
.outsideNearestLeftEdge
)) ||
329 ((selRect
.right() < oldSelRect
.right()) &&
330 (selRect
.left() >= m_band
.insideNearestRightEdge
)) ||
331 ((selRect
.right() > oldSelRect
.right()) &&
332 (selRect
.right() >= m_band
.outsideNearestRightEdge
));
334 if (!itemSelectionChanged
) {
339 // This is the only piece of optimization data that needs to be explicitly
341 m_band
.lastSelectionOrigin
= QPoint();
342 m_band
.lastSelectionDestination
= QPoint();
343 oldSelRect
= selRect
;
346 // Do the selection from scratch. Force a update of the horizontal distances info.
347 m_band
.insideNearestLeftEdge
= nameColumnX
+ nameColumnWidth
+ 1;
348 m_band
.insideNearestRightEdge
= nameColumnX
- 1;
349 m_band
.outsideNearestLeftEdge
= nameColumnX
- 1;
350 m_band
.outsideNearestRightEdge
= nameColumnX
+ nameColumnWidth
+ 1;
352 // Include the old selection rect as well, so we can deselect
353 // items that were inside it but not in the new selRect.
354 const QRect boundingRect
= selRect
.united(oldSelRect
).normalized();
355 if (boundingRect
.isNull()) {
359 // Get the index of the item in this row in the name column.
360 // TODO - would this still work if the columns could be re-ordered?
361 QModelIndex startIndex
= QTreeView::indexAt(boundingRect
.topLeft());
362 if (startIndex
.parent().isValid()) {
363 startIndex
= startIndex
.parent().child(startIndex
.row(), KDirModel::Name
);
365 startIndex
= model()->index(startIndex
.row(), KDirModel::Name
);
367 if (!startIndex
.isValid()) {
368 selectionModel()->select(m_band
.originalSelection
, QItemSelectionModel::ClearAndSelect
);
369 m_band
.ignoreOldInfo
= true;
373 // Go through all indexes between the top and bottom of boundingRect, and
374 // update the selection.
375 const int verticalCutoff
= boundingRect
.bottom();
376 QModelIndex currIndex
= startIndex
;
377 QModelIndex lastIndex
;
378 bool allItemsInBoundDone
= false;
380 // Calling selectionModel()->select(...) for each item that needs to be
381 // toggled is slow as each call emits selectionChanged(...) so store them
382 // and do the selection toggle in one batch.
383 QItemSelection itemsToToggle
;
384 // QItemSelection's deal with continuous ranges of indexes better than
385 // single indexes, so try to portion items that need to be toggled into ranges.
386 bool formingToggleIndexRange
= false;
387 QModelIndex toggleIndexRangeBegin
= QModelIndex();
390 QRect currIndexRect
= visualRect(currIndex
);
392 // Update some optimization info as we go.
393 const int cr
= currIndexRect
.right();
394 const int cl
= currIndexRect
.left();
395 const int sl
= selRect
.left();
396 const int sr
= selRect
.right();
397 // "The right edge of the name is outside of the rect but nearer than m_outsideNearestLeft", etc
398 if ((cr
< sl
&& cr
> m_band
.outsideNearestLeftEdge
)) {
399 m_band
.outsideNearestLeftEdge
= cr
;
401 if ((cl
> sr
&& cl
< m_band
.outsideNearestRightEdge
)) {
402 m_band
.outsideNearestRightEdge
= cl
;
404 if ((cl
>= sl
&& cl
<= sr
&& cl
> m_band
.insideNearestRightEdge
)) {
405 m_band
.insideNearestRightEdge
= cl
;
407 if ((cr
>= sl
&& cr
<= sr
&& cr
< m_band
.insideNearestLeftEdge
)) {
408 m_band
.insideNearestLeftEdge
= cr
;
411 bool currentlySelected
= selectionModel()->isSelected(currIndex
);
412 bool originallySelected
= m_band
.originalSelection
.contains(currIndex
);
413 bool intersectsSelectedRect
= currIndexRect
.intersects(selRect
);
414 bool shouldBeSelected
= (intersectsSelectedRect
&& !originallySelected
) || (!intersectsSelectedRect
&& originallySelected
);
415 bool needToToggleItem
= (currentlySelected
&& !shouldBeSelected
) || (!currentlySelected
&& shouldBeSelected
);
416 if (needToToggleItem
&& !formingToggleIndexRange
) {
417 toggleIndexRangeBegin
= currIndex
;
418 formingToggleIndexRange
= true;
421 // NOTE: indexBelow actually walks up and down expanded trees for us.
422 QModelIndex nextIndex
= indexBelow(currIndex
);
423 allItemsInBoundDone
= !nextIndex
.isValid() || currIndexRect
.top() > verticalCutoff
;
425 const bool commitToggleIndexRange
= formingToggleIndexRange
&&
426 (!needToToggleItem
||
427 allItemsInBoundDone
||
428 currIndex
.parent() != toggleIndexRangeBegin
.parent());
429 if (commitToggleIndexRange
) {
430 formingToggleIndexRange
= false;
431 // If this is the last item in the bounds and it is also the beginning of a range,
432 // don't toggle lastIndex - it will already have been dealt with.
433 if (!allItemsInBoundDone
|| toggleIndexRangeBegin
!= currIndex
) {
434 itemsToToggle
.select(toggleIndexRangeBegin
, lastIndex
);
436 // Need to start a new range immediately with currIndex?
437 if (needToToggleItem
) {
438 toggleIndexRangeBegin
= currIndex
;
439 formingToggleIndexRange
= true;
441 if (allItemsInBoundDone
&& needToToggleItem
) {
442 // Toggle the very last item in the bounds.
443 itemsToToggle
.select(currIndex
, currIndex
);
448 lastIndex
= currIndex
;
449 currIndex
= nextIndex
;
450 } while (!allItemsInBoundDone
);
453 selectionModel()->select(itemsToToggle
, QItemSelectionModel::Toggle
);
455 m_band
.lastSelectionOrigin
= m_band
.origin
;
456 m_band
.lastSelectionDestination
= m_band
.destination
;
457 m_band
.ignoreOldInfo
= false;
460 void DolphinTreeView::updateElasticBand()
463 QRect
dirtyRegion(elasticBandRect());
464 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
465 m_band
.destination
= viewport()->mapFromGlobal(QCursor::pos()) + scrollPos
;
466 // Going above the (logical) top-left of the view causes complications during selection;
467 // we may as well prevent it.
468 if (m_band
.destination
.y() < 0) {
469 m_band
.destination
.setY(0);
471 if (m_band
.destination
.x() < 0) {
472 m_band
.destination
.setX(0);
474 dirtyRegion
= dirtyRegion
.united(elasticBandRect());
475 setDirtyRegion(dirtyRegion
);
479 QRect
DolphinTreeView::elasticBandRect() const
481 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
483 const QPoint topLeft
= m_band
.origin
- scrollPos
;
484 const QPoint bottomRight
= m_band
.destination
- scrollPos
;
485 return QRect(topLeft
, bottomRight
).normalized();
488 bool DolphinTreeView::isAboveExpandingToggle(const QPoint
& pos
) const
490 // QTreeView offers no public API to get the information whether an index has an
491 // expanding toggle and what boundaries the toggle has. The following approach
492 // also assumes a toggle for file items.
493 if (itemsExpandable()) {
494 const QModelIndex index
= QTreeView::indexAt(pos
);
495 if (index
.isValid() && (index
.column() == KDirModel::Name
)) {
496 QRect rect
= visualRect(index
);
497 const int toggleSize
= rect
.height();
498 if (isRightToLeft()) {
499 rect
.moveRight(rect
.right());
501 rect
.moveLeft(rect
.x() - toggleSize
);
503 rect
.setWidth(toggleSize
);
508 rect
= style()->subElementRect(QStyle::SE_TreeViewDisclosureItem
, &opt
, this);
510 return rect
.contains(pos
);
516 DolphinTreeView::ElasticBand::ElasticBand() :
520 lastSelectionOrigin(),
521 lastSelectionDestination(),
523 outsideNearestLeftEdge(0),
524 outsideNearestRightEdge(0),
525 insideNearestLeftEdge(0),
526 insideNearestRightEdge(0)
530 #include "dolphintreeview.moc"