]> cloud.milkyroute.net Git - dolphin.git/blob - src/views/dolphindetailsview.cpp
Further optimizations for calculating the width of columns
[dolphin.git] / src / views / dolphindetailsview.cpp
1 /***************************************************************************
2 * Copyright (C) 2006 by Peter Penz (peter.penz@gmx.at) *
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 "dolphindetailsview.h"
22
23 #include "additionalinfoaccessor.h"
24 #include "dolphinmodel.h"
25 #include "dolphinviewcontroller.h"
26 #include "dolphinfileitemdelegate.h"
27 #include "settings/dolphinsettings.h"
28 #include "dolphinsortfilterproxymodel.h"
29 #include "dolphinviewautoscroller.h"
30 #include "draganddrophelper.h"
31 #include "viewextensionsfactory.h"
32 #include "viewmodecontroller.h"
33 #include "viewproperties.h"
34 #include "zoomlevelinfo.h"
35
36 #include "dolphin_detailsmodesettings.h"
37 #include "dolphin_generalsettings.h"
38
39 #include <kdirmodel.h>
40 #include <kdirlister.h>
41 #include <klocale.h>
42 #include <kmenu.h>
43
44 #include <QAction>
45 #include <QApplication>
46 #include <QHeaderView>
47 #include <QRubberBand>
48 #include <QPainter>
49 #include <QScrollBar>
50
51 DolphinDetailsView::DolphinDetailsView(QWidget* parent,
52 DolphinViewController* dolphinViewController,
53 const ViewModeController* viewModeController,
54 DolphinSortFilterProxyModel* proxyModel) :
55 QTreeView(parent),
56 m_autoResize(true),
57 m_expandingTogglePressed(false),
58 m_keyPressed(false),
59 m_useDefaultIndexAt(true),
60 m_ignoreScrollTo(false),
61 m_dolphinViewController(dolphinViewController),
62 m_viewModeController(viewModeController),
63 m_extensionsFactory(0),
64 m_expandableFoldersAction(0),
65 m_expandedUrls(),
66 m_font(),
67 m_decorationSize(),
68 m_band()
69 {
70 const DetailsModeSettings* settings = DolphinSettings::instance().detailsModeSettings();
71 Q_ASSERT(settings != 0);
72 Q_ASSERT(dolphinViewController != 0);
73 Q_ASSERT(viewModeController != 0);
74
75 setLayoutDirection(Qt::LeftToRight);
76 setAcceptDrops(true);
77 setSortingEnabled(true);
78 setUniformRowHeights(true);
79 setSelectionBehavior(SelectItems);
80 setDragDropMode(QAbstractItemView::DragDrop);
81 setDropIndicatorShown(false);
82 setAlternatingRowColors(true);
83 setRootIsDecorated(settings->expandableFolders());
84 setItemsExpandable(settings->expandableFolders());
85 setEditTriggers(QAbstractItemView::NoEditTriggers);
86 setModel(proxyModel);
87
88 setMouseTracking(true);
89
90 const ViewProperties props(viewModeController->url());
91 setSortIndicatorSection(props.sorting());
92 setSortIndicatorOrder(props.sortOrder());
93
94 QHeaderView* headerView = header();
95 connect(headerView, SIGNAL(sectionClicked(int)),
96 this, SLOT(synchronizeSortingState(int)));
97 headerView->setContextMenuPolicy(Qt::CustomContextMenu);
98 connect(headerView, SIGNAL(customContextMenuRequested(const QPoint&)),
99 this, SLOT(configureSettings(const QPoint&)));
100 connect(headerView, SIGNAL(sectionResized(int, int, int)),
101 this, SLOT(slotHeaderSectionResized(int, int, int)));
102 connect(headerView, SIGNAL(sectionHandleDoubleClicked(int)),
103 this, SLOT(disableAutoResizing()));
104
105 connect(parent, SIGNAL(sortingChanged(DolphinView::Sorting)),
106 this, SLOT(setSortIndicatorSection(DolphinView::Sorting)));
107 connect(parent, SIGNAL(sortOrderChanged(Qt::SortOrder)),
108 this, SLOT(setSortIndicatorOrder(Qt::SortOrder)));
109
110 connect(this, SIGNAL(clicked(const QModelIndex&)),
111 dolphinViewController, SLOT(requestTab(const QModelIndex&)));
112 if (KGlobalSettings::singleClick()) {
113 connect(this, SIGNAL(clicked(const QModelIndex&)),
114 dolphinViewController, SLOT(triggerItem(const QModelIndex&)));
115 } else {
116 connect(this, SIGNAL(doubleClicked(const QModelIndex&)),
117 dolphinViewController, SLOT(triggerItem(const QModelIndex&)));
118 }
119
120 connect(this, SIGNAL(entered(const QModelIndex&)),
121 this, SLOT(slotEntered(const QModelIndex&)));
122 connect(this, SIGNAL(viewportEntered()),
123 dolphinViewController, SLOT(emitViewportEntered()));
124 connect(viewModeController, SIGNAL(zoomLevelChanged(int)),
125 this, SLOT(setZoomLevel(int)));
126 connect(dolphinViewController->view(), SIGNAL(additionalInfoChanged()),
127 this, SLOT(updateColumnVisibility()));
128 connect(viewModeController, SIGNAL(activationChanged(bool)),
129 this, SLOT(slotActivationChanged(bool)));
130
131 if (settings->useSystemFont()) {
132 m_font = KGlobalSettings::generalFont();
133 } else {
134 m_font = QFont(settings->fontFamily(),
135 qRound(settings->fontSize()),
136 settings->fontWeight(),
137 settings->italicFont());
138 m_font.setPointSizeF(settings->fontSize());
139 }
140
141 setVerticalScrollMode(QTreeView::ScrollPerPixel);
142 setHorizontalScrollMode(QTreeView::ScrollPerPixel);
143
144 const DolphinView* view = dolphinViewController->view();
145 connect(view, SIGNAL(showPreviewChanged()),
146 this, SLOT(slotShowPreviewChanged()));
147
148
149 setFocus();
150 viewport()->installEventFilter(this);
151
152 connect(KGlobalSettings::self(), SIGNAL(settingsChanged(int)),
153 this, SLOT(slotGlobalSettingsChanged(int)));
154
155 m_useDefaultIndexAt = false;
156
157 m_expandableFoldersAction = new QAction(i18nc("@option:check", "Expandable Folders"), this);
158 m_expandableFoldersAction->setCheckable(true);
159 connect(m_expandableFoldersAction, SIGNAL(toggled(bool)),
160 this, SLOT(setFoldersExpandable(bool)));
161
162 connect(this, SIGNAL(expanded(const QModelIndex&)), this, SLOT(slotExpanded(const QModelIndex&)));
163 connect(this, SIGNAL(collapsed(const QModelIndex&)), this, SLOT(slotCollapsed(const QModelIndex&)));
164
165 updateDecorationSize(view->showPreview());
166
167 m_extensionsFactory = new ViewExtensionsFactory(this, dolphinViewController, viewModeController);
168 m_extensionsFactory->fileItemDelegate()->setMinimizedNameColumn(true);
169 m_extensionsFactory->setAutoFolderExpandingEnabled(settings->expandableFolders());
170
171 KDirLister *dirLister = qobject_cast<KDirModel*>(proxyModel->sourceModel())->dirLister();
172 connect(dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(resizeColumns()));
173 }
174
175 DolphinDetailsView::~DolphinDetailsView()
176 {
177 }
178
179 QSet<KUrl> DolphinDetailsView::expandedUrls() const
180 {
181 return m_expandedUrls;
182 }
183
184 QRegion DolphinDetailsView::visualRegionForSelection(const QItemSelection& selection) const
185 {
186 // We have to make sure that the visualRect of each model index is inside the region.
187 // QTreeView::visualRegionForSelection does not do it right because it assumes implicitly
188 // that all visualRects have the same width, which is in general not the case here.
189 QRegion selectionRegion;
190 const QModelIndexList indexes = selection.indexes();
191
192 foreach(const QModelIndex& index, indexes) {
193 selectionRegion += visualRect(index);
194 }
195
196 return selectionRegion;
197 }
198
199 bool DolphinDetailsView::event(QEvent* event)
200 {
201 switch (event->type()) {
202 case QEvent::Polish:
203 header()->setResizeMode(QHeaderView::Interactive);
204 updateColumnVisibility();
205 break;
206
207 case QEvent::FocusOut:
208 // If a key-press triggers an action that e. g. opens a dialog, the
209 // widget gets no key-release event. Assure that the pressed state
210 // is reset to prevent accidently setting the current index during a selection.
211 m_keyPressed = false;
212 break;
213
214 default:
215 break;
216 }
217
218 return QTreeView::event(event);
219 }
220
221 QStyleOptionViewItem DolphinDetailsView::viewOptions() const
222 {
223 QStyleOptionViewItem viewOptions = QTreeView::viewOptions();
224 viewOptions.font = m_font;
225 viewOptions.fontMetrics = QFontMetrics(m_font);
226 viewOptions.showDecorationSelected = true;
227 viewOptions.decorationSize = m_decorationSize;
228 return viewOptions;
229 }
230
231 void DolphinDetailsView::contextMenuEvent(QContextMenuEvent* event)
232 {
233 QTreeView::contextMenuEvent(event);
234
235 DetailsModeSettings* settings = DolphinSettings::instance().detailsModeSettings();
236 m_expandableFoldersAction->setChecked(settings->expandableFolders());
237 m_dolphinViewController->triggerContextMenuRequest(event->pos(),
238 QList<QAction*>() << m_expandableFoldersAction);
239 }
240
241 void DolphinDetailsView::mousePressEvent(QMouseEvent* event)
242 {
243 m_dolphinViewController->requestActivation();
244
245 const QModelIndex current = currentIndex();
246 QTreeView::mousePressEvent(event);
247
248 m_expandingTogglePressed = isAboveExpandingToggle(event->pos());
249
250 const QModelIndex index = indexAt(event->pos());
251 const bool updateState = index.isValid() &&
252 (index.column() == DolphinModel::Name) &&
253 (event->button() == Qt::LeftButton);
254 if (updateState) {
255 setState(QAbstractItemView::DraggingState);
256 }
257
258 if (!index.isValid() || (index.column() != DolphinModel::Name)) {
259 // the mouse press is done somewhere outside the filename column
260 if (QApplication::mouseButtons() & Qt::MidButton) {
261 m_dolphinViewController->replaceUrlByClipboard();
262 }
263
264 const Qt::KeyboardModifiers mod = QApplication::keyboardModifiers();
265 if (!m_expandingTogglePressed && !(mod & Qt::ShiftModifier) && !(mod & Qt::ControlModifier)) {
266 clearSelection();
267 }
268
269 // restore the current index, other columns are handled as viewport area.
270 // setCurrentIndex(...) implicitly calls scrollTo(...), which we want to ignore.
271 m_ignoreScrollTo = true;
272 selectionModel()->setCurrentIndex(current, QItemSelectionModel::Current);
273 m_ignoreScrollTo = false;
274
275 if ((event->button() == Qt::LeftButton) && !m_expandingTogglePressed) {
276 // Inform Qt about what we are doing - otherwise it starts dragging items around!
277 setState(DragSelectingState);
278 m_band.show = true;
279 // Incremental update data will not be useful - start from scratch.
280 m_band.ignoreOldInfo = true;
281 const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
282 m_band.origin = event->pos() + scrollPos;
283 m_band.destination = m_band.origin;
284 m_band.originalSelection = selectionModel()->selection();
285 }
286 }
287 }
288
289 void DolphinDetailsView::mouseMoveEvent(QMouseEvent* event)
290 {
291 if (m_expandingTogglePressed) {
292 // Per default QTreeView starts either a selection or a drag operation when dragging
293 // the expanding toggle button (Qt-issue - see TODO comment in DolphinIconsView::mousePressEvent()).
294 // Turn off this behavior in Dolphin to stay predictable:
295 setState(QAbstractItemView::NoState);
296 return;
297 }
298
299 if (m_band.show) {
300 const QPoint mousePos = event->pos();
301 const QModelIndex index = indexAt(mousePos);
302 if (!index.isValid()) {
303 // the destination of the selection rectangle is above the viewport. In this
304 // case QTreeView does no selection at all, which is not the wanted behavior
305 // in Dolphin -> select all items within the elastic band rectangle
306 updateElasticBandSelection();
307 }
308
309 // TODO: enable QTreeView::mouseMoveEvent(event) again, as soon
310 // as the Qt-issue #199631 has been fixed.
311 // QTreeView::mouseMoveEvent(event);
312 QAbstractItemView::mouseMoveEvent(event);
313 updateElasticBand();
314 } else {
315 // TODO: enable QTreeView::mouseMoveEvent(event) again, as soon
316 // as the Qt-issue #199631 has been fixed.
317 // QTreeView::mouseMoveEvent(event);
318 QAbstractItemView::mouseMoveEvent(event);
319 }
320 }
321
322 void DolphinDetailsView::mouseReleaseEvent(QMouseEvent* event)
323 {
324 if (!m_expandingTogglePressed) {
325 const QModelIndex index = indexAt(event->pos());
326 if (index.isValid() && (index.column() == DolphinModel::Name)) {
327 QTreeView::mouseReleaseEvent(event);
328 } else {
329 // don't change the current index if the cursor is released
330 // above any other column than the name column, as the other
331 // columns act as viewport
332 const QModelIndex current = currentIndex();
333 QTreeView::mouseReleaseEvent(event);
334 selectionModel()->setCurrentIndex(current, QItemSelectionModel::Current);
335 }
336 }
337 m_expandingTogglePressed = false;
338
339 if (m_band.show) {
340 setState(NoState);
341 updateElasticBand();
342 m_band.show = false;
343 }
344 }
345
346 void DolphinDetailsView::startDrag(Qt::DropActions supportedActions)
347 {
348 DragAndDropHelper::instance().startDrag(this, supportedActions, m_dolphinViewController);
349 m_band.show = false;
350 }
351
352 void DolphinDetailsView::dragEnterEvent(QDragEnterEvent* event)
353 {
354 if (DragAndDropHelper::instance().isMimeDataSupported(event->mimeData())) {
355 event->acceptProposedAction();
356 }
357
358 if (m_band.show) {
359 updateElasticBand();
360 m_band.show = false;
361 }
362 }
363
364 void DolphinDetailsView::dragLeaveEvent(QDragLeaveEvent* event)
365 {
366 QTreeView::dragLeaveEvent(event);
367 setDirtyRegion(m_dropRect);
368 }
369
370 void DolphinDetailsView::dragMoveEvent(QDragMoveEvent* event)
371 {
372 QTreeView::dragMoveEvent(event);
373
374 // TODO: remove this code when the issue #160611 is solved in Qt 4.4
375 setDirtyRegion(m_dropRect);
376 const QModelIndex index = indexAt(event->pos());
377 if (index.isValid() && (index.column() == DolphinModel::Name)) {
378 const KFileItem item = m_dolphinViewController->itemForIndex(index);
379 if (!item.isNull() && item.isDir()) {
380 m_dropRect = visualRect(index);
381 } else {
382 m_dropRect.setSize(QSize()); // set as invalid
383 }
384 setDirtyRegion(m_dropRect);
385 }
386
387 if (DragAndDropHelper::instance().isMimeDataSupported(event->mimeData())) {
388 // accept url drops, independently from the destination item
389 event->acceptProposedAction();
390 }
391 }
392
393 void DolphinDetailsView::dropEvent(QDropEvent* event)
394 {
395 const QModelIndex index = indexAt(event->pos());
396 KFileItem item;
397 if (index.isValid() && (index.column() == DolphinModel::Name)) {
398 item = m_dolphinViewController->itemForIndex(index);
399 }
400 m_dolphinViewController->indicateDroppedUrls(item, m_viewModeController->url(), event);
401 QTreeView::dropEvent(event);
402 }
403
404 void DolphinDetailsView::paintEvent(QPaintEvent* event)
405 {
406 QTreeView::paintEvent(event);
407 if (m_band.show) {
408 // The following code has been taken from QListView
409 // and adapted to DolphinDetailsView.
410 // (C) 1992-2007 Trolltech ASA
411 QStyleOptionRubberBand opt;
412 opt.initFrom(this);
413 opt.shape = QRubberBand::Rectangle;
414 opt.opaque = false;
415 opt.rect = elasticBandRect();
416
417 QPainter painter(viewport());
418 painter.save();
419 style()->drawControl(QStyle::CE_RubberBand, &opt, &painter);
420 painter.restore();
421 }
422 }
423
424 void DolphinDetailsView::keyPressEvent(QKeyEvent* event)
425 {
426 // If the Control modifier is pressed, a multiple selection
427 // is done and DolphinDetailsView::currentChanged() may not
428 // not change the selection in a custom way.
429 m_keyPressed = !(event->modifiers() & Qt::ControlModifier);
430
431 QTreeView::keyPressEvent(event);
432 m_dolphinViewController->handleKeyPressEvent(event);
433 }
434
435 void DolphinDetailsView::keyReleaseEvent(QKeyEvent* event)
436 {
437 QTreeView::keyReleaseEvent(event);
438 m_keyPressed = false;
439 }
440
441 void DolphinDetailsView::resizeEvent(QResizeEvent* event)
442 {
443 QTreeView::resizeEvent(event);
444 if (m_autoResize) {
445 resizeColumns();
446 }
447 }
448
449 void DolphinDetailsView::wheelEvent(QWheelEvent* event)
450 {
451 const int step = m_decorationSize.height();
452 verticalScrollBar()->setSingleStep(step);
453 QTreeView::wheelEvent(event);
454 }
455
456 void DolphinDetailsView::currentChanged(const QModelIndex& current, const QModelIndex& previous)
457 {
458 QTreeView::currentChanged(current, previous);
459 m_extensionsFactory->handleCurrentIndexChange(current, previous);
460
461 // Stay consistent with QListView: When changing the current index by key presses,
462 // also change the selection.
463 if (m_keyPressed) {
464 setCurrentIndex(current);
465 }
466
467 // If folders are expanded, the width which is available for editing may have changed
468 // because it depends on the level of the current item in the folder hierarchy.
469 adjustMaximumSizeForEditing(current);
470 }
471
472 bool DolphinDetailsView::eventFilter(QObject* watched, QEvent* event)
473 {
474 if ((watched == viewport()) && (event->type() == QEvent::Leave)) {
475 // if the mouse is above an item and moved very fast outside the widget,
476 // no viewportEntered() signal might be emitted although the mouse has been moved
477 // above the viewport
478 m_dolphinViewController->emitViewportEntered();
479 }
480
481 return QTreeView::eventFilter(watched, event);
482 }
483
484 QModelIndex DolphinDetailsView::indexAt(const QPoint& point) const
485 {
486 // the blank portion of the name column counts as empty space
487 const QModelIndex index = QTreeView::indexAt(point);
488 const bool isAboveEmptySpace = !m_useDefaultIndexAt &&
489 (index.column() == KDirModel::Name) && !visualRect(index).contains(point);
490 return isAboveEmptySpace ? QModelIndex() : index;
491 }
492
493 QRect DolphinDetailsView::visualRect(const QModelIndex& index) const
494 {
495 QRect rect = QTreeView::visualRect(index);
496 const KFileItem item = m_dolphinViewController->itemForIndex(index);
497 if (!item.isNull()) {
498 const int width = DolphinFileItemDelegate::nameColumnWidth(item.text(), viewOptions());
499 rect.setWidth(width);
500 }
501
502 return rect;
503 }
504
505 void DolphinDetailsView::setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags command)
506 {
507 // We must override setSelection() as Qt calls it internally and when this happens
508 // we must ensure that the default indexAt() is used.
509 if (!m_band.show) {
510 m_useDefaultIndexAt = true;
511 QTreeView::setSelection(rect, command);
512 m_useDefaultIndexAt = false;
513 } else {
514 // Use our own elastic band selection algorithm
515 updateElasticBandSelection();
516 }
517 }
518
519 void DolphinDetailsView::scrollTo(const QModelIndex & index, ScrollHint hint)
520 {
521 if (!m_ignoreScrollTo) {
522 QTreeView::scrollTo(index, hint);
523 }
524 }
525
526 void DolphinDetailsView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
527 {
528 removeExpandedIndexes(parent, start, end);
529 QTreeView::rowsAboutToBeRemoved(parent, start, end);
530 }
531
532 void DolphinDetailsView::setSortIndicatorSection(DolphinView::Sorting sorting)
533 {
534 header()->setSortIndicator(sorting, header()->sortIndicatorOrder());
535 }
536
537 void DolphinDetailsView::setSortIndicatorOrder(Qt::SortOrder sortOrder)
538 {
539 header()->setSortIndicator(header()->sortIndicatorSection(), sortOrder);
540 }
541
542 void DolphinDetailsView::synchronizeSortingState(int column)
543 {
544 // The sorting has already been changed in QTreeView if this slot is
545 // invoked, but Dolphin is not informed about this.
546 DolphinView::Sorting sorting = DolphinSortFilterProxyModel::sortingForColumn(column);
547 const Qt::SortOrder sortOrder = header()->sortIndicatorOrder();
548 m_dolphinViewController->indicateSortingChange(sorting);
549 m_dolphinViewController->indicateSortOrderChange(sortOrder);
550 }
551
552 void DolphinDetailsView::slotEntered(const QModelIndex& index)
553 {
554 if (index.column() == DolphinModel::Name) {
555 m_dolphinViewController->emitItemEntered(index);
556 } else {
557 m_dolphinViewController->emitViewportEntered();
558 }
559 }
560
561 void DolphinDetailsView::updateElasticBand()
562 {
563 if (m_band.show) {
564 QRect dirtyRegion(elasticBandRect());
565 const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
566 m_band.destination = viewport()->mapFromGlobal(QCursor::pos()) + scrollPos;
567 // Going above the (logical) top-left of the view causes complications during selection;
568 // we may as well prevent it.
569 if (m_band.destination.y() < 0) {
570 m_band.destination.setY(0);
571 }
572 if (m_band.destination.x() < 0) {
573 m_band.destination.setX(0);
574 }
575 dirtyRegion = dirtyRegion.united(elasticBandRect());
576 setDirtyRegion(dirtyRegion);
577 }
578 }
579
580 QRect DolphinDetailsView::elasticBandRect() const
581 {
582 const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
583
584 const QPoint topLeft = m_band.origin - scrollPos;
585 const QPoint bottomRight = m_band.destination - scrollPos;
586 return QRect(topLeft, bottomRight).normalized();
587 }
588
589 void DolphinDetailsView::setZoomLevel(int level)
590 {
591 const int size = ZoomLevelInfo::iconSizeForZoomLevel(level);
592 DetailsModeSettings* settings = DolphinSettings::instance().detailsModeSettings();
593
594 const bool showPreview = m_dolphinViewController->view()->showPreview();
595 if (showPreview) {
596 settings->setPreviewSize(size);
597 } else {
598 settings->setIconSize(size);
599 }
600
601 updateDecorationSize(showPreview);
602 }
603
604 void DolphinDetailsView::slotShowPreviewChanged()
605 {
606 const DolphinView* view = m_dolphinViewController->view();
607 updateDecorationSize(view->showPreview());
608 }
609
610 void DolphinDetailsView::configureSettings(const QPoint& pos)
611 {
612 KMenu popup(this);
613 popup.addTitle(i18nc("@title:menu", "Columns"));
614
615 // add checkbox items for each column
616 QHeaderView* headerView = header();
617 const int columns = model()->columnCount();
618 for (int i = 0; i < columns; ++i) {
619 const int logicalIndex = headerView->logicalIndex(i);
620 const QString text = model()->headerData(logicalIndex, Qt::Horizontal).toString();
621 if (!text.isEmpty()) {
622 QAction* action = popup.addAction(text);
623 action->setCheckable(true);
624 action->setChecked(!headerView->isSectionHidden(logicalIndex));
625 action->setData(logicalIndex);
626 action->setEnabled(logicalIndex != DolphinModel::Name);
627 }
628 }
629 popup.addSeparator();
630
631 QAction* activatedAction = popup.exec(header()->mapToGlobal(pos));
632 if (activatedAction != 0) {
633 const bool show = activatedAction->isChecked();
634 const int columnIndex = activatedAction->data().toInt();
635
636 KFileItemDelegate::InformationList list = m_dolphinViewController->view()->additionalInfo();
637 const KFileItemDelegate::Information info = infoForColumn(columnIndex);
638 if (show) {
639 Q_ASSERT(!list.contains(info));
640 list.append(info);
641 } else {
642 Q_ASSERT(list.contains(info));
643 const int index = list.indexOf(info);
644 list.removeAt(index);
645 }
646
647 m_dolphinViewController->indicateAdditionalInfoChange(list);
648 setColumnHidden(columnIndex, !show);
649 resizeColumns();
650 }
651 }
652
653 void DolphinDetailsView::updateColumnVisibility()
654 {
655 QHeaderView* headerView = header();
656 disconnect(headerView, SIGNAL(sectionMoved(int, int, int)),
657 this, SLOT(saveColumnPositions()));
658
659 const DetailsModeSettings* settings = DolphinSettings::instance().detailsModeSettings();
660 const QList<int> columnPositions = settings->columnPositions();
661
662 const KFileItemDelegate::InformationList list = m_dolphinViewController->view()->additionalInfo();
663 for (int i = DolphinModel::Name; i < DolphinModel::ExtraColumnCount; ++i) {
664 const KFileItemDelegate::Information info = infoForColumn(i);
665 const bool hide = !list.contains(info) && (i != DolphinModel::Name);
666 if (isColumnHidden(i) != hide) {
667 setColumnHidden(i, hide);
668 }
669
670 // If the list columnPositions has been written by an older Dolphin version,
671 // its length might be smaller than DolphinModel::ExtraColumnCount. Therefore,
672 // we have to check if item number i exists before accessing it.
673 if (i < columnPositions.length()) {
674 const int position = columnPositions[i];
675
676 // The position might be outside the correct range if the list columnPositions
677 // has been written by a newer Dolphin version with more columns.
678 if (position < DolphinModel::ExtraColumnCount) {
679 const int from = headerView->visualIndex(i);
680 headerView->moveSection(from, position);
681 }
682 }
683 }
684
685 resizeColumns();
686
687 connect(headerView, SIGNAL(sectionMoved(int, int, int)),
688 this, SLOT(saveColumnPositions()));
689 }
690
691 void DolphinDetailsView::resizeColumns()
692 {
693 // Using the resize mode QHeaderView::ResizeToContents is too slow (it takes
694 // around 3 seconds for each (!) resize operation when having > 10000 items).
695 // This gets a problem especially when opening large directories, where several
696 // resize operations are received for showing the currently available items during
697 // loading (the application hangs around 20 seconds when loading > 10000 items).
698
699 QHeaderView* headerView = header();
700 const int rowCount = model()->rowCount();
701 QFontMetrics fontMetrics(viewport()->font());
702
703 // Define the maximum number of rows, where an exact (but expensive) calculation
704 // of the widths is done.
705 const int maxRowCount = 200;
706
707 // Calculate the required with for each column and store it in columnWidth[]
708 int columnWidth[DolphinModel::ExtraColumnCount];
709
710 for (int column = 0; column < DolphinModel::ExtraColumnCount; ++column) {
711 columnWidth[column] = 0;
712 if (!isColumnHidden(column)) {
713 // Calculate the required width for the current column and consider only
714 // up to maxRowCount columns for performance reasons
715 if (rowCount > 0) {
716 const QAbstractProxyModel* proxyModel = qobject_cast<const QAbstractProxyModel*>(model());
717 const KDirModel* dirModel = qobject_cast<const KDirModel*>(proxyModel->sourceModel());
718
719 const int count = qMin(rowCount, maxRowCount);
720 const QStyleOptionViewItem option = viewOptions();
721 for (int row = 0; row < count; ++row) {
722 const QModelIndex index = dirModel->index(row, column);
723 const int width = itemDelegate()->sizeHint(option, index).width();
724 if (width > columnWidth[column]) {
725 columnWidth[column] = width;
726 }
727 }
728 }
729
730 // Assure that the required width is sufficient for the header too
731 const int logicalIndex = headerView->logicalIndex(column);
732 const QString headline = model()->headerData(logicalIndex, Qt::Horizontal).toString();
733 // TODO: check Qt-sources which left/right-gap is used for the headlines
734 const int headlineWidth = fontMetrics.width(headline) + 20;
735
736 columnWidth[column] = qMax(columnWidth[column], headlineWidth);
737 }
738 }
739
740 // Resize all columns except of the name column
741 int requiredWidth = 0;
742 for (int column = KDirModel::Size; column < DolphinModel::ExtraColumnCount; ++column) {
743 if (!isColumnHidden(column)) {
744 requiredWidth += columnWidth[column];
745 headerView->resizeSection(column, columnWidth[column]);
746 }
747 }
748
749 // Resize the name column in a way that the whole available width is used
750 columnWidth[KDirModel::Name] = viewport()->width() - requiredWidth;
751
752 const int minNameWidth = 300;
753 if (columnWidth[KDirModel::Name] < minNameWidth) {
754 columnWidth[KDirModel::Name] = minNameWidth;
755
756 if ((rowCount > 0) && (rowCount < maxRowCount)) {
757 // Try to decrease the name column width without clipping any text
758 const int nameWidth = sizeHintForColumn(DolphinModel::Name);
759 if (nameWidth + requiredWidth <= viewport()->width()) {
760 columnWidth[KDirModel::Name] = viewport()->width() - requiredWidth;
761 } else if (nameWidth < minNameWidth) {
762 columnWidth[KDirModel::Name] = nameWidth;
763 }
764 }
765 }
766
767 headerView->resizeSection(KDirModel::Name, columnWidth[KDirModel::Name]);
768 }
769
770 void DolphinDetailsView::saveColumnPositions()
771 {
772 QList<int> columnPositions;
773 for (int i = DolphinModel::Name; i < DolphinModel::ExtraColumnCount; ++i) {
774 columnPositions.append(header()->visualIndex(i));
775 }
776
777 DetailsModeSettings* settings = DolphinSettings::instance().detailsModeSettings();
778 settings->setColumnPositions(columnPositions);
779 }
780
781 void DolphinDetailsView::slotHeaderSectionResized(int logicalIndex, int oldSize, int newSize)
782 {
783 Q_UNUSED(logicalIndex);
784 Q_UNUSED(oldSize);
785 Q_UNUSED(newSize);
786 // If the user changes the size of the headers, the autoresize feature should be
787 // turned off. As there is no dedicated interface to find out whether the header
788 // section has been resized by the user or by a resize event, another approach is used.
789 // Attention: Take care when changing the if-condition to verify that there is no
790 // regression in combination with bug 178630 (see fix in comment #8).
791 if ((QApplication::mouseButtons() & Qt::LeftButton) && header()->underMouse()) {
792 disableAutoResizing();
793 }
794
795 adjustMaximumSizeForEditing(currentIndex());
796 }
797
798 void DolphinDetailsView::slotActivationChanged(bool active)
799 {
800 setAlternatingRowColors(active);
801 }
802
803 void DolphinDetailsView::disableAutoResizing()
804 {
805 m_autoResize = false;
806 }
807
808 void DolphinDetailsView::requestActivation()
809 {
810 m_dolphinViewController->requestActivation();
811 }
812
813 void DolphinDetailsView::slotGlobalSettingsChanged(int category)
814 {
815 Q_UNUSED(category);
816
817 const DetailsModeSettings* settings = DolphinSettings::instance().detailsModeSettings();
818 Q_ASSERT(settings != 0);
819 if (settings->useSystemFont()) {
820 m_font = KGlobalSettings::generalFont();
821 }
822 //Disconnect then reconnect, since the settings have been changed, the connection requirements may have also.
823 disconnect(this, SIGNAL(clicked(QModelIndex)), m_dolphinViewController, SLOT(triggerItem(QModelIndex)));
824 disconnect(this, SIGNAL(doubleClicked(QModelIndex)), m_dolphinViewController, SLOT(triggerItem(QModelIndex)));
825 if (KGlobalSettings::singleClick()) {
826 connect(this, SIGNAL(clicked(QModelIndex)), m_dolphinViewController, SLOT(triggerItem(QModelIndex)));
827 } else {
828 connect(this, SIGNAL(doubleClicked(QModelIndex)), m_dolphinViewController, SLOT(triggerItem(QModelIndex)));
829 }
830 }
831
832 void DolphinDetailsView::updateElasticBandSelection()
833 {
834 if (!m_band.show) {
835 return;
836 }
837
838 // Ensure the elastic band itself is up-to-date, in
839 // case we are being called due to e.g. a drag event.
840 updateElasticBand();
841
842 // Clip horizontally to the name column, as some filenames will be
843 // longer than the column. We don't clip vertically as origin
844 // may be above or below the current viewport area.
845 const int nameColumnX = header()->sectionPosition(DolphinModel::Name);
846 const int nameColumnWidth = header()->sectionSize(DolphinModel::Name);
847 QRect selRect = elasticBandRect().normalized();
848 QRect nameColumnArea(nameColumnX, selRect.y(), nameColumnWidth, selRect.height());
849 selRect = nameColumnArea.intersect(selRect).normalized();
850 // Get the last elastic band rectangle, expressed in viewpoint coordinates.
851 const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
852 QRect oldSelRect = QRect(m_band.lastSelectionOrigin - scrollPos, m_band.lastSelectionDestination - scrollPos).normalized();
853
854 if (selRect.isNull()) {
855 selectionModel()->select(m_band.originalSelection, QItemSelectionModel::ClearAndSelect);
856 m_band.ignoreOldInfo = true;
857 return;
858 }
859
860 if (!m_band.ignoreOldInfo) {
861 // Do some quick checks to see if we can rule out the need to
862 // update the selection.
863 Q_ASSERT(uniformRowHeights());
864 QModelIndex dummyIndex = model()->index(0, 0);
865 if (!dummyIndex.isValid()) {
866 // No items in the model presumably.
867 return;
868 }
869
870 // If the elastic band does not cover the same rows as before, we'll
871 // need to re-check, and also invalidate the old item distances.
872 const int rowHeight = QTreeView::rowHeight(dummyIndex);
873 const bool coveringSameRows =
874 (selRect.top() / rowHeight == oldSelRect.top() / rowHeight) &&
875 (selRect.bottom() / rowHeight == oldSelRect.bottom() / rowHeight);
876 if (coveringSameRows) {
877 // Covering the same rows, but have we moved far enough horizontally
878 // that we might have (de)selected some other items?
879 const bool itemSelectionChanged =
880 ((selRect.left() > oldSelRect.left()) &&
881 (selRect.left() > m_band.insideNearestLeftEdge)) ||
882 ((selRect.left() < oldSelRect.left()) &&
883 (selRect.left() <= m_band.outsideNearestLeftEdge)) ||
884 ((selRect.right() < oldSelRect.right()) &&
885 (selRect.left() >= m_band.insideNearestRightEdge)) ||
886 ((selRect.right() > oldSelRect.right()) &&
887 (selRect.right() >= m_band.outsideNearestRightEdge));
888
889 if (!itemSelectionChanged) {
890 return;
891 }
892 }
893 } else {
894 // This is the only piece of optimization data that needs to be explicitly
895 // discarded.
896 m_band.lastSelectionOrigin = QPoint();
897 m_band.lastSelectionDestination = QPoint();
898 oldSelRect = selRect;
899 }
900
901 // Do the selection from scratch. Force a update of the horizontal distances info.
902 m_band.insideNearestLeftEdge = nameColumnX + nameColumnWidth + 1;
903 m_band.insideNearestRightEdge = nameColumnX - 1;
904 m_band.outsideNearestLeftEdge = nameColumnX - 1;
905 m_band.outsideNearestRightEdge = nameColumnX + nameColumnWidth + 1;
906
907 // Include the old selection rect as well, so we can deselect
908 // items that were inside it but not in the new selRect.
909 const QRect boundingRect = selRect.united(oldSelRect).normalized();
910 if (boundingRect.isNull()) {
911 return;
912 }
913
914 // Get the index of the item in this row in the name column.
915 // TODO - would this still work if the columns could be re-ordered?
916 QModelIndex startIndex = QTreeView::indexAt(boundingRect.topLeft());
917 if (startIndex.parent().isValid()) {
918 startIndex = startIndex.parent().child(startIndex.row(), KDirModel::Name);
919 } else {
920 startIndex = model()->index(startIndex.row(), KDirModel::Name);
921 }
922 if (!startIndex.isValid()) {
923 selectionModel()->select(m_band.originalSelection, QItemSelectionModel::ClearAndSelect);
924 m_band.ignoreOldInfo = true;
925 return;
926 }
927
928 // Go through all indexes between the top and bottom of boundingRect, and
929 // update the selection.
930 const int verticalCutoff = boundingRect.bottom();
931 QModelIndex currIndex = startIndex;
932 QModelIndex lastIndex;
933 bool allItemsInBoundDone = false;
934
935 // Calling selectionModel()->select(...) for each item that needs to be
936 // toggled is slow as each call emits selectionChanged(...) so store them
937 // and do the selection toggle in one batch.
938 QItemSelection itemsToToggle;
939 // QItemSelection's deal with continuous ranges of indexes better than
940 // single indexes, so try to portion items that need to be toggled into ranges.
941 bool formingToggleIndexRange = false;
942 QModelIndex toggleIndexRangeBegin = QModelIndex();
943
944 do {
945 QRect currIndexRect = visualRect(currIndex);
946
947 // Update some optimization info as we go.
948 const int cr = currIndexRect.right();
949 const int cl = currIndexRect.left();
950 const int sl = selRect.left();
951 const int sr = selRect.right();
952 // "The right edge of the name is outside of the rect but nearer than m_outsideNearestLeft", etc
953 if ((cr < sl && cr > m_band.outsideNearestLeftEdge)) {
954 m_band.outsideNearestLeftEdge = cr;
955 }
956 if ((cl > sr && cl < m_band.outsideNearestRightEdge)) {
957 m_band.outsideNearestRightEdge = cl;
958 }
959 if ((cl >= sl && cl <= sr && cl > m_band.insideNearestRightEdge)) {
960 m_band.insideNearestRightEdge = cl;
961 }
962 if ((cr >= sl && cr <= sr && cr < m_band.insideNearestLeftEdge)) {
963 m_band.insideNearestLeftEdge = cr;
964 }
965
966 bool currentlySelected = selectionModel()->isSelected(currIndex);
967 bool originallySelected = m_band.originalSelection.contains(currIndex);
968 bool intersectsSelectedRect = currIndexRect.intersects(selRect);
969 bool shouldBeSelected = (intersectsSelectedRect && !originallySelected) || (!intersectsSelectedRect && originallySelected);
970 bool needToToggleItem = (currentlySelected && !shouldBeSelected) || (!currentlySelected && shouldBeSelected);
971 if (needToToggleItem && !formingToggleIndexRange) {
972 toggleIndexRangeBegin = currIndex;
973 formingToggleIndexRange = true;
974 }
975
976 // NOTE: indexBelow actually walks up and down expanded trees for us.
977 QModelIndex nextIndex = indexBelow(currIndex);
978 allItemsInBoundDone = !nextIndex.isValid() || currIndexRect.top() > verticalCutoff;
979
980 const bool commitToggleIndexRange = formingToggleIndexRange &&
981 (!needToToggleItem ||
982 allItemsInBoundDone ||
983 currIndex.parent() != toggleIndexRangeBegin.parent());
984 if (commitToggleIndexRange) {
985 formingToggleIndexRange = false;
986 // If this is the last item in the bounds and it is also the beginning of a range,
987 // don't toggle lastIndex - it will already have been dealt with.
988 if (!allItemsInBoundDone || toggleIndexRangeBegin != currIndex) {
989 itemsToToggle.select(toggleIndexRangeBegin, lastIndex);
990 }
991 // Need to start a new range immediately with currIndex?
992 if (needToToggleItem) {
993 toggleIndexRangeBegin = currIndex;
994 formingToggleIndexRange = true;
995 }
996 if (allItemsInBoundDone && needToToggleItem) {
997 // Toggle the very last item in the bounds.
998 itemsToToggle.select(currIndex, currIndex);
999 }
1000 }
1001
1002 // next item
1003 lastIndex = currIndex;
1004 currIndex = nextIndex;
1005 } while (!allItemsInBoundDone);
1006
1007
1008 selectionModel()->select(itemsToToggle, QItemSelectionModel::Toggle);
1009
1010 m_band.lastSelectionOrigin = m_band.origin;
1011 m_band.lastSelectionDestination = m_band.destination;
1012 m_band.ignoreOldInfo = false;
1013 }
1014
1015 void DolphinDetailsView::setFoldersExpandable(bool expandable)
1016 {
1017 if (!expandable) {
1018 // collapse all expanded folders, as QTreeView::setItemsExpandable(false)
1019 // does not do this task
1020 const int rowCount = model()->rowCount();
1021 for (int row = 0; row < rowCount; ++row) {
1022 setExpanded(model()->index(row, 0), false);
1023 }
1024 }
1025 DetailsModeSettings* settings = DolphinSettings::instance().detailsModeSettings();
1026 settings->setExpandableFolders(expandable);
1027 setRootIsDecorated(expandable);
1028 setItemsExpandable(expandable);
1029
1030 // The width of the space which is available for editing has changed
1031 // because of the (dis)appearance of the expanding toggles
1032 adjustMaximumSizeForEditing(currentIndex());
1033 }
1034
1035 void DolphinDetailsView::slotExpanded(const QModelIndex& index)
1036 {
1037 KFileItem item = m_dolphinViewController->itemForIndex(index);
1038 if (!item.isNull()) {
1039 m_expandedUrls.insert(item.url());
1040 }
1041 }
1042
1043 void DolphinDetailsView::slotCollapsed(const QModelIndex& index)
1044 {
1045 KFileItem item = m_dolphinViewController->itemForIndex(index);
1046 if (!item.isNull()) {
1047 m_expandedUrls.remove(item.url());
1048 }
1049 }
1050
1051 void DolphinDetailsView::removeExpandedIndexes(const QModelIndex& parent, int start, int end)
1052 {
1053 if (m_expandedUrls.isEmpty()) {
1054 return;
1055 }
1056
1057 for (int row = start; row <= end; row++) {
1058 const QModelIndex index = model()->index(row, 0, parent);
1059 if (isExpanded(index)) {
1060 slotCollapsed(index);
1061 removeExpandedIndexes(index, 0, model()->rowCount(index) - 1);
1062 }
1063 }
1064 }
1065
1066 void DolphinDetailsView::updateDecorationSize(bool showPreview)
1067 {
1068 DetailsModeSettings* settings = DolphinSettings::instance().detailsModeSettings();
1069 const int iconSize = showPreview ? settings->previewSize() : settings->iconSize();
1070 setIconSize(QSize(iconSize, iconSize));
1071 m_decorationSize = QSize(iconSize, iconSize);
1072
1073 doItemsLayout();
1074 }
1075
1076 KFileItemDelegate::Information DolphinDetailsView::infoForColumn(int columnIndex) const
1077 {
1078 return AdditionalInfoAccessor::instance().keyForColumn(columnIndex);
1079 }
1080
1081 bool DolphinDetailsView::isAboveExpandingToggle(const QPoint& pos) const
1082 {
1083 // QTreeView offers no public API to get the information whether an index has an
1084 // expanding toggle and what boundaries the toggle has. The following approach
1085 // also assumes a toggle for file items.
1086 if (itemsExpandable()) {
1087 const QModelIndex index = QTreeView::indexAt(pos);
1088 if (index.isValid() && (index.column() == KDirModel::Name)) {
1089 QRect rect = visualRect(index);
1090 const int toggleSize = rect.height();
1091 if (isRightToLeft()) {
1092 rect.moveRight(rect.right());
1093 } else {
1094 rect.moveLeft(rect.x() - toggleSize);
1095 }
1096 rect.setWidth(toggleSize);
1097
1098 QStyleOption opt;
1099 opt.initFrom(this);
1100 opt.rect = rect;
1101 rect = style()->subElementRect(QStyle::SE_TreeViewDisclosureItem, &opt, this);
1102
1103 return rect.contains(pos);
1104 }
1105 }
1106 return false;
1107 }
1108
1109 void DolphinDetailsView::adjustMaximumSizeForEditing(const QModelIndex& index)
1110 {
1111 // Make sure that the full width of the "Name" column is available for "Rename Inline"
1112 m_extensionsFactory->fileItemDelegate()->setMaximumSize(QTreeView::visualRect(index).size());
1113 }
1114
1115 DolphinDetailsView::ElasticBand::ElasticBand() :
1116 show(false),
1117 origin(),
1118 destination(),
1119 lastSelectionOrigin(),
1120 lastSelectionDestination(),
1121 ignoreOldInfo(true),
1122 outsideNearestLeftEdge(0),
1123 outsideNearestRightEdge(0),
1124 insideNearestLeftEdge(0),
1125 insideNearestRightEdge(0)
1126 {
1127 }
1128
1129 #include "dolphindetailsview.moc"