]> cloud.milkyroute.net Git - dolphin.git/blob - src/views/dolphintreeview.cpp
Use capitalized KDE includes
[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_expandingTogglePressed(false),
35 m_useDefaultIndexAt(true),
36 m_ignoreScrollTo(false),
37 m_dropRect(),
38 m_band()
39 {
40 setUniformRowHeights(true);
41 }
42
43 DolphinTreeView::~DolphinTreeView()
44 {
45 }
46
47 void DolphinTreeView::keyboardSearch(const QString & search)
48 {
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());
55 }
56 }
57
58 QRegion DolphinTreeView::visualRegionForSelection(const QItemSelection& selection) const
59 {
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();
65
66 foreach(const QModelIndex& index, indexes) {
67 selectionRegion += visualRect(index);
68 }
69
70 return selectionRegion;
71 }
72
73 bool DolphinTreeView::acceptsDrop(const QModelIndex& index) const
74 {
75 Q_UNUSED(index);
76 return false;
77 }
78
79 bool DolphinTreeView::event(QEvent* event)
80 {
81 switch (event->type()) {
82 case QEvent::Polish:
83 m_useDefaultIndexAt = false;
84 break;
85 default:
86 break;
87 }
88 return QTreeView::event(event);
89 }
90
91 void DolphinTreeView::mousePressEvent(QMouseEvent* event)
92 {
93 const QModelIndex current = currentIndex();
94 QTreeView::mousePressEvent(event);
95
96 m_expandingTogglePressed = isAboveExpandingToggle(event->pos());
97
98 const QModelIndex index = indexAt(event->pos());
99 const bool updateState = index.isValid() &&
100 (index.column() == DolphinModel::Name) &&
101 (event->button() == Qt::LeftButton);
102 if (updateState) {
103 setState(QAbstractItemView::DraggingState);
104 }
105
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)) {
109 clearSelection();
110 }
111
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;
117
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);
121 m_band.show = true;
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();
128 }
129 }
130 }
131
132 void DolphinTreeView::mouseMoveEvent(QMouseEvent* event)
133 {
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);
139 return;
140 }
141
142 if (m_band.show) {
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();
150 }
151
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);
156 updateElasticBand();
157 } else {
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);
162 }
163 }
164
165 void DolphinTreeView::mouseReleaseEvent(QMouseEvent* event)
166 {
167 if (!m_expandingTogglePressed) {
168 const QModelIndex index = indexAt(event->pos());
169 if (index.isValid() && (index.column() == DolphinModel::Name)) {
170 QTreeView::mouseReleaseEvent(event);
171 } else {
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);
178 }
179 }
180 m_expandingTogglePressed = false;
181
182 if (m_band.show) {
183 setState(NoState);
184 updateElasticBand();
185 m_band.show = false;
186 }
187 }
188
189 void DolphinTreeView::startDrag(Qt::DropActions supportedActions)
190 {
191 Q_UNUSED(supportedActions);
192 m_band.show = false;
193 }
194
195 void DolphinTreeView::dragEnterEvent(QDragEnterEvent* event)
196 {
197 Q_UNUSED(event);
198 if (m_band.show) {
199 updateElasticBand();
200 m_band.show = false;
201 }
202 }
203
204 void DolphinTreeView::dragMoveEvent(QDragMoveEvent* event)
205 {
206 QTreeView::dragMoveEvent(event);
207
208 setDirtyRegion(m_dropRect);
209
210 const QModelIndex index = indexAt(event->pos());
211 if (acceptsDrop(index)) {
212 m_dropRect = visualRect(index);
213 } else {
214 m_dropRect.setSize(QSize()); // set invalid
215 }
216 setDirtyRegion(m_dropRect);
217 }
218
219 void DolphinTreeView::dragLeaveEvent(QDragLeaveEvent* event)
220 {
221 QTreeView::dragLeaveEvent(event);
222 setDirtyRegion(m_dropRect);
223 }
224
225 void DolphinTreeView::paintEvent(QPaintEvent* event)
226 {
227 QTreeView::paintEvent(event);
228 if (m_band.show) {
229 // The following code has been taken from QListView
230 // and adapted to DolphinDetailsView.
231 // (C) 1992-2007 Trolltech ASA
232 QStyleOptionRubberBand opt;
233 opt.initFrom(this);
234 opt.shape = QRubberBand::Rectangle;
235 opt.opaque = false;
236 opt.rect = elasticBandRect();
237
238 QPainter painter(viewport());
239 painter.save();
240 style()->drawControl(QStyle::CE_RubberBand, &opt, &painter);
241 painter.restore();
242 }
243 }
244
245 QModelIndex DolphinTreeView::indexAt(const QPoint& point) const
246 {
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;
253 }
254
255 void DolphinTreeView::setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags command)
256 {
257 // We must override setSelection() as Qt calls it internally and when this happens
258 // we must ensure that the default indexAt() is used.
259 if (!m_band.show) {
260 m_useDefaultIndexAt = true;
261 QTreeView::setSelection(rect, command);
262 m_useDefaultIndexAt = false;
263 } else {
264 // Use our own elastic band selection algorithm
265 updateElasticBandSelection();
266 }
267 }
268
269
270 void DolphinTreeView::scrollTo(const QModelIndex & index, ScrollHint hint)
271 {
272 if (!m_ignoreScrollTo) {
273 QTreeView::scrollTo(index, hint);
274 }
275 }
276
277 void DolphinTreeView::updateElasticBandSelection()
278 {
279 if (!m_band.show) {
280 return;
281 }
282
283 // Ensure the elastic band itself is up-to-date, in
284 // case we are being called due to e.g. a drag event.
285 updateElasticBand();
286
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();
298
299 if (selRect.isNull()) {
300 selectionModel()->select(m_band.originalSelection, QItemSelectionModel::ClearAndSelect);
301 m_band.ignoreOldInfo = true;
302 return;
303 }
304
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.
312 return;
313 }
314
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));
333
334 if (!itemSelectionChanged) {
335 return;
336 }
337 }
338 } else {
339 // This is the only piece of optimization data that needs to be explicitly
340 // discarded.
341 m_band.lastSelectionOrigin = QPoint();
342 m_band.lastSelectionDestination = QPoint();
343 oldSelRect = selRect;
344 }
345
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;
351
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()) {
356 return;
357 }
358
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);
364 } else {
365 startIndex = model()->index(startIndex.row(), KDirModel::Name);
366 }
367 if (!startIndex.isValid()) {
368 selectionModel()->select(m_band.originalSelection, QItemSelectionModel::ClearAndSelect);
369 m_band.ignoreOldInfo = true;
370 return;
371 }
372
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;
379
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();
388
389 do {
390 QRect currIndexRect = visualRect(currIndex);
391
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;
400 }
401 if ((cl > sr && cl < m_band.outsideNearestRightEdge)) {
402 m_band.outsideNearestRightEdge = cl;
403 }
404 if ((cl >= sl && cl <= sr && cl > m_band.insideNearestRightEdge)) {
405 m_band.insideNearestRightEdge = cl;
406 }
407 if ((cr >= sl && cr <= sr && cr < m_band.insideNearestLeftEdge)) {
408 m_band.insideNearestLeftEdge = cr;
409 }
410
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;
419 }
420
421 // NOTE: indexBelow actually walks up and down expanded trees for us.
422 QModelIndex nextIndex = indexBelow(currIndex);
423 allItemsInBoundDone = !nextIndex.isValid() || currIndexRect.top() > verticalCutoff;
424
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);
435 }
436 // Need to start a new range immediately with currIndex?
437 if (needToToggleItem) {
438 toggleIndexRangeBegin = currIndex;
439 formingToggleIndexRange = true;
440 }
441 if (allItemsInBoundDone && needToToggleItem) {
442 // Toggle the very last item in the bounds.
443 itemsToToggle.select(currIndex, currIndex);
444 }
445 }
446
447 // Next item
448 lastIndex = currIndex;
449 currIndex = nextIndex;
450 } while (!allItemsInBoundDone);
451
452
453 selectionModel()->select(itemsToToggle, QItemSelectionModel::Toggle);
454
455 m_band.lastSelectionOrigin = m_band.origin;
456 m_band.lastSelectionDestination = m_band.destination;
457 m_band.ignoreOldInfo = false;
458 }
459
460 void DolphinTreeView::updateElasticBand()
461 {
462 if (m_band.show) {
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);
470 }
471 if (m_band.destination.x() < 0) {
472 m_band.destination.setX(0);
473 }
474 dirtyRegion = dirtyRegion.united(elasticBandRect());
475 setDirtyRegion(dirtyRegion);
476 }
477 }
478
479 QRect DolphinTreeView::elasticBandRect() const
480 {
481 const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
482
483 const QPoint topLeft = m_band.origin - scrollPos;
484 const QPoint bottomRight = m_band.destination - scrollPos;
485 return QRect(topLeft, bottomRight).normalized();
486 }
487
488 bool DolphinTreeView::isAboveExpandingToggle(const QPoint& pos) const
489 {
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());
500 } else {
501 rect.moveLeft(rect.x() - toggleSize);
502 }
503 rect.setWidth(toggleSize);
504
505 QStyleOption opt;
506 opt.initFrom(this);
507 opt.rect = rect;
508 rect = style()->subElementRect(QStyle::SE_TreeViewDisclosureItem, &opt, this);
509
510 return rect.contains(pos);
511 }
512 }
513 return false;
514 }
515
516 DolphinTreeView::ElasticBand::ElasticBand() :
517 show(false),
518 origin(),
519 destination(),
520 lastSelectionOrigin(),
521 lastSelectionDestination(),
522 ignoreOldInfo(true),
523 outsideNearestLeftEdge(0),
524 outsideNearestRightEdge(0),
525 insideNearestLeftEdge(0),
526 insideNearestRightEdge(0)
527 {
528 }
529
530 #include "dolphintreeview.moc"