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_updateCurrentIndex(false),
35 m_expandingTogglePressed(false),
36 m_useDefaultIndexAt(true),
37 m_ignoreScrollTo(false),
41 setUniformRowHeights(true);
44 DolphinTreeView::~DolphinTreeView()
48 QRegion
DolphinTreeView::visualRegionForSelection(const QItemSelection
& selection
) const
50 // We have to make sure that the visualRect of each model index is inside the region.
51 // QTreeView::visualRegionForSelection does not do it right because it assumes implicitly
52 // that all visualRects have the same width, which is in general not the case here.
53 QRegion selectionRegion
;
54 const QModelIndexList indexes
= selection
.indexes();
56 foreach(const QModelIndex
& index
, indexes
) {
57 selectionRegion
+= visualRect(index
);
60 return selectionRegion
;
63 bool DolphinTreeView::acceptsDrop(const QModelIndex
& index
) const
69 bool DolphinTreeView::event(QEvent
* event
)
71 switch (event
->type()) {
73 m_useDefaultIndexAt
= false;
75 case QEvent::FocusOut
:
76 // If a key-press triggers an action that e. g. opens a dialog, the
77 // widget gets no key-release event. Assure that the pressed state
78 // is reset to prevent accidently setting the current index during a selection.
79 m_updateCurrentIndex
= false;
84 return QTreeView::event(event
);
87 void DolphinTreeView::mousePressEvent(QMouseEvent
* event
)
89 const QModelIndex current
= currentIndex();
90 QTreeView::mousePressEvent(event
);
92 m_expandingTogglePressed
= isAboveExpandingToggle(event
->pos());
94 const QModelIndex index
= indexAt(event
->pos());
95 const bool updateState
= index
.isValid() &&
96 (index
.column() == DolphinModel::Name
) &&
97 (event
->button() == Qt::LeftButton
);
99 setState(QAbstractItemView::DraggingState
);
102 if (!index
.isValid() || (index
.column() != DolphinModel::Name
)) {
103 const Qt::KeyboardModifiers mod
= QApplication::keyboardModifiers();
104 if (!m_expandingTogglePressed
&& !(mod
& Qt::ShiftModifier
) && !(mod
& Qt::ControlModifier
)) {
108 // Restore the current index, other columns are handled as viewport area.
109 // setCurrentIndex(...) implicitly calls scrollTo(...), which we want to ignore.
110 m_ignoreScrollTo
= true;
111 selectionModel()->setCurrentIndex(current
, QItemSelectionModel::Current
);
112 m_ignoreScrollTo
= false;
114 if ((event
->button() == Qt::LeftButton
) && !m_expandingTogglePressed
) {
115 // Inform Qt about what we are doing - otherwise it starts dragging items around!
116 setState(DragSelectingState
);
118 // Incremental update data will not be useful - start from scratch.
119 m_band
.ignoreOldInfo
= true;
120 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
121 m_band
.origin
= event
->pos() + scrollPos
;
122 m_band
.destination
= m_band
.origin
;
123 m_band
.originalSelection
= selectionModel()->selection();
128 void DolphinTreeView::mouseMoveEvent(QMouseEvent
* event
)
130 if (m_expandingTogglePressed
) {
131 // Per default QTreeView starts either a selection or a drag operation when dragging
132 // the expanding toggle button (Qt-issue - see TODO comment in DolphinIconsView::mousePressEvent()).
133 // Turn off this behavior in Dolphin to stay predictable:
134 setState(QAbstractItemView::NoState
);
139 const QPoint mousePos
= event
->pos();
140 const QModelIndex index
= indexAt(mousePos
);
141 if (!index
.isValid()) {
142 // The destination of the selection rectangle is above the viewport. In this
143 // case QTreeView does no selection at all, which is not the wanted behavior
144 // in Dolphin. Select all items within the elastic band rectangle.
145 updateElasticBandSelection();
148 // TODO: Enable QTreeView::mouseMoveEvent(event) again, as soon
149 // as the Qt-issue #199631 has been fixed.
150 // QTreeView::mouseMoveEvent(event);
151 QAbstractItemView::mouseMoveEvent(event
);
154 // TODO: Enable QTreeView::mouseMoveEvent(event) again, as soon
155 // as the Qt-issue #199631 has been fixed.
156 // QTreeView::mouseMoveEvent(event);
157 QAbstractItemView::mouseMoveEvent(event
);
161 void DolphinTreeView::mouseReleaseEvent(QMouseEvent
* event
)
163 if (!m_expandingTogglePressed
) {
164 const QModelIndex index
= indexAt(event
->pos());
165 if (index
.isValid() && (index
.column() == DolphinModel::Name
)) {
166 QTreeView::mouseReleaseEvent(event
);
168 // don't change the current index if the cursor is released
169 // above any other column than the name column, as the other
170 // columns act as viewport
171 const QModelIndex current
= currentIndex();
172 QTreeView::mouseReleaseEvent(event
);
173 selectionModel()->setCurrentIndex(current
, QItemSelectionModel::Current
);
176 m_expandingTogglePressed
= false;
185 void DolphinTreeView::startDrag(Qt::DropActions supportedActions
)
187 Q_UNUSED(supportedActions
);
191 void DolphinTreeView::dragEnterEvent(QDragEnterEvent
* event
)
200 void DolphinTreeView::dragMoveEvent(QDragMoveEvent
* event
)
202 QTreeView::dragMoveEvent(event
);
204 setDirtyRegion(m_dropRect
);
206 const QModelIndex index
= indexAt(event
->pos());
207 if (acceptsDrop(index
)) {
208 m_dropRect
= visualRect(index
);
210 m_dropRect
.setSize(QSize()); // set invalid
212 setDirtyRegion(m_dropRect
);
215 void DolphinTreeView::dragLeaveEvent(QDragLeaveEvent
* event
)
217 QTreeView::dragLeaveEvent(event
);
218 setDirtyRegion(m_dropRect
);
221 void DolphinTreeView::paintEvent(QPaintEvent
* event
)
223 QTreeView::paintEvent(event
);
225 // The following code has been taken from QListView
226 // and adapted to DolphinDetailsView.
227 // (C) 1992-2007 Trolltech ASA
228 QStyleOptionRubberBand opt
;
230 opt
.shape
= QRubberBand::Rectangle
;
232 opt
.rect
= elasticBandRect();
234 QPainter
painter(viewport());
236 style()->drawControl(QStyle::CE_RubberBand
, &opt
, &painter
);
241 void DolphinTreeView::keyPressEvent(QKeyEvent
* event
)
243 // See DolphinTreeView::currentChanged() for more information about m_updateCurrentIndex
244 m_updateCurrentIndex
= (event
->modifiers() == Qt::NoModifier
);
245 QTreeView::keyPressEvent(event
);
248 void DolphinTreeView::keyReleaseEvent(QKeyEvent
* event
)
250 QTreeView::keyReleaseEvent(event
);
251 m_updateCurrentIndex
= false;
254 void DolphinTreeView::currentChanged(const QModelIndex
& current
, const QModelIndex
& previous
)
256 QTreeView::currentChanged(current
, previous
);
258 // Stay consistent with QListView: When changing the current index by key presses
259 // without modifiers, also change the selection.
260 if (m_updateCurrentIndex
) {
261 setCurrentIndex(current
);
265 QModelIndex
DolphinTreeView::indexAt(const QPoint
& point
) const
267 // The blank portion of the name column counts as empty space
268 const QModelIndex index
= QTreeView::indexAt(point
);
269 const bool isAboveEmptySpace
= !m_useDefaultIndexAt
&&
270 (index
.column() == KDirModel::Name
) &&
271 !visualRect(index
).contains(point
);
272 return isAboveEmptySpace
? QModelIndex() : index
;
275 void DolphinTreeView::setSelection(const QRect
& rect
, QItemSelectionModel::SelectionFlags command
)
277 // We must override setSelection() as Qt calls it internally and when this happens
278 // we must ensure that the default indexAt() is used.
280 m_useDefaultIndexAt
= true;
281 QTreeView::setSelection(rect
, command
);
282 m_useDefaultIndexAt
= false;
284 // Use our own elastic band selection algorithm
285 updateElasticBandSelection();
290 void DolphinTreeView::scrollTo(const QModelIndex
& index
, ScrollHint hint
)
292 if (!m_ignoreScrollTo
) {
293 QTreeView::scrollTo(index
, hint
);
297 void DolphinTreeView::updateElasticBandSelection()
303 // Ensure the elastic band itself is up-to-date, in
304 // case we are being called due to e.g. a drag event.
307 // Clip horizontally to the name column, as some filenames will be
308 // longer than the column. We don't clip vertically as origin
309 // may be above or below the current viewport area.
310 const int nameColumnX
= header()->sectionPosition(DolphinModel::Name
);
311 const int nameColumnWidth
= header()->sectionSize(DolphinModel::Name
);
312 QRect selRect
= elasticBandRect().normalized();
313 QRect
nameColumnArea(nameColumnX
, selRect
.y(), nameColumnWidth
, selRect
.height());
314 selRect
= nameColumnArea
.intersect(selRect
).normalized();
315 // Get the last elastic band rectangle, expressed in viewpoint coordinates.
316 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
317 QRect oldSelRect
= QRect(m_band
.lastSelectionOrigin
- scrollPos
, m_band
.lastSelectionDestination
- scrollPos
).normalized();
319 if (selRect
.isNull()) {
320 selectionModel()->select(m_band
.originalSelection
, QItemSelectionModel::ClearAndSelect
);
321 m_band
.ignoreOldInfo
= true;
325 if (!m_band
.ignoreOldInfo
) {
326 // Do some quick checks to see if we can rule out the need to
327 // update the selection.
328 Q_ASSERT(uniformRowHeights());
329 QModelIndex dummyIndex
= model()->index(0, 0);
330 if (!dummyIndex
.isValid()) {
331 // No items in the model presumably.
335 // If the elastic band does not cover the same rows as before, we'll
336 // need to re-check, and also invalidate the old item distances.
337 const int rowHeight
= QTreeView::rowHeight(dummyIndex
);
338 const bool coveringSameRows
=
339 (selRect
.top() / rowHeight
== oldSelRect
.top() / rowHeight
) &&
340 (selRect
.bottom() / rowHeight
== oldSelRect
.bottom() / rowHeight
);
341 if (coveringSameRows
) {
342 // Covering the same rows, but have we moved far enough horizontally
343 // that we might have (de)selected some other items?
344 const bool itemSelectionChanged
=
345 ((selRect
.left() > oldSelRect
.left()) &&
346 (selRect
.left() > m_band
.insideNearestLeftEdge
)) ||
347 ((selRect
.left() < oldSelRect
.left()) &&
348 (selRect
.left() <= m_band
.outsideNearestLeftEdge
)) ||
349 ((selRect
.right() < oldSelRect
.right()) &&
350 (selRect
.left() >= m_band
.insideNearestRightEdge
)) ||
351 ((selRect
.right() > oldSelRect
.right()) &&
352 (selRect
.right() >= m_band
.outsideNearestRightEdge
));
354 if (!itemSelectionChanged
) {
359 // This is the only piece of optimization data that needs to be explicitly
361 m_band
.lastSelectionOrigin
= QPoint();
362 m_band
.lastSelectionDestination
= QPoint();
363 oldSelRect
= selRect
;
366 // Do the selection from scratch. Force a update of the horizontal distances info.
367 m_band
.insideNearestLeftEdge
= nameColumnX
+ nameColumnWidth
+ 1;
368 m_band
.insideNearestRightEdge
= nameColumnX
- 1;
369 m_band
.outsideNearestLeftEdge
= nameColumnX
- 1;
370 m_band
.outsideNearestRightEdge
= nameColumnX
+ nameColumnWidth
+ 1;
372 // Include the old selection rect as well, so we can deselect
373 // items that were inside it but not in the new selRect.
374 const QRect boundingRect
= selRect
.united(oldSelRect
).normalized();
375 if (boundingRect
.isNull()) {
379 // Get the index of the item in this row in the name column.
380 // TODO - would this still work if the columns could be re-ordered?
381 QModelIndex startIndex
= QTreeView::indexAt(boundingRect
.topLeft());
382 if (startIndex
.parent().isValid()) {
383 startIndex
= startIndex
.parent().child(startIndex
.row(), KDirModel::Name
);
385 startIndex
= model()->index(startIndex
.row(), KDirModel::Name
);
387 if (!startIndex
.isValid()) {
388 selectionModel()->select(m_band
.originalSelection
, QItemSelectionModel::ClearAndSelect
);
389 m_band
.ignoreOldInfo
= true;
393 // Go through all indexes between the top and bottom of boundingRect, and
394 // update the selection.
395 const int verticalCutoff
= boundingRect
.bottom();
396 QModelIndex currIndex
= startIndex
;
397 QModelIndex lastIndex
;
398 bool allItemsInBoundDone
= false;
400 // Calling selectionModel()->select(...) for each item that needs to be
401 // toggled is slow as each call emits selectionChanged(...) so store them
402 // and do the selection toggle in one batch.
403 QItemSelection itemsToToggle
;
404 // QItemSelection's deal with continuous ranges of indexes better than
405 // single indexes, so try to portion items that need to be toggled into ranges.
406 bool formingToggleIndexRange
= false;
407 QModelIndex toggleIndexRangeBegin
= QModelIndex();
410 QRect currIndexRect
= visualRect(currIndex
);
412 // Update some optimization info as we go.
413 const int cr
= currIndexRect
.right();
414 const int cl
= currIndexRect
.left();
415 const int sl
= selRect
.left();
416 const int sr
= selRect
.right();
417 // "The right edge of the name is outside of the rect but nearer than m_outsideNearestLeft", etc
418 if ((cr
< sl
&& cr
> m_band
.outsideNearestLeftEdge
)) {
419 m_band
.outsideNearestLeftEdge
= cr
;
421 if ((cl
> sr
&& cl
< m_band
.outsideNearestRightEdge
)) {
422 m_band
.outsideNearestRightEdge
= cl
;
424 if ((cl
>= sl
&& cl
<= sr
&& cl
> m_band
.insideNearestRightEdge
)) {
425 m_band
.insideNearestRightEdge
= cl
;
427 if ((cr
>= sl
&& cr
<= sr
&& cr
< m_band
.insideNearestLeftEdge
)) {
428 m_band
.insideNearestLeftEdge
= cr
;
431 bool currentlySelected
= selectionModel()->isSelected(currIndex
);
432 bool originallySelected
= m_band
.originalSelection
.contains(currIndex
);
433 bool intersectsSelectedRect
= currIndexRect
.intersects(selRect
);
434 bool shouldBeSelected
= (intersectsSelectedRect
&& !originallySelected
) || (!intersectsSelectedRect
&& originallySelected
);
435 bool needToToggleItem
= (currentlySelected
&& !shouldBeSelected
) || (!currentlySelected
&& shouldBeSelected
);
436 if (needToToggleItem
&& !formingToggleIndexRange
) {
437 toggleIndexRangeBegin
= currIndex
;
438 formingToggleIndexRange
= true;
441 // NOTE: indexBelow actually walks up and down expanded trees for us.
442 QModelIndex nextIndex
= indexBelow(currIndex
);
443 allItemsInBoundDone
= !nextIndex
.isValid() || currIndexRect
.top() > verticalCutoff
;
445 const bool commitToggleIndexRange
= formingToggleIndexRange
&&
446 (!needToToggleItem
||
447 allItemsInBoundDone
||
448 currIndex
.parent() != toggleIndexRangeBegin
.parent());
449 if (commitToggleIndexRange
) {
450 formingToggleIndexRange
= false;
451 // If this is the last item in the bounds and it is also the beginning of a range,
452 // don't toggle lastIndex - it will already have been dealt with.
453 if (!allItemsInBoundDone
|| toggleIndexRangeBegin
!= currIndex
) {
454 itemsToToggle
.select(toggleIndexRangeBegin
, lastIndex
);
456 // Need to start a new range immediately with currIndex?
457 if (needToToggleItem
) {
458 toggleIndexRangeBegin
= currIndex
;
459 formingToggleIndexRange
= true;
461 if (allItemsInBoundDone
&& needToToggleItem
) {
462 // Toggle the very last item in the bounds.
463 itemsToToggle
.select(currIndex
, currIndex
);
468 lastIndex
= currIndex
;
469 currIndex
= nextIndex
;
470 } while (!allItemsInBoundDone
);
473 selectionModel()->select(itemsToToggle
, QItemSelectionModel::Toggle
);
475 m_band
.lastSelectionOrigin
= m_band
.origin
;
476 m_band
.lastSelectionDestination
= m_band
.destination
;
477 m_band
.ignoreOldInfo
= false;
480 void DolphinTreeView::updateElasticBand()
483 QRect
dirtyRegion(elasticBandRect());
484 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
485 m_band
.destination
= viewport()->mapFromGlobal(QCursor::pos()) + scrollPos
;
486 // Going above the (logical) top-left of the view causes complications during selection;
487 // we may as well prevent it.
488 if (m_band
.destination
.y() < 0) {
489 m_band
.destination
.setY(0);
491 if (m_band
.destination
.x() < 0) {
492 m_band
.destination
.setX(0);
494 dirtyRegion
= dirtyRegion
.united(elasticBandRect());
495 setDirtyRegion(dirtyRegion
);
499 QRect
DolphinTreeView::elasticBandRect() const
501 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
503 const QPoint topLeft
= m_band
.origin
- scrollPos
;
504 const QPoint bottomRight
= m_band
.destination
- scrollPos
;
505 return QRect(topLeft
, bottomRight
).normalized();
508 bool DolphinTreeView::isAboveExpandingToggle(const QPoint
& pos
) const
510 // QTreeView offers no public API to get the information whether an index has an
511 // expanding toggle and what boundaries the toggle has. The following approach
512 // also assumes a toggle for file items.
513 if (itemsExpandable()) {
514 const QModelIndex index
= QTreeView::indexAt(pos
);
515 if (index
.isValid() && (index
.column() == KDirModel::Name
)) {
516 QRect rect
= visualRect(index
);
517 const int toggleSize
= rect
.height();
518 if (isRightToLeft()) {
519 rect
.moveRight(rect
.right());
521 rect
.moveLeft(rect
.x() - toggleSize
);
523 rect
.setWidth(toggleSize
);
528 rect
= style()->subElementRect(QStyle::SE_TreeViewDisclosureItem
, &opt
, this);
530 return rect
.contains(pos
);
536 DolphinTreeView::ElasticBand::ElasticBand() :
540 lastSelectionOrigin(),
541 lastSelectionDestination(),
543 outsideNearestLeftEdge(0),
544 outsideNearestRightEdge(0),
545 insideNearestLeftEdge(0),
546 insideNearestRightEdge(0)
550 #include "dolphintreeview.moc"