]> cloud.milkyroute.net Git - dolphin.git/blob - src/views/dolphintreeview.cpp
DolphinTreeViewTest: Add unit test for bug 220898 (rubberband
[dolphin.git] / src / views / dolphintreeview.cpp
1 /***************************************************************************
2 * Copyright (C) 2010 by Peter Penz <peter.penz19@gmail.com> *
3 * Copyright (C) 2008 by Simon St. James <kdedevel@etotheipiplusone.com> *
4 * *
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. *
9 * *
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. *
14 * *
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 ***************************************************************************/
20
21 #include "dolphintreeview.h"
22
23 #include "dolphinmodel.h"
24
25 #include <QApplication>
26 #include <QEvent>
27 #include <QHeaderView>
28 #include <QMouseEvent>
29 #include <QPainter>
30 #include <QScrollBar>
31
32 DolphinTreeView::DolphinTreeView(QWidget* parent) :
33 QTreeView(parent),
34 m_updateCurrentIndex(false),
35 m_expandingTogglePressed(false),
36 m_useDefaultIndexAt(true),
37 m_ignoreScrollTo(false),
38 m_dropRect(),
39 m_band()
40 {
41 setUniformRowHeights(true);
42 }
43
44 DolphinTreeView::~DolphinTreeView()
45 {
46 }
47
48 QRegion DolphinTreeView::visualRegionForSelection(const QItemSelection& selection) const
49 {
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();
55
56 foreach(const QModelIndex& index, indexes) {
57 selectionRegion += visualRect(index);
58 }
59
60 return selectionRegion;
61 }
62
63 bool DolphinTreeView::acceptsDrop(const QModelIndex& index) const
64 {
65 Q_UNUSED(index);
66 return false;
67 }
68
69 bool DolphinTreeView::event(QEvent* event)
70 {
71 switch (event->type()) {
72 case QEvent::Polish:
73 m_useDefaultIndexAt = false;
74 break;
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;
80 break;
81 default:
82 break;
83 }
84 return QTreeView::event(event);
85 }
86
87 void DolphinTreeView::mousePressEvent(QMouseEvent* event)
88 {
89 const QModelIndex current = currentIndex();
90 QTreeView::mousePressEvent(event);
91
92 m_expandingTogglePressed = isAboveExpandingToggle(event->pos());
93
94 const QModelIndex index = indexAt(event->pos());
95 const bool updateState = index.isValid() &&
96 (index.column() == DolphinModel::Name) &&
97 (event->button() == Qt::LeftButton);
98 if (updateState) {
99 setState(QAbstractItemView::DraggingState);
100 }
101
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)) {
105 clearSelection();
106 }
107
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;
113
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);
117 m_band.show = true;
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();
124 }
125 }
126 }
127
128 void DolphinTreeView::mouseMoveEvent(QMouseEvent* event)
129 {
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);
135 return;
136 }
137
138 if (m_band.show) {
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();
146 }
147
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);
152 updateElasticBand();
153 } else {
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);
158 }
159 }
160
161 void DolphinTreeView::mouseReleaseEvent(QMouseEvent* event)
162 {
163 if (!m_expandingTogglePressed) {
164 const QModelIndex index = indexAt(event->pos());
165 if (index.isValid() && (index.column() == DolphinModel::Name)) {
166 QTreeView::mouseReleaseEvent(event);
167 } else {
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);
174 }
175 }
176 m_expandingTogglePressed = false;
177
178 if (m_band.show) {
179 setState(NoState);
180 updateElasticBand();
181 m_band.show = false;
182 }
183 }
184
185 void DolphinTreeView::startDrag(Qt::DropActions supportedActions)
186 {
187 Q_UNUSED(supportedActions);
188 m_band.show = false;
189 }
190
191 void DolphinTreeView::dragEnterEvent(QDragEnterEvent* event)
192 {
193 Q_UNUSED(event);
194 if (m_band.show) {
195 updateElasticBand();
196 m_band.show = false;
197 }
198 }
199
200 void DolphinTreeView::dragMoveEvent(QDragMoveEvent* event)
201 {
202 QTreeView::dragMoveEvent(event);
203
204 setDirtyRegion(m_dropRect);
205
206 const QModelIndex index = indexAt(event->pos());
207 if (acceptsDrop(index)) {
208 m_dropRect = visualRect(index);
209 } else {
210 m_dropRect.setSize(QSize()); // set invalid
211 }
212 setDirtyRegion(m_dropRect);
213 }
214
215 void DolphinTreeView::dragLeaveEvent(QDragLeaveEvent* event)
216 {
217 QTreeView::dragLeaveEvent(event);
218 setDirtyRegion(m_dropRect);
219 }
220
221 void DolphinTreeView::paintEvent(QPaintEvent* event)
222 {
223 QTreeView::paintEvent(event);
224 if (m_band.show) {
225 // The following code has been taken from QListView
226 // and adapted to DolphinDetailsView.
227 // (C) 1992-2007 Trolltech ASA
228 QStyleOptionRubberBand opt;
229 opt.initFrom(this);
230 opt.shape = QRubberBand::Rectangle;
231 opt.opaque = false;
232 opt.rect = elasticBandRect();
233
234 QPainter painter(viewport());
235 painter.save();
236 style()->drawControl(QStyle::CE_RubberBand, &opt, &painter);
237 painter.restore();
238 }
239 }
240
241 void DolphinTreeView::keyPressEvent(QKeyEvent* event)
242 {
243 // See DolphinTreeView::currentChanged() for more information about m_updateCurrentIndex
244 m_updateCurrentIndex = (event->modifiers() == Qt::NoModifier);
245 QTreeView::keyPressEvent(event);
246 }
247
248 void DolphinTreeView::keyReleaseEvent(QKeyEvent* event)
249 {
250 QTreeView::keyReleaseEvent(event);
251 m_updateCurrentIndex = false;
252 }
253
254 void DolphinTreeView::currentChanged(const QModelIndex& current, const QModelIndex& previous)
255 {
256 QTreeView::currentChanged(current, previous);
257
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);
262 }
263 }
264
265 QModelIndex DolphinTreeView::indexAt(const QPoint& point) const
266 {
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;
273 }
274
275 void DolphinTreeView::setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags command)
276 {
277 // We must override setSelection() as Qt calls it internally and when this happens
278 // we must ensure that the default indexAt() is used.
279 if (!m_band.show) {
280 m_useDefaultIndexAt = true;
281 QTreeView::setSelection(rect, command);
282 m_useDefaultIndexAt = false;
283 } else {
284 // Use our own elastic band selection algorithm
285 updateElasticBandSelection();
286 }
287 }
288
289
290 void DolphinTreeView::scrollTo(const QModelIndex & index, ScrollHint hint)
291 {
292 if (!m_ignoreScrollTo) {
293 QTreeView::scrollTo(index, hint);
294 }
295 }
296
297 void DolphinTreeView::updateElasticBandSelection()
298 {
299 if (!m_band.show) {
300 return;
301 }
302
303 // Ensure the elastic band itself is up-to-date, in
304 // case we are being called due to e.g. a drag event.
305 updateElasticBand();
306
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();
318
319 if (selRect.isNull()) {
320 selectionModel()->select(m_band.originalSelection, QItemSelectionModel::ClearAndSelect);
321 m_band.ignoreOldInfo = true;
322 return;
323 }
324
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.
332 return;
333 }
334
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));
353
354 if (!itemSelectionChanged) {
355 return;
356 }
357 }
358 } else {
359 // This is the only piece of optimization data that needs to be explicitly
360 // discarded.
361 m_band.lastSelectionOrigin = QPoint();
362 m_band.lastSelectionDestination = QPoint();
363 oldSelRect = selRect;
364 }
365
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;
371
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()) {
376 return;
377 }
378
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);
384 } else {
385 startIndex = model()->index(startIndex.row(), KDirModel::Name);
386 }
387 if (!startIndex.isValid()) {
388 selectionModel()->select(m_band.originalSelection, QItemSelectionModel::ClearAndSelect);
389 m_band.ignoreOldInfo = true;
390 return;
391 }
392
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;
399
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();
408
409 do {
410 QRect currIndexRect = visualRect(currIndex);
411
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;
420 }
421 if ((cl > sr && cl < m_band.outsideNearestRightEdge)) {
422 m_band.outsideNearestRightEdge = cl;
423 }
424 if ((cl >= sl && cl <= sr && cl > m_band.insideNearestRightEdge)) {
425 m_band.insideNearestRightEdge = cl;
426 }
427 if ((cr >= sl && cr <= sr && cr < m_band.insideNearestLeftEdge)) {
428 m_band.insideNearestLeftEdge = cr;
429 }
430
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;
439 }
440
441 // NOTE: indexBelow actually walks up and down expanded trees for us.
442 QModelIndex nextIndex = indexBelow(currIndex);
443 allItemsInBoundDone = !nextIndex.isValid() || currIndexRect.top() > verticalCutoff;
444
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);
455 }
456 // Need to start a new range immediately with currIndex?
457 if (needToToggleItem) {
458 toggleIndexRangeBegin = currIndex;
459 formingToggleIndexRange = true;
460 }
461 if (allItemsInBoundDone && needToToggleItem) {
462 // Toggle the very last item in the bounds.
463 itemsToToggle.select(currIndex, currIndex);
464 }
465 }
466
467 // Next item
468 lastIndex = currIndex;
469 currIndex = nextIndex;
470 } while (!allItemsInBoundDone);
471
472
473 selectionModel()->select(itemsToToggle, QItemSelectionModel::Toggle);
474
475 m_band.lastSelectionOrigin = m_band.origin;
476 m_band.lastSelectionDestination = m_band.destination;
477 m_band.ignoreOldInfo = false;
478 }
479
480 void DolphinTreeView::updateElasticBand()
481 {
482 if (m_band.show) {
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);
490 }
491 if (m_band.destination.x() < 0) {
492 m_band.destination.setX(0);
493 }
494 dirtyRegion = dirtyRegion.united(elasticBandRect());
495 setDirtyRegion(dirtyRegion);
496 }
497 }
498
499 QRect DolphinTreeView::elasticBandRect() const
500 {
501 const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
502
503 const QPoint topLeft = m_band.origin - scrollPos;
504 const QPoint bottomRight = m_band.destination - scrollPos;
505 return QRect(topLeft, bottomRight).normalized();
506 }
507
508 bool DolphinTreeView::isAboveExpandingToggle(const QPoint& pos) const
509 {
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());
520 } else {
521 rect.moveLeft(rect.x() - toggleSize);
522 }
523 rect.setWidth(toggleSize);
524
525 QStyleOption opt;
526 opt.initFrom(this);
527 opt.rect = rect;
528 rect = style()->subElementRect(QStyle::SE_TreeViewDisclosureItem, &opt, this);
529
530 return rect.contains(pos);
531 }
532 }
533 return false;
534 }
535
536 DolphinTreeView::ElasticBand::ElasticBand() :
537 show(false),
538 origin(),
539 destination(),
540 lastSelectionOrigin(),
541 lastSelectionDestination(),
542 ignoreOldInfo(true),
543 outsideNearestLeftEdge(0),
544 outsideNearestRightEdge(0),
545 insideNearestLeftEdge(0),
546 insideNearestRightEdge(0)
547 {
548 }
549
550 #include "dolphintreeview.moc"