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),
43 DolphinTreeView::~DolphinTreeView()
47 QRegion
DolphinTreeView::visualRegionForSelection(const QItemSelection
& selection
) const
49 // We have to make sure that the visualRect of each model index is inside the region.
50 // QTreeView::visualRegionForSelection does not do it right because it assumes implicitly
51 // that all visualRects have the same width, which is in general not the case here.
52 QRegion selectionRegion
;
53 const QModelIndexList indexes
= selection
.indexes();
55 foreach(const QModelIndex
& index
, indexes
) {
56 selectionRegion
+= visualRect(index
);
59 return selectionRegion
;
62 bool DolphinTreeView::acceptsDrop(const QModelIndex
& index
) const
68 bool DolphinTreeView::event(QEvent
* event
)
70 switch (event
->type()) {
72 m_useDefaultIndexAt
= false;
74 case QEvent::FocusOut
:
75 // If a key-press triggers an action that e. g. opens a dialog, the
76 // widget gets no key-release event. Assure that the pressed state
77 // is reset to prevent accidently setting the current index during a selection.
78 m_updateCurrentIndex
= false;
83 return QTreeView::event(event
);
86 void DolphinTreeView::mousePressEvent(QMouseEvent
* event
)
88 const QModelIndex current
= currentIndex();
89 QTreeView::mousePressEvent(event
);
91 m_expandingTogglePressed
= isAboveExpandingToggle(event
->pos());
93 const QModelIndex index
= indexAt(event
->pos());
94 const bool updateState
= index
.isValid() &&
95 (index
.column() == DolphinModel::Name
) &&
96 (event
->button() == Qt::LeftButton
);
98 setState(QAbstractItemView::DraggingState
);
101 if (!index
.isValid() || (index
.column() != DolphinModel::Name
)) {
102 const Qt::KeyboardModifiers mod
= QApplication::keyboardModifiers();
103 if (!m_expandingTogglePressed
&& !(mod
& Qt::ShiftModifier
) && !(mod
& Qt::ControlModifier
)) {
107 // Restore the current index, other columns are handled as viewport area.
108 // setCurrentIndex(...) implicitly calls scrollTo(...), which we want to ignore.
109 m_ignoreScrollTo
= true;
110 selectionModel()->setCurrentIndex(current
, QItemSelectionModel::Current
);
111 m_ignoreScrollTo
= false;
113 if ((event
->button() == Qt::LeftButton
) && !m_expandingTogglePressed
) {
114 // Inform Qt about what we are doing - otherwise it starts dragging items around!
115 setState(DragSelectingState
);
117 // Incremental update data will not be useful - start from scratch.
118 m_band
.ignoreOldInfo
= true;
119 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
120 m_band
.origin
= event
->pos() + scrollPos
;
121 m_band
.destination
= m_band
.origin
;
122 m_band
.originalSelection
= selectionModel()->selection();
127 void DolphinTreeView::mouseMoveEvent(QMouseEvent
* event
)
129 if (m_expandingTogglePressed
) {
130 // Per default QTreeView starts either a selection or a drag operation when dragging
131 // the expanding toggle button (Qt-issue - see TODO comment in DolphinIconsView::mousePressEvent()).
132 // Turn off this behavior in Dolphin to stay predictable:
133 setState(QAbstractItemView::NoState
);
138 const QPoint mousePos
= event
->pos();
139 const QModelIndex index
= indexAt(mousePos
);
140 if (!index
.isValid()) {
141 // The destination of the selection rectangle is above the viewport. In this
142 // case QTreeView does no selection at all, which is not the wanted behavior
143 // in Dolphin. Select all items within the elastic band rectangle.
144 updateElasticBandSelection();
147 // TODO: Enable QTreeView::mouseMoveEvent(event) again, as soon
148 // as the Qt-issue #199631 has been fixed.
149 // QTreeView::mouseMoveEvent(event);
150 QAbstractItemView::mouseMoveEvent(event
);
153 // TODO: Enable QTreeView::mouseMoveEvent(event) again, as soon
154 // as the Qt-issue #199631 has been fixed.
155 // QTreeView::mouseMoveEvent(event);
156 QAbstractItemView::mouseMoveEvent(event
);
160 void DolphinTreeView::mouseReleaseEvent(QMouseEvent
* event
)
162 if (!m_expandingTogglePressed
) {
163 const QModelIndex index
= indexAt(event
->pos());
164 if (index
.isValid() && (index
.column() == DolphinModel::Name
)) {
165 QTreeView::mouseReleaseEvent(event
);
167 // don't change the current index if the cursor is released
168 // above any other column than the name column, as the other
169 // columns act as viewport
170 const QModelIndex current
= currentIndex();
171 QTreeView::mouseReleaseEvent(event
);
172 selectionModel()->setCurrentIndex(current
, QItemSelectionModel::Current
);
175 m_expandingTogglePressed
= false;
184 void DolphinTreeView::startDrag(Qt::DropActions supportedActions
)
186 Q_UNUSED(supportedActions
);
190 void DolphinTreeView::dragEnterEvent(QDragEnterEvent
* event
)
199 void DolphinTreeView::dragMoveEvent(QDragMoveEvent
* event
)
201 QTreeView::dragMoveEvent(event
);
203 setDirtyRegion(m_dropRect
);
205 const QModelIndex index
= indexAt(event
->pos());
206 if (acceptsDrop(index
)) {
207 m_dropRect
= visualRect(index
);
209 m_dropRect
.setSize(QSize()); // set invalid
211 setDirtyRegion(m_dropRect
);
214 void DolphinTreeView::dragLeaveEvent(QDragLeaveEvent
* event
)
216 QTreeView::dragLeaveEvent(event
);
217 setDirtyRegion(m_dropRect
);
220 void DolphinTreeView::paintEvent(QPaintEvent
* event
)
222 QTreeView::paintEvent(event
);
224 // The following code has been taken from QListView
225 // and adapted to DolphinDetailsView.
226 // (C) 1992-2007 Trolltech ASA
227 QStyleOptionRubberBand opt
;
229 opt
.shape
= QRubberBand::Rectangle
;
231 opt
.rect
= elasticBandRect();
233 QPainter
painter(viewport());
235 style()->drawControl(QStyle::CE_RubberBand
, &opt
, &painter
);
240 void DolphinTreeView::keyPressEvent(QKeyEvent
* event
)
242 // See DolphinTreeView::currentChanged() for more information about m_updateCurrentIndex
243 m_updateCurrentIndex
= (event
->modifiers() == Qt::NoModifier
);
244 QTreeView::keyPressEvent(event
);
247 void DolphinTreeView::keyReleaseEvent(QKeyEvent
* event
)
249 QTreeView::keyReleaseEvent(event
);
250 m_updateCurrentIndex
= false;
253 void DolphinTreeView::currentChanged(const QModelIndex
& current
, const QModelIndex
& previous
)
255 QTreeView::currentChanged(current
, previous
);
257 // Stay consistent with QListView: When changing the current index by key presses
258 // without modifiers, also change the selection.
259 if (m_updateCurrentIndex
) {
260 setCurrentIndex(current
);
264 QModelIndex
DolphinTreeView::indexAt(const QPoint
& point
) const
266 // The blank portion of the name column counts as empty space
267 const QModelIndex index
= QTreeView::indexAt(point
);
268 const bool isAboveEmptySpace
= !m_useDefaultIndexAt
&&
269 (index
.column() == KDirModel::Name
) &&
270 !visualRect(index
).contains(point
);
271 return isAboveEmptySpace
? QModelIndex() : index
;
274 void DolphinTreeView::setSelection(const QRect
& rect
, QItemSelectionModel::SelectionFlags command
)
276 // We must override setSelection() as Qt calls it internally and when this happens
277 // we must ensure that the default indexAt() is used.
279 m_useDefaultIndexAt
= true;
280 QTreeView::setSelection(rect
, command
);
281 m_useDefaultIndexAt
= false;
283 // Use our own elastic band selection algorithm
284 updateElasticBandSelection();
289 void DolphinTreeView::scrollTo(const QModelIndex
& index
, ScrollHint hint
)
291 if (!m_ignoreScrollTo
) {
292 QTreeView::scrollTo(index
, hint
);
296 void DolphinTreeView::updateElasticBandSelection()
302 // Ensure the elastic band itself is up-to-date, in
303 // case we are being called due to e.g. a drag event.
306 // Clip horizontally to the name column, as some filenames will be
307 // longer than the column. We don't clip vertically as origin
308 // may be above or below the current viewport area.
309 const int nameColumnX
= header()->sectionPosition(DolphinModel::Name
);
310 const int nameColumnWidth
= header()->sectionSize(DolphinModel::Name
);
311 QRect selRect
= elasticBandRect().normalized();
312 QRect
nameColumnArea(nameColumnX
, selRect
.y(), nameColumnWidth
, selRect
.height());
313 selRect
= nameColumnArea
.intersect(selRect
).normalized();
314 // Get the last elastic band rectangle, expressed in viewpoint coordinates.
315 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
316 QRect oldSelRect
= QRect(m_band
.lastSelectionOrigin
- scrollPos
, m_band
.lastSelectionDestination
- scrollPos
).normalized();
318 if (selRect
.isNull()) {
319 selectionModel()->select(m_band
.originalSelection
, QItemSelectionModel::ClearAndSelect
);
320 m_band
.ignoreOldInfo
= true;
324 if (!m_band
.ignoreOldInfo
) {
325 // Do some quick checks to see if we can rule out the need to
326 // update the selection.
327 Q_ASSERT(uniformRowHeights());
328 QModelIndex dummyIndex
= model()->index(0, 0);
329 if (!dummyIndex
.isValid()) {
330 // No items in the model presumably.
334 // If the elastic band does not cover the same rows as before, we'll
335 // need to re-check, and also invalidate the old item distances.
336 const int rowHeight
= QTreeView::rowHeight(dummyIndex
);
337 const bool coveringSameRows
=
338 (selRect
.top() / rowHeight
== oldSelRect
.top() / rowHeight
) &&
339 (selRect
.bottom() / rowHeight
== oldSelRect
.bottom() / rowHeight
);
340 if (coveringSameRows
) {
341 // Covering the same rows, but have we moved far enough horizontally
342 // that we might have (de)selected some other items?
343 const bool itemSelectionChanged
=
344 ((selRect
.left() > oldSelRect
.left()) &&
345 (selRect
.left() > m_band
.insideNearestLeftEdge
)) ||
346 ((selRect
.left() < oldSelRect
.left()) &&
347 (selRect
.left() <= m_band
.outsideNearestLeftEdge
)) ||
348 ((selRect
.right() < oldSelRect
.right()) &&
349 (selRect
.left() >= m_band
.insideNearestRightEdge
)) ||
350 ((selRect
.right() > oldSelRect
.right()) &&
351 (selRect
.right() >= m_band
.outsideNearestRightEdge
));
353 if (!itemSelectionChanged
) {
358 // This is the only piece of optimization data that needs to be explicitly
360 m_band
.lastSelectionOrigin
= QPoint();
361 m_band
.lastSelectionDestination
= QPoint();
362 oldSelRect
= selRect
;
365 // Do the selection from scratch. Force a update of the horizontal distances info.
366 m_band
.insideNearestLeftEdge
= nameColumnX
+ nameColumnWidth
+ 1;
367 m_band
.insideNearestRightEdge
= nameColumnX
- 1;
368 m_band
.outsideNearestLeftEdge
= nameColumnX
- 1;
369 m_band
.outsideNearestRightEdge
= nameColumnX
+ nameColumnWidth
+ 1;
371 // Include the old selection rect as well, so we can deselect
372 // items that were inside it but not in the new selRect.
373 const QRect boundingRect
= selRect
.united(oldSelRect
).normalized();
374 if (boundingRect
.isNull()) {
378 // Get the index of the item in this row in the name column.
379 // TODO - would this still work if the columns could be re-ordered?
380 QModelIndex startIndex
= QTreeView::indexAt(boundingRect
.topLeft());
381 if (startIndex
.parent().isValid()) {
382 startIndex
= startIndex
.parent().child(startIndex
.row(), KDirModel::Name
);
384 startIndex
= model()->index(startIndex
.row(), KDirModel::Name
);
386 if (!startIndex
.isValid()) {
387 selectionModel()->select(m_band
.originalSelection
, QItemSelectionModel::ClearAndSelect
);
388 m_band
.ignoreOldInfo
= true;
392 // Go through all indexes between the top and bottom of boundingRect, and
393 // update the selection.
394 const int verticalCutoff
= boundingRect
.bottom();
395 QModelIndex currIndex
= startIndex
;
396 QModelIndex lastIndex
;
397 bool allItemsInBoundDone
= false;
399 // Calling selectionModel()->select(...) for each item that needs to be
400 // toggled is slow as each call emits selectionChanged(...) so store them
401 // and do the selection toggle in one batch.
402 QItemSelection itemsToToggle
;
403 // QItemSelection's deal with continuous ranges of indexes better than
404 // single indexes, so try to portion items that need to be toggled into ranges.
405 bool formingToggleIndexRange
= false;
406 QModelIndex toggleIndexRangeBegin
= QModelIndex();
409 QRect currIndexRect
= visualRect(currIndex
);
411 // Update some optimization info as we go.
412 const int cr
= currIndexRect
.right();
413 const int cl
= currIndexRect
.left();
414 const int sl
= selRect
.left();
415 const int sr
= selRect
.right();
416 // "The right edge of the name is outside of the rect but nearer than m_outsideNearestLeft", etc
417 if ((cr
< sl
&& cr
> m_band
.outsideNearestLeftEdge
)) {
418 m_band
.outsideNearestLeftEdge
= cr
;
420 if ((cl
> sr
&& cl
< m_band
.outsideNearestRightEdge
)) {
421 m_band
.outsideNearestRightEdge
= cl
;
423 if ((cl
>= sl
&& cl
<= sr
&& cl
> m_band
.insideNearestRightEdge
)) {
424 m_band
.insideNearestRightEdge
= cl
;
426 if ((cr
>= sl
&& cr
<= sr
&& cr
< m_band
.insideNearestLeftEdge
)) {
427 m_band
.insideNearestLeftEdge
= cr
;
430 bool currentlySelected
= selectionModel()->isSelected(currIndex
);
431 bool originallySelected
= m_band
.originalSelection
.contains(currIndex
);
432 bool intersectsSelectedRect
= currIndexRect
.intersects(selRect
);
433 bool shouldBeSelected
= (intersectsSelectedRect
&& !originallySelected
) || (!intersectsSelectedRect
&& originallySelected
);
434 bool needToToggleItem
= (currentlySelected
&& !shouldBeSelected
) || (!currentlySelected
&& shouldBeSelected
);
435 if (needToToggleItem
&& !formingToggleIndexRange
) {
436 toggleIndexRangeBegin
= currIndex
;
437 formingToggleIndexRange
= true;
440 // NOTE: indexBelow actually walks up and down expanded trees for us.
441 QModelIndex nextIndex
= indexBelow(currIndex
);
442 allItemsInBoundDone
= !nextIndex
.isValid() || currIndexRect
.top() > verticalCutoff
;
444 const bool commitToggleIndexRange
= formingToggleIndexRange
&&
445 (!needToToggleItem
||
446 allItemsInBoundDone
||
447 currIndex
.parent() != toggleIndexRangeBegin
.parent());
448 if (commitToggleIndexRange
) {
449 formingToggleIndexRange
= false;
450 // If this is the last item in the bounds and it is also the beginning of a range,
451 // don't toggle lastIndex - it will already have been dealt with.
452 if (!allItemsInBoundDone
|| toggleIndexRangeBegin
!= currIndex
) {
453 itemsToToggle
.select(toggleIndexRangeBegin
, lastIndex
);
455 // Need to start a new range immediately with currIndex?
456 if (needToToggleItem
) {
457 toggleIndexRangeBegin
= currIndex
;
458 formingToggleIndexRange
= true;
460 if (allItemsInBoundDone
&& needToToggleItem
) {
461 // Toggle the very last item in the bounds.
462 itemsToToggle
.select(currIndex
, currIndex
);
467 lastIndex
= currIndex
;
468 currIndex
= nextIndex
;
469 } while (!allItemsInBoundDone
);
472 selectionModel()->select(itemsToToggle
, QItemSelectionModel::Toggle
);
474 m_band
.lastSelectionOrigin
= m_band
.origin
;
475 m_band
.lastSelectionDestination
= m_band
.destination
;
476 m_band
.ignoreOldInfo
= false;
479 void DolphinTreeView::updateElasticBand()
482 QRect
dirtyRegion(elasticBandRect());
483 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
484 m_band
.destination
= viewport()->mapFromGlobal(QCursor::pos()) + scrollPos
;
485 // Going above the (logical) top-left of the view causes complications during selection;
486 // we may as well prevent it.
487 if (m_band
.destination
.y() < 0) {
488 m_band
.destination
.setY(0);
490 if (m_band
.destination
.x() < 0) {
491 m_band
.destination
.setX(0);
493 dirtyRegion
= dirtyRegion
.united(elasticBandRect());
494 setDirtyRegion(dirtyRegion
);
498 QRect
DolphinTreeView::elasticBandRect() const
500 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
502 const QPoint topLeft
= m_band
.origin
- scrollPos
;
503 const QPoint bottomRight
= m_band
.destination
- scrollPos
;
504 return QRect(topLeft
, bottomRight
).normalized();
507 bool DolphinTreeView::isAboveExpandingToggle(const QPoint
& pos
) const
509 // QTreeView offers no public API to get the information whether an index has an
510 // expanding toggle and what boundaries the toggle has. The following approach
511 // also assumes a toggle for file items.
512 if (itemsExpandable()) {
513 const QModelIndex index
= QTreeView::indexAt(pos
);
514 if (index
.isValid() && (index
.column() == KDirModel::Name
)) {
515 QRect rect
= visualRect(index
);
516 const int toggleSize
= rect
.height();
517 if (isRightToLeft()) {
518 rect
.moveRight(rect
.right());
520 rect
.moveLeft(rect
.x() - toggleSize
);
522 rect
.setWidth(toggleSize
);
527 rect
= style()->subElementRect(QStyle::SE_TreeViewDisclosureItem
, &opt
, this);
529 return rect
.contains(pos
);
535 DolphinTreeView::ElasticBand::ElasticBand() :
539 lastSelectionOrigin(),
540 lastSelectionDestination(),
542 outsideNearestLeftEdge(0),
543 outsideNearestRightEdge(0),
544 insideNearestLeftEdge(0),
545 insideNearestRightEdge(0)
549 #include "dolphintreeview.moc"