]> cloud.milkyroute.net Git - dolphin.git/blob - src/views/dolphindetailsview.cpp
Use the size-hint provided by the item-delegate to calculate the required width of...
[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(completed()), 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 QHeaderView* headerView = header();
694 const int rowCount = model()->rowCount();
695 if (rowCount <= 0) {
696 headerView->resizeSection(KDirModel::Name, viewport()->width());
697 return;
698 }
699
700 // Using the resize mode QHeaderView::ResizeToContents is too slow (it takes
701 // around 3 seconds for each (!) resize operation when having > 10000 items).
702 // This gets a problem especially when opening large directories, where several
703 // resize operations are received for showing the currently available items during
704 // loading (the application hangs around 20 seconds when loading > 10000 items).
705
706 QFontMetrics fontMetrics(viewport()->font());
707
708 // Define the maximum number of rows, where an exact (but expensive) calculation
709 // of the widths is done.
710 const int maxRowCount = 200;
711
712 // Calculate the required with for each column and store it in columnWidth[]
713 int columnWidth[DolphinModel::ExtraColumnCount];
714
715 const QAbstractProxyModel* proxyModel = qobject_cast<const QAbstractProxyModel*>(model());
716 const KDirModel* dirModel = qobject_cast<const KDirModel*>(proxyModel->sourceModel());
717 for (int column = 0; column < DolphinModel::ExtraColumnCount; ++column) {
718 columnWidth[column] = 0;
719 if (!isColumnHidden(column)) {
720 // Calculate the required width for the current column and consider only
721 // up to maxRowCount columns for performance reasons
722 const int count = qMin(rowCount, maxRowCount);
723 const QStyleOptionViewItem option = viewOptions();
724 for (int row = 0; row < count; ++row) {
725 const QModelIndex index = dirModel->index(row, column);
726 const int width = itemDelegate()->sizeHint(option, index).width();
727 if (width > columnWidth[column]) {
728 columnWidth[column] = width;
729 }
730 }
731
732 // Assure that the required width is sufficient for the header too
733 const int logicalIndex = headerView->logicalIndex(column);
734 const QString headline = model()->headerData(logicalIndex, Qt::Horizontal).toString();
735 const int headlineWidth = fontMetrics.width(headline);
736
737 columnWidth[column] = qMax(columnWidth[column], headlineWidth);
738 }
739 }
740
741 // Resize all columns except of the name column
742 int requiredWidth = 0;
743 for (int column = KDirModel::Size; column < DolphinModel::ExtraColumnCount; ++column) {
744 if (!isColumnHidden(column)) {
745 requiredWidth += columnWidth[column];
746 headerView->resizeSection(column, columnWidth[column]);
747 }
748 }
749
750 // Resize the name column in a way that the whole available width is used
751 columnWidth[KDirModel::Name] = viewport()->width() - requiredWidth;
752
753 const int minNameWidth = 300;
754 if (columnWidth[KDirModel::Name] < minNameWidth) {
755 columnWidth[KDirModel::Name] = minNameWidth;
756
757 if ((rowCount > 0) && (rowCount < maxRowCount)) {
758 // Try to decrease the name column width without clipping any text
759 const int nameWidth = sizeHintForColumn(DolphinModel::Name);
760 if (nameWidth + requiredWidth <= viewport()->width()) {
761 columnWidth[KDirModel::Name] = viewport()->width() - requiredWidth;
762 } else if (nameWidth < minNameWidth) {
763 columnWidth[KDirModel::Name] = nameWidth;
764 }
765 }
766 }
767
768 headerView->resizeSection(KDirModel::Name, columnWidth[KDirModel::Name]);
769 }
770
771 void DolphinDetailsView::saveColumnPositions()
772 {
773 QList<int> columnPositions;
774 for (int i = DolphinModel::Name; i < DolphinModel::ExtraColumnCount; ++i) {
775 columnPositions.append(header()->visualIndex(i));
776 }
777
778 DetailsModeSettings* settings = DolphinSettings::instance().detailsModeSettings();
779 settings->setColumnPositions(columnPositions);
780 }
781
782 void DolphinDetailsView::slotHeaderSectionResized(int logicalIndex, int oldSize, int newSize)
783 {
784 Q_UNUSED(logicalIndex);
785 Q_UNUSED(oldSize);
786 Q_UNUSED(newSize);
787 // If the user changes the size of the headers, the autoresize feature should be
788 // turned off. As there is no dedicated interface to find out whether the header
789 // section has been resized by the user or by a resize event, another approach is used.
790 // Attention: Take care when changing the if-condition to verify that there is no
791 // regression in combination with bug 178630 (see fix in comment #8).
792 if ((QApplication::mouseButtons() & Qt::LeftButton) && header()->underMouse()) {
793 disableAutoResizing();
794 }
795
796 adjustMaximumSizeForEditing(currentIndex());
797 }
798
799 void DolphinDetailsView::slotActivationChanged(bool active)
800 {
801 setAlternatingRowColors(active);
802 }
803
804 void DolphinDetailsView::disableAutoResizing()
805 {
806 m_autoResize = false;
807 }
808
809 void DolphinDetailsView::requestActivation()
810 {
811 m_dolphinViewController->requestActivation();
812 }
813
814 void DolphinDetailsView::slotGlobalSettingsChanged(int category)
815 {
816 Q_UNUSED(category);
817
818 const DetailsModeSettings* settings = DolphinSettings::instance().detailsModeSettings();
819 Q_ASSERT(settings != 0);
820 if (settings->useSystemFont()) {
821 m_font = KGlobalSettings::generalFont();
822 }
823 //Disconnect then reconnect, since the settings have been changed, the connection requirements may have also.
824 disconnect(this, SIGNAL(clicked(QModelIndex)), m_dolphinViewController, SLOT(triggerItem(QModelIndex)));
825 disconnect(this, SIGNAL(doubleClicked(QModelIndex)), m_dolphinViewController, SLOT(triggerItem(QModelIndex)));
826 if (KGlobalSettings::singleClick()) {
827 connect(this, SIGNAL(clicked(QModelIndex)), m_dolphinViewController, SLOT(triggerItem(QModelIndex)));
828 } else {
829 connect(this, SIGNAL(doubleClicked(QModelIndex)), m_dolphinViewController, SLOT(triggerItem(QModelIndex)));
830 }
831 }
832
833 void DolphinDetailsView::updateElasticBandSelection()
834 {
835 if (!m_band.show) {
836 return;
837 }
838
839 // Ensure the elastic band itself is up-to-date, in
840 // case we are being called due to e.g. a drag event.
841 updateElasticBand();
842
843 // Clip horizontally to the name column, as some filenames will be
844 // longer than the column. We don't clip vertically as origin
845 // may be above or below the current viewport area.
846 const int nameColumnX = header()->sectionPosition(DolphinModel::Name);
847 const int nameColumnWidth = header()->sectionSize(DolphinModel::Name);
848 QRect selRect = elasticBandRect().normalized();
849 QRect nameColumnArea(nameColumnX, selRect.y(), nameColumnWidth, selRect.height());
850 selRect = nameColumnArea.intersect(selRect).normalized();
851 // Get the last elastic band rectangle, expressed in viewpoint coordinates.
852 const QPoint scrollPos(horizontalScrollBar()->value(), verticalScrollBar()->value());
853 QRect oldSelRect = QRect(m_band.lastSelectionOrigin - scrollPos, m_band.lastSelectionDestination - scrollPos).normalized();
854
855 if (selRect.isNull()) {
856 selectionModel()->select(m_band.originalSelection, QItemSelectionModel::ClearAndSelect);
857 m_band.ignoreOldInfo = true;
858 return;
859 }
860
861 if (!m_band.ignoreOldInfo) {
862 // Do some quick checks to see if we can rule out the need to
863 // update the selection.
864 Q_ASSERT(uniformRowHeights());
865 QModelIndex dummyIndex = model()->index(0, 0);
866 if (!dummyIndex.isValid()) {
867 // No items in the model presumably.
868 return;
869 }
870
871 // If the elastic band does not cover the same rows as before, we'll
872 // need to re-check, and also invalidate the old item distances.
873 const int rowHeight = QTreeView::rowHeight(dummyIndex);
874 const bool coveringSameRows =
875 (selRect.top() / rowHeight == oldSelRect.top() / rowHeight) &&
876 (selRect.bottom() / rowHeight == oldSelRect.bottom() / rowHeight);
877 if (coveringSameRows) {
878 // Covering the same rows, but have we moved far enough horizontally
879 // that we might have (de)selected some other items?
880 const bool itemSelectionChanged =
881 ((selRect.left() > oldSelRect.left()) &&
882 (selRect.left() > m_band.insideNearestLeftEdge)) ||
883 ((selRect.left() < oldSelRect.left()) &&
884 (selRect.left() <= m_band.outsideNearestLeftEdge)) ||
885 ((selRect.right() < oldSelRect.right()) &&
886 (selRect.left() >= m_band.insideNearestRightEdge)) ||
887 ((selRect.right() > oldSelRect.right()) &&
888 (selRect.right() >= m_band.outsideNearestRightEdge));
889
890 if (!itemSelectionChanged) {
891 return;
892 }
893 }
894 } else {
895 // This is the only piece of optimization data that needs to be explicitly
896 // discarded.
897 m_band.lastSelectionOrigin = QPoint();
898 m_band.lastSelectionDestination = QPoint();
899 oldSelRect = selRect;
900 }
901
902 // Do the selection from scratch. Force a update of the horizontal distances info.
903 m_band.insideNearestLeftEdge = nameColumnX + nameColumnWidth + 1;
904 m_band.insideNearestRightEdge = nameColumnX - 1;
905 m_band.outsideNearestLeftEdge = nameColumnX - 1;
906 m_band.outsideNearestRightEdge = nameColumnX + nameColumnWidth + 1;
907
908 // Include the old selection rect as well, so we can deselect
909 // items that were inside it but not in the new selRect.
910 const QRect boundingRect = selRect.united(oldSelRect).normalized();
911 if (boundingRect.isNull()) {
912 return;
913 }
914
915 // Get the index of the item in this row in the name column.
916 // TODO - would this still work if the columns could be re-ordered?
917 QModelIndex startIndex = QTreeView::indexAt(boundingRect.topLeft());
918 if (startIndex.parent().isValid()) {
919 startIndex = startIndex.parent().child(startIndex.row(), KDirModel::Name);
920 } else {
921 startIndex = model()->index(startIndex.row(), KDirModel::Name);
922 }
923 if (!startIndex.isValid()) {
924 selectionModel()->select(m_band.originalSelection, QItemSelectionModel::ClearAndSelect);
925 m_band.ignoreOldInfo = true;
926 return;
927 }
928
929 // Go through all indexes between the top and bottom of boundingRect, and
930 // update the selection.
931 const int verticalCutoff = boundingRect.bottom();
932 QModelIndex currIndex = startIndex;
933 QModelIndex lastIndex;
934 bool allItemsInBoundDone = false;
935
936 // Calling selectionModel()->select(...) for each item that needs to be
937 // toggled is slow as each call emits selectionChanged(...) so store them
938 // and do the selection toggle in one batch.
939 QItemSelection itemsToToggle;
940 // QItemSelection's deal with continuous ranges of indexes better than
941 // single indexes, so try to portion items that need to be toggled into ranges.
942 bool formingToggleIndexRange = false;
943 QModelIndex toggleIndexRangeBegin = QModelIndex();
944
945 do {
946 QRect currIndexRect = visualRect(currIndex);
947
948 // Update some optimization info as we go.
949 const int cr = currIndexRect.right();
950 const int cl = currIndexRect.left();
951 const int sl = selRect.left();
952 const int sr = selRect.right();
953 // "The right edge of the name is outside of the rect but nearer than m_outsideNearestLeft", etc
954 if ((cr < sl && cr > m_band.outsideNearestLeftEdge)) {
955 m_band.outsideNearestLeftEdge = cr;
956 }
957 if ((cl > sr && cl < m_band.outsideNearestRightEdge)) {
958 m_band.outsideNearestRightEdge = cl;
959 }
960 if ((cl >= sl && cl <= sr && cl > m_band.insideNearestRightEdge)) {
961 m_band.insideNearestRightEdge = cl;
962 }
963 if ((cr >= sl && cr <= sr && cr < m_band.insideNearestLeftEdge)) {
964 m_band.insideNearestLeftEdge = cr;
965 }
966
967 bool currentlySelected = selectionModel()->isSelected(currIndex);
968 bool originallySelected = m_band.originalSelection.contains(currIndex);
969 bool intersectsSelectedRect = currIndexRect.intersects(selRect);
970 bool shouldBeSelected = (intersectsSelectedRect && !originallySelected) || (!intersectsSelectedRect && originallySelected);
971 bool needToToggleItem = (currentlySelected && !shouldBeSelected) || (!currentlySelected && shouldBeSelected);
972 if (needToToggleItem && !formingToggleIndexRange) {
973 toggleIndexRangeBegin = currIndex;
974 formingToggleIndexRange = true;
975 }
976
977 // NOTE: indexBelow actually walks up and down expanded trees for us.
978 QModelIndex nextIndex = indexBelow(currIndex);
979 allItemsInBoundDone = !nextIndex.isValid() || currIndexRect.top() > verticalCutoff;
980
981 const bool commitToggleIndexRange = formingToggleIndexRange &&
982 (!needToToggleItem ||
983 allItemsInBoundDone ||
984 currIndex.parent() != toggleIndexRangeBegin.parent());
985 if (commitToggleIndexRange) {
986 formingToggleIndexRange = false;
987 // If this is the last item in the bounds and it is also the beginning of a range,
988 // don't toggle lastIndex - it will already have been dealt with.
989 if (!allItemsInBoundDone || toggleIndexRangeBegin != currIndex) {
990 itemsToToggle.select(toggleIndexRangeBegin, lastIndex);
991 }
992 // Need to start a new range immediately with currIndex?
993 if (needToToggleItem) {
994 toggleIndexRangeBegin = currIndex;
995 formingToggleIndexRange = true;
996 }
997 if (allItemsInBoundDone && needToToggleItem) {
998 // Toggle the very last item in the bounds.
999 itemsToToggle.select(currIndex, currIndex);
1000 }
1001 }
1002
1003 // next item
1004 lastIndex = currIndex;
1005 currIndex = nextIndex;
1006 } while (!allItemsInBoundDone);
1007
1008
1009 selectionModel()->select(itemsToToggle, QItemSelectionModel::Toggle);
1010
1011 m_band.lastSelectionOrigin = m_band.origin;
1012 m_band.lastSelectionDestination = m_band.destination;
1013 m_band.ignoreOldInfo = false;
1014 }
1015
1016 void DolphinDetailsView::setFoldersExpandable(bool expandable)
1017 {
1018 if (!expandable) {
1019 // collapse all expanded folders, as QTreeView::setItemsExpandable(false)
1020 // does not do this task
1021 const int rowCount = model()->rowCount();
1022 for (int row = 0; row < rowCount; ++row) {
1023 setExpanded(model()->index(row, 0), false);
1024 }
1025 }
1026 DetailsModeSettings* settings = DolphinSettings::instance().detailsModeSettings();
1027 settings->setExpandableFolders(expandable);
1028 setRootIsDecorated(expandable);
1029 setItemsExpandable(expandable);
1030
1031 // The width of the space which is available for editing has changed
1032 // because of the (dis)appearance of the expanding toggles
1033 adjustMaximumSizeForEditing(currentIndex());
1034 }
1035
1036 void DolphinDetailsView::slotExpanded(const QModelIndex& index)
1037 {
1038 KFileItem item = m_dolphinViewController->itemForIndex(index);
1039 if (!item.isNull()) {
1040 m_expandedUrls.insert(item.url());
1041 }
1042 }
1043
1044 void DolphinDetailsView::slotCollapsed(const QModelIndex& index)
1045 {
1046 KFileItem item = m_dolphinViewController->itemForIndex(index);
1047 if (!item.isNull()) {
1048 m_expandedUrls.remove(item.url());
1049 }
1050 }
1051
1052 void DolphinDetailsView::removeExpandedIndexes(const QModelIndex& parent, int start, int end)
1053 {
1054 if (m_expandedUrls.isEmpty()) {
1055 return;
1056 }
1057
1058 for (int row = start; row <= end; row++) {
1059 const QModelIndex index = model()->index(row, 0, parent);
1060 if (isExpanded(index)) {
1061 slotCollapsed(index);
1062 removeExpandedIndexes(index, 0, model()->rowCount(index) - 1);
1063 }
1064 }
1065 }
1066
1067 void DolphinDetailsView::updateDecorationSize(bool showPreview)
1068 {
1069 DetailsModeSettings* settings = DolphinSettings::instance().detailsModeSettings();
1070 const int iconSize = showPreview ? settings->previewSize() : settings->iconSize();
1071 setIconSize(QSize(iconSize, iconSize));
1072 m_decorationSize = QSize(iconSize, iconSize);
1073
1074 doItemsLayout();
1075 }
1076
1077 KFileItemDelegate::Information DolphinDetailsView::infoForColumn(int columnIndex) const
1078 {
1079 return AdditionalInfoAccessor::instance().keyForColumn(columnIndex);
1080 }
1081
1082 bool DolphinDetailsView::isAboveExpandingToggle(const QPoint& pos) const
1083 {
1084 // QTreeView offers no public API to get the information whether an index has an
1085 // expanding toggle and what boundaries the toggle has. The following approach
1086 // also assumes a toggle for file items.
1087 if (itemsExpandable()) {
1088 const QModelIndex index = QTreeView::indexAt(pos);
1089 if (index.isValid() && (index.column() == KDirModel::Name)) {
1090 QRect rect = visualRect(index);
1091 const int toggleSize = rect.height();
1092 if (isRightToLeft()) {
1093 rect.moveRight(rect.right());
1094 } else {
1095 rect.moveLeft(rect.x() - toggleSize);
1096 }
1097 rect.setWidth(toggleSize);
1098
1099 QStyleOption opt;
1100 opt.initFrom(this);
1101 opt.rect = rect;
1102 rect = style()->subElementRect(QStyle::SE_TreeViewDisclosureItem, &opt, this);
1103
1104 return rect.contains(pos);
1105 }
1106 }
1107 return false;
1108 }
1109
1110 void DolphinDetailsView::adjustMaximumSizeForEditing(const QModelIndex& index)
1111 {
1112 // Make sure that the full width of the "Name" column is available for "Rename Inline"
1113 m_extensionsFactory->fileItemDelegate()->setMaximumSize(QTreeView::visualRect(index).size());
1114 }
1115
1116 DolphinDetailsView::ElasticBand::ElasticBand() :
1117 show(false),
1118 origin(),
1119 destination(),
1120 lastSelectionOrigin(),
1121 lastSelectionDestination(),
1122 ignoreOldInfo(true),
1123 outsideNearestLeftEdge(0),
1124 outsideNearestRightEdge(0),
1125 insideNearestLeftEdge(0),
1126 insideNearestRightEdge(0)
1127 {
1128 }
1129
1130 #include "dolphindetailsview.moc"