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
) :
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.
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 // If the Control modifier is pressed, a multiple selection
243 // is done and DolphinDetailsView::currentChanged() may not
244 // not change the selection in a custom way.
245 m_keyPressed
= !(event
->modifiers() & Qt::ControlModifier
);
247 QTreeView::keyPressEvent(event
);
250 void DolphinTreeView::keyReleaseEvent(QKeyEvent
* event
)
252 QTreeView::keyReleaseEvent(event
);
253 m_keyPressed
= false;
256 void DolphinTreeView::currentChanged(const QModelIndex
& current
, const QModelIndex
& previous
)
258 QTreeView::currentChanged(current
, previous
);
260 // Stay consistent with QListView: When changing the current index by key presses,
261 // also change the selection.
263 setCurrentIndex(current
);
267 QModelIndex
DolphinTreeView::indexAt(const QPoint
& point
) const
269 // The blank portion of the name column counts as empty space
270 const QModelIndex index
= QTreeView::indexAt(point
);
271 const bool isAboveEmptySpace
= !m_useDefaultIndexAt
&&
272 (index
.column() == KDirModel::Name
) &&
273 !visualRect(index
).contains(point
);
274 return isAboveEmptySpace
? QModelIndex() : index
;
277 void DolphinTreeView::setSelection(const QRect
& rect
, QItemSelectionModel::SelectionFlags command
)
279 // We must override setSelection() as Qt calls it internally and when this happens
280 // we must ensure that the default indexAt() is used.
282 m_useDefaultIndexAt
= true;
283 QTreeView::setSelection(rect
, command
);
284 m_useDefaultIndexAt
= false;
286 // Use our own elastic band selection algorithm
287 updateElasticBandSelection();
292 void DolphinTreeView::scrollTo(const QModelIndex
& index
, ScrollHint hint
)
294 if (!m_ignoreScrollTo
) {
295 QTreeView::scrollTo(index
, hint
);
299 void DolphinTreeView::updateElasticBandSelection()
305 // Ensure the elastic band itself is up-to-date, in
306 // case we are being called due to e.g. a drag event.
309 // Clip horizontally to the name column, as some filenames will be
310 // longer than the column. We don't clip vertically as origin
311 // may be above or below the current viewport area.
312 const int nameColumnX
= header()->sectionPosition(DolphinModel::Name
);
313 const int nameColumnWidth
= header()->sectionSize(DolphinModel::Name
);
314 QRect selRect
= elasticBandRect().normalized();
315 QRect
nameColumnArea(nameColumnX
, selRect
.y(), nameColumnWidth
, selRect
.height());
316 selRect
= nameColumnArea
.intersect(selRect
).normalized();
317 // Get the last elastic band rectangle, expressed in viewpoint coordinates.
318 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
319 QRect oldSelRect
= QRect(m_band
.lastSelectionOrigin
- scrollPos
, m_band
.lastSelectionDestination
- scrollPos
).normalized();
321 if (selRect
.isNull()) {
322 selectionModel()->select(m_band
.originalSelection
, QItemSelectionModel::ClearAndSelect
);
323 m_band
.ignoreOldInfo
= true;
327 if (!m_band
.ignoreOldInfo
) {
328 // Do some quick checks to see if we can rule out the need to
329 // update the selection.
330 Q_ASSERT(uniformRowHeights());
331 QModelIndex dummyIndex
= model()->index(0, 0);
332 if (!dummyIndex
.isValid()) {
333 // No items in the model presumably.
337 // If the elastic band does not cover the same rows as before, we'll
338 // need to re-check, and also invalidate the old item distances.
339 const int rowHeight
= QTreeView::rowHeight(dummyIndex
);
340 const bool coveringSameRows
=
341 (selRect
.top() / rowHeight
== oldSelRect
.top() / rowHeight
) &&
342 (selRect
.bottom() / rowHeight
== oldSelRect
.bottom() / rowHeight
);
343 if (coveringSameRows
) {
344 // Covering the same rows, but have we moved far enough horizontally
345 // that we might have (de)selected some other items?
346 const bool itemSelectionChanged
=
347 ((selRect
.left() > oldSelRect
.left()) &&
348 (selRect
.left() > m_band
.insideNearestLeftEdge
)) ||
349 ((selRect
.left() < oldSelRect
.left()) &&
350 (selRect
.left() <= m_band
.outsideNearestLeftEdge
)) ||
351 ((selRect
.right() < oldSelRect
.right()) &&
352 (selRect
.left() >= m_band
.insideNearestRightEdge
)) ||
353 ((selRect
.right() > oldSelRect
.right()) &&
354 (selRect
.right() >= m_band
.outsideNearestRightEdge
));
356 if (!itemSelectionChanged
) {
361 // This is the only piece of optimization data that needs to be explicitly
363 m_band
.lastSelectionOrigin
= QPoint();
364 m_band
.lastSelectionDestination
= QPoint();
365 oldSelRect
= selRect
;
368 // Do the selection from scratch. Force a update of the horizontal distances info.
369 m_band
.insideNearestLeftEdge
= nameColumnX
+ nameColumnWidth
+ 1;
370 m_band
.insideNearestRightEdge
= nameColumnX
- 1;
371 m_band
.outsideNearestLeftEdge
= nameColumnX
- 1;
372 m_band
.outsideNearestRightEdge
= nameColumnX
+ nameColumnWidth
+ 1;
374 // Include the old selection rect as well, so we can deselect
375 // items that were inside it but not in the new selRect.
376 const QRect boundingRect
= selRect
.united(oldSelRect
).normalized();
377 if (boundingRect
.isNull()) {
381 // Get the index of the item in this row in the name column.
382 // TODO - would this still work if the columns could be re-ordered?
383 QModelIndex startIndex
= QTreeView::indexAt(boundingRect
.topLeft());
384 if (startIndex
.parent().isValid()) {
385 startIndex
= startIndex
.parent().child(startIndex
.row(), KDirModel::Name
);
387 startIndex
= model()->index(startIndex
.row(), KDirModel::Name
);
389 if (!startIndex
.isValid()) {
390 selectionModel()->select(m_band
.originalSelection
, QItemSelectionModel::ClearAndSelect
);
391 m_band
.ignoreOldInfo
= true;
395 // Go through all indexes between the top and bottom of boundingRect, and
396 // update the selection.
397 const int verticalCutoff
= boundingRect
.bottom();
398 QModelIndex currIndex
= startIndex
;
399 QModelIndex lastIndex
;
400 bool allItemsInBoundDone
= false;
402 // Calling selectionModel()->select(...) for each item that needs to be
403 // toggled is slow as each call emits selectionChanged(...) so store them
404 // and do the selection toggle in one batch.
405 QItemSelection itemsToToggle
;
406 // QItemSelection's deal with continuous ranges of indexes better than
407 // single indexes, so try to portion items that need to be toggled into ranges.
408 bool formingToggleIndexRange
= false;
409 QModelIndex toggleIndexRangeBegin
= QModelIndex();
412 QRect currIndexRect
= visualRect(currIndex
);
414 // Update some optimization info as we go.
415 const int cr
= currIndexRect
.right();
416 const int cl
= currIndexRect
.left();
417 const int sl
= selRect
.left();
418 const int sr
= selRect
.right();
419 // "The right edge of the name is outside of the rect but nearer than m_outsideNearestLeft", etc
420 if ((cr
< sl
&& cr
> m_band
.outsideNearestLeftEdge
)) {
421 m_band
.outsideNearestLeftEdge
= cr
;
423 if ((cl
> sr
&& cl
< m_band
.outsideNearestRightEdge
)) {
424 m_band
.outsideNearestRightEdge
= cl
;
426 if ((cl
>= sl
&& cl
<= sr
&& cl
> m_band
.insideNearestRightEdge
)) {
427 m_band
.insideNearestRightEdge
= cl
;
429 if ((cr
>= sl
&& cr
<= sr
&& cr
< m_band
.insideNearestLeftEdge
)) {
430 m_band
.insideNearestLeftEdge
= cr
;
433 bool currentlySelected
= selectionModel()->isSelected(currIndex
);
434 bool originallySelected
= m_band
.originalSelection
.contains(currIndex
);
435 bool intersectsSelectedRect
= currIndexRect
.intersects(selRect
);
436 bool shouldBeSelected
= (intersectsSelectedRect
&& !originallySelected
) || (!intersectsSelectedRect
&& originallySelected
);
437 bool needToToggleItem
= (currentlySelected
&& !shouldBeSelected
) || (!currentlySelected
&& shouldBeSelected
);
438 if (needToToggleItem
&& !formingToggleIndexRange
) {
439 toggleIndexRangeBegin
= currIndex
;
440 formingToggleIndexRange
= true;
443 // NOTE: indexBelow actually walks up and down expanded trees for us.
444 QModelIndex nextIndex
= indexBelow(currIndex
);
445 allItemsInBoundDone
= !nextIndex
.isValid() || currIndexRect
.top() > verticalCutoff
;
447 const bool commitToggleIndexRange
= formingToggleIndexRange
&&
448 (!needToToggleItem
||
449 allItemsInBoundDone
||
450 currIndex
.parent() != toggleIndexRangeBegin
.parent());
451 if (commitToggleIndexRange
) {
452 formingToggleIndexRange
= false;
453 // If this is the last item in the bounds and it is also the beginning of a range,
454 // don't toggle lastIndex - it will already have been dealt with.
455 if (!allItemsInBoundDone
|| toggleIndexRangeBegin
!= currIndex
) {
456 itemsToToggle
.select(toggleIndexRangeBegin
, lastIndex
);
458 // Need to start a new range immediately with currIndex?
459 if (needToToggleItem
) {
460 toggleIndexRangeBegin
= currIndex
;
461 formingToggleIndexRange
= true;
463 if (allItemsInBoundDone
&& needToToggleItem
) {
464 // Toggle the very last item in the bounds.
465 itemsToToggle
.select(currIndex
, currIndex
);
470 lastIndex
= currIndex
;
471 currIndex
= nextIndex
;
472 } while (!allItemsInBoundDone
);
475 selectionModel()->select(itemsToToggle
, QItemSelectionModel::Toggle
);
477 m_band
.lastSelectionOrigin
= m_band
.origin
;
478 m_band
.lastSelectionDestination
= m_band
.destination
;
479 m_band
.ignoreOldInfo
= false;
482 void DolphinTreeView::updateElasticBand()
485 QRect
dirtyRegion(elasticBandRect());
486 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
487 m_band
.destination
= viewport()->mapFromGlobal(QCursor::pos()) + scrollPos
;
488 // Going above the (logical) top-left of the view causes complications during selection;
489 // we may as well prevent it.
490 if (m_band
.destination
.y() < 0) {
491 m_band
.destination
.setY(0);
493 if (m_band
.destination
.x() < 0) {
494 m_band
.destination
.setX(0);
496 dirtyRegion
= dirtyRegion
.united(elasticBandRect());
497 setDirtyRegion(dirtyRegion
);
501 QRect
DolphinTreeView::elasticBandRect() const
503 const QPoint
scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
505 const QPoint topLeft
= m_band
.origin
- scrollPos
;
506 const QPoint bottomRight
= m_band
.destination
- scrollPos
;
507 return QRect(topLeft
, bottomRight
).normalized();
510 bool DolphinTreeView::isAboveExpandingToggle(const QPoint
& pos
) const
512 // QTreeView offers no public API to get the information whether an index has an
513 // expanding toggle and what boundaries the toggle has. The following approach
514 // also assumes a toggle for file items.
515 if (itemsExpandable()) {
516 const QModelIndex index
= QTreeView::indexAt(pos
);
517 if (index
.isValid() && (index
.column() == KDirModel::Name
)) {
518 QRect rect
= visualRect(index
);
519 const int toggleSize
= rect
.height();
520 if (isRightToLeft()) {
521 rect
.moveRight(rect
.right());
523 rect
.moveLeft(rect
.x() - toggleSize
);
525 rect
.setWidth(toggleSize
);
530 rect
= style()->subElementRect(QStyle::SE_TreeViewDisclosureItem
, &opt
, this);
532 return rect
.contains(pos
);
538 DolphinTreeView::ElasticBand::ElasticBand() :
542 lastSelectionOrigin(),
543 lastSelectionDestination(),
545 outsideNearestLeftEdge(0),
546 outsideNearestRightEdge(0),
547 insideNearestLeftEdge(0),
548 insideNearestRightEdge(0)
552 #include "dolphintreeview.moc"