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