1 /*
2 * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 *
18 */
19
20
21 #include "folderview.h"
22 #include "foldermodel.h"
23 #include <QHeaderView>
24 #include <QVBoxLayout>
25 #include <QContextMenuEvent>
26 #include "proxyfoldermodel.h"
27 #include "folderitemdelegate.h"
28 #include "dndactionmenu.h"
29 #include "filemenu.h"
30 #include "foldermenu.h"
31 #include "filelauncher.h"
32 #include "utilities.h"
33 #include <QTimer>
34 #include <QDate>
35 #include <QDebug>
36 #include <QClipboard>
37 #include <QMimeData>
38 #include <QHoverEvent>
39 #include <QApplication>
40 #include <QPainter>
41 #include <QScrollBar>
42 #include <QMetaType>
43 #include <QMessageBox>
44 #include <QLineEdit>
45 #include <QTextEdit>
46 #include <QWidgetAction> // for detailed list header context menu
47 #include <QLabel> // for detailed list header context menu
48 #include <QX11Info> // for XDS support
49 #include <xcb/xcb.h> // for XDS support
50 #include "xdndworkaround.h" // for XDS support
51 #include "folderview_p.h"
52 #include "utilities.h"
53
54 #include <algorithm>
55
56 #define SCROLL_FRAMES_PER_SEC 50
57 #define SCROLL_DURATION 300 // in ms
58
59 static const int scrollAnimFrames = SCROLL_FRAMES_PER_SEC * SCROLL_DURATION / 1000;
60
61 using namespace Fm;
62
FolderViewListView(QWidget * parent)63 FolderViewListView::FolderViewListView(QWidget* parent):
64 QListView(parent),
65 activationAllowed_(true),
66 cursorOnSelectionCorner_(false),
67 mouseLeftPressed_(false) {
68 connect(this, &QListView::activated, this, &FolderViewListView::activation);
69 // inline renaming
70 setEditTriggers(QAbstractItemView::NoEditTriggers);
71 setMouseTracking(true); // needed with selection corner icon
72
73 // for smooth scrolling (it is Qt's default)
74 setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
75
76
77 viewport()->setAcceptDrops(true);
78 /* If the list view is already visible, setMovement() will lay out items again with delay
79 (see Qt, QListView::setMovement(), d->doDelayedItemsLayout()) and thus drop events will
80 remain disabled for its viewport. So, we should call it here, before showing the view. */
81 setMovement(QListView::Static);
82 }
83
~FolderViewListView()84 FolderViewListView::~FolderViewListView() {
85 }
86
startDrag(Qt::DropActions supportedActions)87 void FolderViewListView::startDrag(Qt::DropActions supportedActions) {
88 if(movement() != Static) {
89 QListView::startDrag(supportedActions);
90 }
91 else {
92 QAbstractItemView::startDrag(supportedActions);
93 }
94 }
95
mousePressEvent(QMouseEvent * event)96 void FolderViewListView::mousePressEvent(QMouseEvent* event) {
97 if(event->buttons() == Qt::LeftButton) { // see FolderViewListView::mouseMoveEvent
98 mouseLeftPressed_ = true;
99 if(indexAt(event->pos()).isValid()) {
100 globalItemPressPoint_ = event->globalPos();
101 }
102 else {
103 globalItemPressPoint_ = QPoint();
104 }
105 }
106 // use the selection corner only with the extended and multiple selection modes
107 // and change the mode to multiple temporarily if it is extended
108 QAbstractItemView::SelectionMode sm = selectionMode();
109 bool cornerSelection(cursorOnSelectionCorner_ && event->button() == Qt::LeftButton);
110 if(sm == QAbstractItemView::ExtendedSelection && cornerSelection) {
111 setSelectionMode(QAbstractItemView::MultiSelection);
112 }
113 QListView::mousePressEvent(event);
114 if (sm == QAbstractItemView::ExtendedSelection) {
115 if(cornerSelection) {
116 setSelectionMode(sm); // restore the selection mode
117 }
118 else {
119 // NOTE: Qt sometimes does not respect the current item sorting with a Shift selection.
120 // That seems like a problem in QListView. As a workaround, the selection is sorted here.
121 if(QApplication::keyboardModifiers() & Qt::ShiftModifier) {
122 auto selModel = selectionModel();
123 auto sel = selModel->selection();
124 if(!sel.isEmpty()) {
125 std::sort(sel.begin(), sel.end(), [](QItemSelectionRange a, QItemSelectionRange b) {
126 return a.top() < b.top();
127 });
128 selModel->select(sel, QItemSelectionModel::SelectCurrent);
129 }
130 }
131 }
132 }
133 static_cast<FolderView*>(parent())->childMousePressEvent(event);
134 }
135
mouseMoveEvent(QMouseEvent * event)136 void FolderViewListView::mouseMoveEvent(QMouseEvent* event) {
137 if (event->buttons() == Qt::NoButton
138 // NOTE: Filter the BACK & FORWARD buttons to not Drag & Drop with them.
139 // (by default Qt views drag with any button)
140 || ((event->buttons() & ~(Qt::BackButton | Qt::ForwardButton))
141 && !(event->buttons() == Qt::LeftButton
142 // don't draw rubberband if left mouse button isn't pressed inside view
143 // (this event may have been sent by FolderView::scrollSmoothly)
144 && (!mouseLeftPressed_
145 // don't start drag if the cursor isn't moved since pressing left mouse button on an item
146 // (because the user may want to scroll the view with mouse wheel before dragging)
147 || (globalItemPressPoint_ - event->globalPos()).manhattanLength() <= QApplication::startDragDistance())))) {
148 bool cursorOnSelectionCorner = cursorOnSelectionCorner_;
149 QListView::mouseMoveEvent(event);
150 // update the index if the cursor enters/leaves the selection corner icon
151 if(cursorOnSelectionCorner != cursorOnSelectionCorner_ && event->buttons() == Qt::NoButton) {
152 update(indexAt(event->pos()));
153 }
154 }
155 }
156
indexAt(const QPoint & point) const157 QModelIndex FolderViewListView::indexAt(const QPoint& point) const {
158 QModelIndex index = QListView::indexAt(point);
159 bool isCursorPos(point == viewport()->mapFromGlobal(QCursor::pos()));
160 if(isCursorPos) {
161 cursorOnSelectionCorner_ = false;
162 }
163 // NOTE: QListView has a severe design flaw here. It does hit-testing based on the
164 // total bound rect of the item. The width of an item is determined by max(icon_width, text_width).
165 // So if the text label is much wider than the icon, when you click outside the icon but
166 // the point is still within the outer bound rect, the item is still selected.
167 // This results in very poor usability. Let's do precise hit-testing here.
168 // An item is hit only when the point is in the icon or text label.
169 // If the point is in the bound rectangle but outside the icon or text, it should not be selected.
170 if(viewMode() == QListView::IconMode && index.isValid()) {
171 QRect visRect = visualRect(index); // visible area on the screen
172 FolderItemDelegate* delegate = static_cast<FolderItemDelegate*>(itemDelegateForColumn(FolderModel::ColumnFileName));
173 QSize margins = delegate->getMargins();
174 QSize _iconSize = iconSize();
175 int iconXMargin = (visRect.width() - _iconSize.width()) / 2;
176 int iconLeft = visRect.left() + iconXMargin;
177 int iconTop = visRect.top() + margins.height();
178 // the selection (hover) corner is a rectangle near the top left corner of
179 // the icon and outside it as far as possible, so that its width and height
180 // are 1/3 of the icon size >= 48 px (see FolderItemDelegate::paint)
181 if(isCursorPos && _iconSize.width() >= 48
182 && (selectionMode() == QAbstractItemView::ExtendedSelection
183 || selectionMode() == QAbstractItemView::MultiSelection)) {
184 int s = _iconSize.width() / 3;
185 int icnLeft = qMax(visRect.left(), iconLeft - s);
186 int icnTop = qMax(visRect.top(), iconTop - s);
187 if(point.x() >= icnLeft && point.x() <= icnLeft + s
188 && point.y() >= icnTop && point.y() <= icnTop + s) {
189 cursorOnSelectionCorner_ = true;
190 return index;
191 }
192 }
193 if(point.y() < iconTop) { // above icon
194 return QModelIndex();
195 }
196 else if(point.y() < visRect.top() + margins.height() + _iconSize.height()) { // on the icon area
197 if(point.x() < iconLeft || point.x() > (visRect.right() + 1 - iconXMargin)) {
198 // to the left or right of the icon
199 return QModelIndex();
200 }
201 }
202 else {
203 QSize _textSize = delegate->iconViewTextSize(index);
204 int textHMargin = (visRect.width() - _textSize.width()) / 2;
205 if(point.y() > visRect.top() + margins.height() + _iconSize.height() + _textSize.height() // below text
206 // on the text area but to the left or right of the text
207 || point.x() < visRect.left() + textHMargin || point.x() > visRect.right() + 1 - textHMargin) {
208 return QModelIndex();
209 }
210 }
211 // qDebug() << "visualRect: " << visRect << "point:" << point;
212 }
213 return index;
214 }
215
216
217 // NOTE:
218 // QListView has a problem which I consider a bug or a design flaw.
219 // When you set movement property to Static, theoratically the icons
220 // should not be movable. However, if you turned on icon mode,
221 // the icons becomes freely movable despite the value of movement is Static.
222 // To overcome this bug, we override all drag handling methods, and
223 // call QAbstractItemView directly, bypassing QListView.
224 // In this way, we can workaround the buggy behavior.
225 // The drag handlers of QListView basically does the same things
226 // as its parent QAbstractItemView, but it also stores the currently
227 // dragged item and paint them in the view as needed.
228 // TODO: I really should file a bug report to Qt developers.
229
dragEnterEvent(QDragEnterEvent * event)230 void FolderViewListView::dragEnterEvent(QDragEnterEvent* event) {
231 QAbstractItemView::dragEnterEvent(event);
232 //qDebug("dragEnterEvent");
233 //static_cast<FolderView*>(parent())->childDragEnterEvent(event);
234 }
235
dragLeaveEvent(QDragLeaveEvent * e)236 void FolderViewListView::dragLeaveEvent(QDragLeaveEvent* e) {
237 QAbstractItemView::dragLeaveEvent(e);
238 static_cast<FolderView*>(parent())->childDragLeaveEvent(e);
239 }
240
dragMoveEvent(QDragMoveEvent * e)241 void FolderViewListView::dragMoveEvent(QDragMoveEvent* e) {
242 QAbstractItemView::dragMoveEvent(e);
243 static_cast<FolderView*>(parent())->childDragMoveEvent(e);
244 }
245
dropEvent(QDropEvent * e)246 void FolderViewListView::dropEvent(QDropEvent* e) {
247 static_cast<FolderView*>(parent())->childDropEvent(e);
248 QAbstractItemView::dropEvent(e);
249 }
250
mouseReleaseEvent(QMouseEvent * event)251 void FolderViewListView::mouseReleaseEvent(QMouseEvent* event) {
252 // NOTE: With mouseReleaseEvent, event->buttons() excludes the button that caused the event
253 // and so, it should not be used here. Instead, event->button() is used.
254
255 bool activationWasAllowed = activationAllowed_;
256 if(!style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this)
257 || event->button() != Qt::LeftButton
258 // no activation with mouse when the cursor is on the selection corner
259 || cursorOnSelectionCorner_) {
260 activationAllowed_ = false;
261 }
262
263 QListView::mouseReleaseEvent(event);
264
265 activationAllowed_ = activationWasAllowed;
266 if(event->button() == Qt::LeftButton) {
267 mouseLeftPressed_ = false;
268 }
269 }
270
mouseDoubleClickEvent(QMouseEvent * event)271 void FolderViewListView::mouseDoubleClickEvent(QMouseEvent* event) {
272 bool activationWasAllowed = activationAllowed_;
273 if(style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this)
274 || event->button() != Qt::LeftButton
275 // no activation with mouse when the cursor is on the selection corner
276 || cursorOnSelectionCorner_) {
277 activationAllowed_ = false;
278 }
279
280 QListView::mouseDoubleClickEvent(event);
281
282 activationAllowed_ = activationWasAllowed;
283 }
284
moveCursor(CursorAction cursorAction,Qt::KeyboardModifiers modifiers)285 QModelIndex FolderViewListView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) {
286 QAbstractItemModel* model_ = model();
287
288 if(model_ && currentIndex().isValid()) {
289 FolderView::ViewMode viewMode = static_cast<FolderView*>(parent())->viewMode();
290 if((viewMode == FolderView::IconMode) || (viewMode == FolderView::ThumbnailMode)) {
291 int next = (layoutDirection() == Qt::RightToLeft) ? - 1 : 1;
292
293 if(cursorAction == QAbstractItemView::MoveRight) {
294 return model_->index(currentIndex().row() + next, 0);
295 }
296 else if(cursorAction == QAbstractItemView::MoveLeft) {
297 return model_->index(currentIndex().row() - next, 0);
298 }
299 }
300 }
301
302 return QListView::moveCursor(cursorAction, modifiers);
303 }
304
currentChanged(const QModelIndex & current,const QModelIndex & previous)305 void FolderViewListView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) {
306 QListView::currentChanged(current, previous);
307 if(viewMode() == QListView::ListMode && current.isValid()) {
308 // QListView has a bug that may reset the horizontal scrollbar
309 // in the list mode when the current item changes. This is a workaround.
310 QTimer::singleShot(0, this, [this] {
311 if(currentIndex().isValid()) {
312 scrollTo(currentIndex());
313 }
314 });
315 }
316 }
317
activation(const QModelIndex & index)318 void FolderViewListView::activation(const QModelIndex& index) {
319 if(activationAllowed_) {
320 Q_EMIT activatedFiltered(index);
321 }
322 }
323
selectAll()324 void FolderViewListView::selectAll() {
325 // NOTE: By default QListView::selectAll() selects all columns in the model.
326 // However, QListView only show the first column. Normal selection by mouse
327 // can only select the first column of every row. I consider this discripancy yet
328 // another design flaw of Qt. To make them consistent, we do it ourselves by only
329 // selecting the first column of every row and do not select all columns as Qt does.
330 // I'll report a Qt bug for this later.
331 if(QAbstractItemModel* model_ = model()) {
332 const QItemSelection sel{model_->index(0, 0), model_->index(model_->rowCount() - 1, 0)};
333 selectionModel()->select(sel, QItemSelectionModel::Select);
334 }
335 }
336
337 //-----------------------------------------------------------------------------
338
FolderViewTreeView(QWidget * parent)339 FolderViewTreeView::FolderViewTreeView(QWidget* parent):
340 QTreeView(parent),
341 doingLayout_(false),
342 layoutTimer_(nullptr),
343 activationAllowed_(true),
344 ctrlDragSelectionFlag_(QItemSelectionModel::NoUpdate) {
345
346 header()->setSectionResizeMode(QHeaderView::Interactive);
347 header()->setStretchLastSection(true);
348
349 // get the new width if the section is resized by user
350 connect(header(), &QHeaderView::sectionResized, [this](int logicalIndex, int/* oldSize*/, int newSize) {
351 if(doingLayout_ || customColumnWidths_.isEmpty()) {
352 return;
353 }
354 int vIndx = header()->visualIndex(logicalIndex);
355 if(vIndx >= 0 && vIndx < customColumnWidths_.size()) {
356 customColumnWidths_[vIndx] = newSize;
357 Q_EMIT columnResizedByUser(vIndx, newSize);
358 queueLayoutColumns();
359 }
360 });
361
362 // header context menu for configuring its resizing and hidden sections
363 header()->setContextMenuPolicy(Qt::CustomContextMenu);
364 connect(header(), &QWidget::customContextMenuRequested, this, &FolderViewTreeView::headerContextMenu);
365
366 setIndentation(0);
367 // the default true value may cause a crash on entering a folder by double clicking (a Qt bug?)
368 setExpandsOnDoubleClick(false);
369
370 connect(this, &QTreeView::activated, this, &FolderViewTreeView::activation);
371 // don't open editor on double clicking
372 setEditTriggers(QAbstractItemView::NoEditTriggers);
373 }
374
~FolderViewTreeView()375 FolderViewTreeView::~FolderViewTreeView() {
376 if(layoutTimer_) {
377 delete layoutTimer_;
378 }
379 }
380
setCustomColumnWidths(const QList<int> & widths)381 void FolderViewTreeView::setCustomColumnWidths(const QList<int> &widths) {
382 // enables cutomizable widths if "widths" is not empty; otherwise, enables auto-resizing
383 if(customColumnWidths_ == widths) {
384 return;
385 }
386 customColumnWidths_.clear();
387 customColumnWidths_ = widths;
388 header()->setStretchLastSection(widths.isEmpty());
389 queueLayoutColumns();
390 if(widths.isEmpty()) {
391 Q_EMIT autoResizeEnabled();
392 }
393 }
394
setHiddenColumns(const QSet<int> & columns)395 void FolderViewTreeView::setHiddenColumns(const QSet<int> &columns) {
396 if(hiddenColumns_ == columns) {
397 return;
398 }
399 hiddenColumns_.clear();
400 hiddenColumns_ = columns;
401 queueLayoutColumns();
402 }
403
headerContextMenu(const QPoint & p)404 void FolderViewTreeView::headerContextMenu(const QPoint &p) {
405 QMenu menu;
406 QAction *action = menu.addAction (tr("Auto-resize columns"));
407 action->setCheckable(true);
408 action->setChecked(customColumnWidths_.isEmpty());
409 connect(action, &QAction::triggered, action, [this] (bool checked) {
410 QList<int> widths;
411 if(!checked) {
412 for(int column = 0; column < FolderModel::NumOfColumns; ++column) {
413 widths << 0;
414 }
415 // one signal is enough to make a raw FolderView::customColumnWidths_
416 Q_EMIT columnResizedByUser(0, 0);
417 }
418 setCustomColumnWidths(widths);
419 });
420 if(model()) {
421 menu.addSeparator();
422 QWidgetAction *labelAction = new QWidgetAction(&menu);
423 QLabel *label = new QLabel(QStringLiteral("<center><b>") + tr("Visible Columns") + QStringLiteral("</b></center>"));
424 labelAction->setDefaultWidget(label);
425 menu.addAction (labelAction);
426
427 int filenameColumn = header()->visualIndex(FolderModel::ColumnFileName);
428 int dTimeColumn = header()->visualIndex(FolderModel::ColumnFileDTime);
429 bool isTrash = false;
430 if(ProxyFolderModel* proxyModel = qobject_cast<ProxyFolderModel*>(model())) {
431 if(auto model = static_cast<FolderModel*>(proxyModel->sourceModel())) {
432 if(model->path() && strcmp(model->path().toString().get(), "trash:///") == 0) {
433 isTrash = true;
434 }
435 }
436 }
437 int numCols = header()->count();
438 for(int column = 0; column < numCols; ++column) {
439 int columnId = header()->logicalIndex(column);
440 if(!isTrash && columnId == dTimeColumn) {
441 // no action for the deletion time column if this isn't trash
442 continue;
443 }
444 if(columnId >= 0 && columnId < FolderModel::NumOfColumns) {
445 action = menu.addAction (model()->headerData(columnId, Qt::Horizontal, Qt::DisplayRole).toString());
446 action->setCheckable(true);
447 if(columnId == filenameColumn) { // never hide the name column
448 action->setChecked(true);
449 action->setDisabled(true);
450 }
451 else {
452 action->setChecked(!header()->isSectionHidden(columnId));
453 connect(action, &QAction::triggered, action, [this, column] (bool checked) {
454 if(checked) {
455 hiddenColumns_.remove(column);
456 }
457 else {
458 hiddenColumns_ << column;
459 }
460 Q_EMIT columnHiddenByUser(column, !checked);
461 queueLayoutColumns();
462 });
463 }
464 }
465 }
466 }
467 menu.exec(header()->mapToGlobal(p));
468 }
469
setModel(QAbstractItemModel * model)470 void FolderViewTreeView::setModel(QAbstractItemModel* model) {
471 QTreeView::setModel(model);
472 layoutColumns();
473 if(ProxyFolderModel* proxyModel = qobject_cast<ProxyFolderModel*>(model)) {
474 connect(proxyModel, &ProxyFolderModel::sortFilterChanged, this, &FolderViewTreeView::onSortFilterChanged,
475 Qt::UniqueConnection);
476 onSortFilterChanged();
477 }
478 }
479
paintEvent(QPaintEvent * event)480 void FolderViewTreeView::paintEvent(QPaintEvent * event) {
481 QTreeView::paintEvent(event);
482 if(rubberBandRect_.isValid()) { // draw rubberband
483 QPainter p(viewport());
484 QStyleOptionRubberBand opt;
485 opt.initFrom(this);
486 opt.shape = QRubberBand::Rectangle;
487 opt.opaque = false;
488 QRect r = rubberBandRect_.adjusted(-horizontalOffset(), -verticalOffset(),
489 -horizontalOffset(), -verticalOffset())
490 .intersected(viewport()->rect()
491 .adjusted(-16, -16, 16, 16));
492 opt.rect = r;
493 style()->drawControl(QStyle::CE_RubberBand, &opt, &p);
494 }
495 }
496
mousePressEvent(QMouseEvent * event)497 void FolderViewTreeView::mousePressEvent(QMouseEvent* event) {
498 if(event->buttons() == Qt::LeftButton) { // see FolderViewTreeView::mouseMoveEvent
499 globalItemPressPoint_ = event->globalPos();
500 }
501 if(selectionMode() == QAbstractItemView::ExtendedSelection) {
502 // remember mouse press position and determine whether selections should be kept
503 // or removed later, when the cursor moves
504 QAbstractItemView::mousePressEvent(event);
505 mousePressPoint_ = event->pos() + QPoint(horizontalOffset(), verticalOffset());
506 QModelIndex index = indexAt(event->pos());
507 if(index.isValid()) {
508 Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers();
509 const bool shiftKeyPressed = modifiers & Qt::ShiftModifier;
510 const bool controlKeyPressed = modifiers & Qt::ControlModifier;
511 const bool rightButtonPressed = event->button() & Qt::RightButton;
512 const bool indexIsSelected = selectionModel()->isSelected(index);
513 if(controlKeyPressed && !shiftKeyPressed && !rightButtonPressed) {
514 ctrlDragSelectionFlag_ = indexIsSelected ? QItemSelectionModel::Deselect : QItemSelectionModel::Select;
515 }
516 }
517 }
518 else {
519 QTreeView::mousePressEvent(event);
520 }
521
522 static_cast<FolderView*>(parent())->childMousePressEvent(event);
523 }
524
mouseMoveEvent(QMouseEvent * event)525 void FolderViewTreeView::mouseMoveEvent(QMouseEvent* event) {
526 // NOTE: Filter the BACK & FORWARD buttons to not Drag & Drop with them.
527 // (by default Qt views drag with any button)
528 if(event->buttons() == Qt::NoButton || (event->buttons() & ~(Qt::BackButton | Qt::ForwardButton))) {
529 // handle rubberband
530 if(selectionMode() == QAbstractItemView::ExtendedSelection
531 && (event->buttons() & Qt::LeftButton)
532 && (rubberBandRect_.isValid()
533 || !indexAt(mousePressPoint_ - QPoint(horizontalOffset(), verticalOffset())).isValid())) {
534 QAbstractItemView::mouseMoveEvent(event);
535
536 // set rubberband rectangle
537 QRect rect(mousePressPoint_, event->pos() + QPoint(horizontalOffset(), verticalOffset()));
538 rect = rect.normalized();
539 QRect r = rect.united(rubberBandRect_);
540 viewport()->update(r.adjusted(-horizontalOffset(), -verticalOffset(),
541 -horizontalOffset(), -verticalOffset()));
542 rubberBandRect_ = rect;
543
544 // set state and selection
545 setState(QAbstractItemView::DragSelectingState);
546 Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers();
547 QItemSelectionModel::SelectionFlags command;
548 if(modifiers & Qt::ControlModifier) {
549 command = QItemSelectionModel::ToggleCurrent;
550 }
551 else if(modifiers & Qt::ShiftModifier) {
552 command = QItemSelectionModel::SelectCurrent;
553 }
554 else {
555 command = QItemSelectionModel::Clear|QItemSelectionModel::SelectCurrent;
556 }
557 command |= QItemSelectionModel::Rows;
558 if(ctrlDragSelectionFlag_ != QItemSelectionModel::NoUpdate && command.testFlag(QItemSelectionModel::Toggle)) {
559 command &= ~QItemSelectionModel::Toggle;
560 command |= ctrlDragSelectionFlag_;
561 }
562 QRect selectionRect = QRect(rubberBandRect_.topLeft(), rubberBandRect_.bottomRight());
563 setSelection(selectionRect, command);
564 }
565 else {
566 // don't start drag if the cursor isn't moved since pressing left mouse button on an item
567 // because the user may want to scroll the view with mouse wheel before dragging
568 if(!(event->buttons() == Qt::LeftButton
569 && (globalItemPressPoint_ - event->globalPos()).manhattanLength() <= QApplication::startDragDistance())) {
570 QTreeView::mouseMoveEvent(event);
571 }
572 }
573 }
574 }
575
setSelection(const QRect & rect,QItemSelectionModel::SelectionFlags command)576 void FolderViewTreeView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command) {
577 if(selectionMode() == QAbstractItemView::ExtendedSelection
578 && model() && state() == QAbstractItemView::DragSelectingState
579 && rubberBandRect_.isValid()) { // rubberband selection
580 QRect r = rubberBandRect_.adjusted(-horizontalOffset(), -verticalOffset(),
581 -horizontalOffset(), -verticalOffset());
582 r.setLeft(qMax(0, r.left()));
583 r.setTop(qMax(-verticalOffset(), r.top()));
584 QModelIndex top = indexAt(r.topLeft());
585 QItemSelection selection;
586 if(top.isValid()) {
587 top = top.sibling(top.row(), 0);
588 if(top.isValid()) {
589 QModelIndex bottom = indexAt(r.bottomLeft());
590 if(!bottom.isValid()) {
591 bottom = model()->index(model()->rowCount() - 1, 0);
592 }
593 if(bottom.isValid()) {
594 selection = QItemSelection(top, bottom);
595 }
596 }
597 }
598 selectionModel()->select(selection, command | QItemSelectionModel::Rows);
599 }
600 else {
601 QTreeView::setSelection(rect, command);
602 }
603 }
604
dragEnterEvent(QDragEnterEvent * event)605 void FolderViewTreeView::dragEnterEvent(QDragEnterEvent* event) {
606 QTreeView::dragEnterEvent(event);
607 //static_cast<FolderView*>(parent())->childDragEnterEvent(event);
608 }
609
dragLeaveEvent(QDragLeaveEvent * e)610 void FolderViewTreeView::dragLeaveEvent(QDragLeaveEvent* e) {
611 QTreeView::dragLeaveEvent(e);
612 static_cast<FolderView*>(parent())->childDragLeaveEvent(e);
613 }
614
dragMoveEvent(QDragMoveEvent * e)615 void FolderViewTreeView::dragMoveEvent(QDragMoveEvent* e) {
616 QTreeView::dragMoveEvent(e);
617 static_cast<FolderView*>(parent())->childDragMoveEvent(e);
618 }
619
dropEvent(QDropEvent * e)620 void FolderViewTreeView::dropEvent(QDropEvent* e) {
621 static_cast<FolderView*>(parent())->childDropEvent(e);
622 QTreeView::dropEvent(e);
623 }
624
625 // the default list mode of QListView handles column widths
626 // very badly (worse than gtk+) and it's not very flexible.
627 // so, let's handle column widths outselves.
layoutColumns()628 void FolderViewTreeView::layoutColumns() {
629 // qDebug("layoutColumns");
630 if(!model()) {
631 return;
632 }
633 doingLayout_ = true;
634 QHeaderView* headerView = header();
635 // the width that's available for showing the columns.
636 int availWidth = viewport()->contentsRect().width();
637
638 // get the width that every column want
639 int numCols = headerView->count();
640 if(numCols > 0) {
641 int desiredWidth = 0;
642 int* widths = new int[numCols]; // array to store the widths every column needs
643 QStyleOptionHeader opt;
644 opt.initFrom(headerView);
645 opt.fontMetrics = QFontMetrics(font());
646 if (headerView->isSortIndicatorShown()) {
647 opt.sortIndicator = QStyleOptionHeader::SortDown;
648 }
649 QAbstractItemModel* model_ = model();
650 int filenameColumn = headerView->visualIndex(FolderModel::ColumnFileName);
651 int dTimeColumn = header()->visualIndex(FolderModel::ColumnFileDTime);
652 bool isTrash = false;
653 if(ProxyFolderModel* proxyModel = qobject_cast<ProxyFolderModel*>(model())) {
654 if(auto model = static_cast<FolderModel*>(proxyModel->sourceModel())) {
655 if(model->path() && strcmp(model->path().toString().get(), "trash:///") == 0) {
656 isTrash = true;
657 }
658 }
659 }
660 int column;
661 for(column = 0; column < numCols; ++column) {
662 int columnId = headerView->logicalIndex(column);
663
664 if(!isTrash && columnId == dTimeColumn) {
665 // hide the deletion time column if this isn't trash
666 headerView->setSectionHidden(columnId, true);
667 continue;
668 }
669
670 // handle hidden columns
671 bool wasHidden = false;
672 if(headerView->isSectionHidden(columnId)) {
673 if(!hiddenColumns_.contains(columnId)) {
674 headerView->setSectionHidden(columnId, false);
675 wasHidden = true;
676 }
677 else {
678 continue;
679 }
680 }
681 else if(hiddenColumns_.contains(columnId)
682 && columnId != filenameColumn) { // never hide the name column
683 headerView->setSectionHidden(columnId, true);
684 continue;
685 }
686
687 if(customColumnWidths_.size() > column) {
688 // see FolderView::setCustomColumnWidths for the meaning of custom width <= 0
689 if(customColumnWidths_.at(column) > 0) {
690 widths[column] = qMax(customColumnWidths_.at(column), headerView->minimumSectionSize());
691 }
692 else {
693 if(wasHidden) {
694 // WARNING: When a section is shown in the interactive mode, Qt gives
695 // a huge width to it. As a workaround, the width is set to the minimum here.
696 customColumnWidths_[column] = widths[column] = headerView->minimumSectionSize();
697 }
698 else {
699 customColumnWidths_[column] = widths[column] = headerView->sectionSize(columnId);
700 }
701 Q_EMIT columnResizedByUser(column, customColumnWidths_.at(column));
702 }
703 }
704 else {
705 // get the size that the column needs
706 if(model_) {
707 QVariant data = model_->headerData(columnId, Qt::Horizontal, Qt::DisplayRole);
708 if(data.isValid()) {
709 opt.text = data.isValid() ? data.toString() : QString();
710 }
711 }
712 opt.section = columnId;
713 widths[column] = qMax(sizeHintForColumn(columnId),
714 style()->sizeFromContents(QStyle::CT_HeaderSection, &opt, QSize(),
715 headerView).width());
716 }
717 // compute the total width needed
718 desiredWidth += widths[column];
719 }
720
721 if(customColumnWidths_.size() <= filenameColumn) { // practically means no custom width
722 // if the total witdh we want exceeds the available space
723 if(desiredWidth > availWidth) {
724 // Compute the width available for the filename column
725 int filenameAvailWidth = availWidth - desiredWidth + widths[filenameColumn];
726
727 // Compute the minimum acceptable width for the filename column
728 int filenameMinWidth = qMin(200, sizeHintForColumn(filenameColumn));
729
730 if(filenameAvailWidth > filenameMinWidth) {
731 // Shrink the filename column to the available width
732 widths[filenameColumn] = filenameAvailWidth;
733 }
734 else {
735 // Set the filename column to its minimum width
736 widths[filenameColumn] = filenameMinWidth;
737 }
738 }
739 else {
740 // Fill the extra available space with the filename column
741 widths[filenameColumn] += availWidth - desiredWidth;
742 }
743 }
744
745 // really do the resizing for every column
746 for(int column = 0; column < numCols; ++column) {
747 headerView->resizeSection(headerView->logicalIndex(column), widths[column]);
748 }
749 delete []widths;
750 }
751 doingLayout_ = false;
752
753 if(layoutTimer_) {
754 delete layoutTimer_;
755 layoutTimer_ = nullptr;
756 }
757 setUpdatesEnabled(true);
758 }
759
resizeEvent(QResizeEvent * event)760 void FolderViewTreeView::resizeEvent(QResizeEvent* event) {
761 QAbstractItemView::resizeEvent(event);
762 // prevent endless recursion.
763 // When manually resizing columns, at the point where a horizontal scroll
764 // bar has to be inserted or removed, the vertical size changes, a resize
765 // event occurs and the column headers are flickering badly if the column
766 // layout is modified at this point. Therefore only layout the columns if
767 // the horizontal size changes.
768 if(!doingLayout_ && event->size().width() != event->oldSize().width()) {
769 layoutColumns(); // layoutColumns() also triggers resizeEvent
770 }
771 }
772
rowsInserted(const QModelIndex & parent,int start,int end)773 void FolderViewTreeView::rowsInserted(const QModelIndex& parent, int start, int end) {
774 setUpdatesEnabled(false); // prevent header text flickering
775 queueLayoutColumns();
776 QTreeView::rowsInserted(parent, start, end);
777 }
778
rowsAboutToBeRemoved(const QModelIndex & parent,int start,int end)779 void FolderViewTreeView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) {
780 QTreeView::rowsAboutToBeRemoved(parent, start, end);
781 queueLayoutColumns();
782 }
783
dataChanged(const QModelIndex & topLeft,const QModelIndex & bottomRight,const QVector<int> & roles)784 void FolderViewTreeView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector<int>& roles /*= QVector<int>{}*/) {
785 QTreeView::dataChanged(topLeft, bottomRight, roles);
786 // FIXME: this will be very inefficient
787 // queueLayoutColumns();
788 }
789
reset()790 void FolderViewTreeView::reset() {
791 // Sometimes when the content of the model is radically changed, Qt does reset()
792 // on the model rather than doing large amount of insertion and deletion.
793 // This is for performance reason so in this case rowsInserted() and rowsAboutToBeRemoved()
794 // might not be called. Hence we also have to re-layout the columns when the model is reset.
795 // This fixes bug #190
796 // https://github.com/lxqt/pcmanfm-qt/issues/190
797 setUpdatesEnabled(false); // prevent header text flickering
798 queueLayoutColumns();
799 QTreeView::reset();
800 }
801
queueLayoutColumns()802 void FolderViewTreeView::queueLayoutColumns() {
803 // qDebug("queueLayoutColumns");
804 if(!layoutTimer_) {
805 layoutTimer_ = new QTimer();
806 layoutTimer_->setSingleShot(true);
807 layoutTimer_->setInterval(0);
808 connect(layoutTimer_, &QTimer::timeout, this, &FolderViewTreeView::layoutColumns);
809 }
810 layoutTimer_->start();
811 }
812
mouseReleaseEvent(QMouseEvent * event)813 void FolderViewTreeView::mouseReleaseEvent(QMouseEvent* event) {
814 bool activationWasAllowed = activationAllowed_;
815 if((!style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this)) || (event->button() != Qt::LeftButton)) {
816 activationAllowed_ = false;
817 }
818
819 if(selectionMode() == QAbstractItemView::ExtendedSelection) {
820 QAbstractItemView::mouseReleaseEvent(event);
821 viewport()->update(rubberBandRect_.adjusted(-horizontalOffset(), -verticalOffset(),
822 -horizontalOffset(), -verticalOffset()));
823 rubberBandRect_ = QRect();
824 ctrlDragSelectionFlag_ = QItemSelectionModel::NoUpdate;
825 }
826 else {
827 QTreeView::mouseReleaseEvent(event);
828 }
829
830 activationAllowed_ = activationWasAllowed;
831
832 }
833
mouseDoubleClickEvent(QMouseEvent * event)834 void FolderViewTreeView::mouseDoubleClickEvent(QMouseEvent* event) {
835 bool activationWasAllowed = activationAllowed_;
836 if((style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this)) || (event->button() != Qt::LeftButton)) {
837 activationAllowed_ = false;
838 }
839
840 QTreeView::mouseDoubleClickEvent(event);
841
842 activationAllowed_ = activationWasAllowed;
843 }
844
activation(const QModelIndex & index)845 void FolderViewTreeView::activation(const QModelIndex& index) {
846 if(activationAllowed_) {
847 Q_EMIT activatedFiltered(index);
848 }
849 }
850
onSortFilterChanged()851 void FolderViewTreeView::onSortFilterChanged() {
852 if(QSortFilterProxyModel* proxyModel = qobject_cast<QSortFilterProxyModel*>(model())) {
853 header()->setSortIndicatorShown(true);
854 header()->setSortIndicator(proxyModel->sortColumn(), proxyModel->sortOrder());
855 if(!isSortingEnabled()) {
856 setSortingEnabled(true);
857 }
858 }
859 }
860
861
862 //-----------------------------------------------------------------------------
863
FolderView(FolderView::ViewMode _mode,QWidget * parent)864 FolderView::FolderView(FolderView::ViewMode _mode, QWidget *parent):
865 QWidget(parent),
866 view(nullptr),
867 model_(nullptr),
868 mode((ViewMode)0),
869 fileLauncher_(nullptr),
870 autoSelectionDelay_(600),
871 autoSelectionTimer_(nullptr),
872 selChangedTimer_(nullptr),
873 itemDelegateMargins_(QSize(3, 3)),
874 shadowHidden_(false),
875 scrollPerPixel_(true),
876 ctrlRightClick_(false),
877 smoothScrollTimer_(nullptr) {
878
879 iconSize_[IconMode - FirstViewMode] = QSize(48, 48);
880 iconSize_[CompactMode - FirstViewMode] = QSize(24, 24);
881 iconSize_[ThumbnailMode - FirstViewMode] = QSize(128, 128);
882 iconSize_[DetailedListMode - FirstViewMode] = QSize(24, 24);
883
884 QVBoxLayout* layout = new QVBoxLayout();
885 layout->setMargin(0);
886 setLayout(layout);
887
888 setViewMode(_mode);
889 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
890
891 connect(this, &FolderView::clicked, this, &FolderView::onFileClicked);
892 }
893
~FolderView()894 FolderView::~FolderView() {
895 if(smoothScrollTimer_) {
896 disconnect(smoothScrollTimer_, &QTimer::timeout, this, &FolderView::scrollSmoothly);
897 smoothScrollTimer_->stop();
898 delete smoothScrollTimer_;
899 }
900 }
901
setCustomColumnWidths(const QList<int> & widths)902 void FolderView::setCustomColumnWidths(const QList<int> &widths) {
903 customColumnWidths_.clear();
904 customColumnWidths_ = widths;
905 // Complete the widths list with zeros if needed. A value of <= 0 means that
906 // the initial custom width of the column should be set to its current width.
907 if(!customColumnWidths_.isEmpty()) {
908 for(int i = customColumnWidths_.size(); i < FolderModel::NumOfColumns; ++i) {
909 customColumnWidths_ << 0;
910 }
911 }
912 // resize header sections to custom widths if the tree view exists
913 if(mode == DetailedListMode) {
914 if(FolderViewTreeView* treeView = static_cast<FolderViewTreeView*>(view)) {
915 treeView->setCustomColumnWidths(customColumnWidths_);
916 }
917 }
918 }
919
setHiddenColumns(const QList<int> & columns)920 void FolderView::setHiddenColumns(const QList<int> &columns) {
921 hiddenColumns_.clear();
922 hiddenColumns_ = QSet<int>(columns.begin(), columns.end());
923 if(mode == DetailedListMode) {
924 if(FolderViewTreeView* treeView = static_cast<FolderViewTreeView*>(view)) {
925 treeView->setHiddenColumns(hiddenColumns_);
926 }
927 }
928 }
929
setScrollPerPixel(bool perPixel)930 void FolderView::setScrollPerPixel(bool perPixel) {
931 if(scrollPerPixel_ == perPixel) {
932 return;
933 }
934 scrollPerPixel_ = perPixel;
935 // icon and thumbnail modes scroll per pixel by default
936 if(mode == DetailedListMode) {
937 if(FolderViewTreeView* treeView = static_cast<FolderViewTreeView*>(view)) {
938 treeView->setVerticalScrollMode(scrollPerPixel_ ? QAbstractItemView::ScrollPerPixel
939 : QAbstractItemView::ScrollPerItem);
940 }
941 }
942 else if(mode == CompactMode) {
943 if(FolderViewListView* listView = static_cast<FolderViewListView*>(view)) {
944 listView->setHorizontalScrollMode(scrollPerPixel_ ? QAbstractItemView::ScrollPerPixel
945 : QAbstractItemView::ScrollPerItem);
946 }
947 }
948 }
949
onItemActivated(QModelIndex index)950 void FolderView::onItemActivated(QModelIndex index) {
951 if(QApplication::keyboardModifiers() & (Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier)) {
952 return;
953 }
954 if(QItemSelectionModel* selModel = selectionModel()) {
955 QVariant data;
956 if(index.isValid() && selModel->isSelected(index)) { // activate index only if it is selected
957 if(index.model()) {
958 data = index.model()->data(index, FolderModel::FileInfoRole);
959 }
960 }
961 else { // if index is not valid or selected, activate the first selected index
962 QModelIndexList selIndexes = mode == DetailedListMode ? selectedRows() : selectedIndexes();
963 if(!selIndexes.isEmpty()) {
964 QModelIndex first = selIndexes.first();
965 if(first.model()) {
966 data = first.model()->data(first, FolderModel::FileInfoRole);
967 }
968 }
969 }
970 if(data.isValid()) {
971 auto info = data.value<std::shared_ptr<const Fm::FileInfo>>();
972 if(info) {
973 Q_EMIT clicked(ActivatedClick, info);
974 }
975 }
976 }
977 }
978
onSelChangedTimeout()979 void FolderView::onSelChangedTimeout() {
980 selChangedTimer_->deleteLater();
981 selChangedTimer_ = nullptr;
982 // qDebug()<<"selected:" << nSel;
983 Q_EMIT selChanged();
984 }
985
onSelectionChanged(const QItemSelection &,const QItemSelection &)986 void FolderView::onSelectionChanged(const QItemSelection& /*selected*/, const QItemSelection& /*deselected*/) {
987 // It's possible that the selected items change too often and this slot gets called for thousands of times.
988 // For example, when you select thousands of files and delete them, we will get one selectionChanged() event
989 // for every deleted file. So, we use a timer to delay the handling to avoid too frequent updates of the UI.
990 if(!selChangedTimer_) {
991 selChangedTimer_ = new QTimer(this);
992 selChangedTimer_->setSingleShot(true);
993 connect(selChangedTimer_, &QTimer::timeout, this, &FolderView::onSelChangedTimeout);
994 selChangedTimer_->start(200);
995 }
996 }
997
onClosingEditor(QWidget * editor,QAbstractItemDelegate::EndEditHint hint)998 void FolderView::onClosingEditor(QWidget* editor, QAbstractItemDelegate::EndEditHint hint) {
999 if (hint != QAbstractItemDelegate::NoHint) {
1000 // we set the hint to NoHint in FolderItemDelegate::eventFilter()
1001 return;
1002 }
1003 QString newName;
1004 if (qobject_cast<QTextEdit*>(editor)) { // icon and thumbnail view
1005 newName = qobject_cast<QTextEdit*>(editor)->toPlainText();
1006 }
1007 else if (qobject_cast<QLineEdit*>(editor)) { // compact view
1008 newName = qobject_cast<QLineEdit*>(editor)->text();
1009 }
1010 if (newName.isEmpty()) {
1011 return;
1012 }
1013 // the editor will be deleted by QAbstractItemDelegate::destroyEditor() when no longer needed
1014
1015 QModelIndex index = view->selectionModel()->currentIndex();
1016 if(index.isValid() && index.model()) {
1017 QVariant data = index.model()->data(index, FolderModel::FileInfoRole);
1018 auto info = data.value<std::shared_ptr<const Fm::FileInfo>>();
1019 if (info) {
1020 // NOTE: "Edit name" is used to handle invalid filename encoding.
1021 auto oldName = QString::fromUtf8(g_file_info_get_edit_name(info->gFileInfo().get()));
1022 if(oldName.isEmpty()) {
1023 oldName = QString::fromStdString(info->name());
1024 }
1025 if(newName == oldName) {
1026 return;
1027 }
1028 QWidget* parent = window();
1029 if (window() == this) { // supposedly desktop, in case it uses this
1030 parent = nullptr;
1031 }
1032 if(changeFileName(info->path(), newName, parent)) {
1033 Q_EMIT inlineRenamed(oldName, newName);
1034 }
1035 }
1036 }
1037 }
1038
setViewMode(ViewMode _mode)1039 void FolderView::setViewMode(ViewMode _mode) {
1040 if(_mode == mode) { // if it's the same more, ignore
1041 return;
1042 }
1043 // disabling of smooth scrolling is only for list and compact modes
1044 if(!scrollPerPixel_ && smoothScrollTimer_ != nullptr
1045 && (_mode == DetailedListMode || _mode == CompactMode)) {
1046 disconnect(smoothScrollTimer_, &QTimer::timeout, this, &FolderView::scrollSmoothly);
1047 smoothScrollTimer_->stop();
1048 delete smoothScrollTimer_;
1049 smoothScrollTimer_ = nullptr;
1050 queuedScrollSteps_.clear(); // also forget the remaining steps
1051 }
1052 // FIXME: retain old selection
1053
1054 // since only detailed list mode uses QTreeView, and others
1055 // all use QListView, it's wise to preserve QListView when possible.
1056 bool recreateView = false;
1057 if(view && (mode == DetailedListMode || _mode == DetailedListMode)) {
1058 delete view; // FIXME: no virtual dtor?
1059 view = nullptr;
1060 recreateView = true;
1061 }
1062 mode = _mode;
1063 QSize iconSize = iconSize_[mode - FirstViewMode];
1064
1065 FolderItemDelegate* delegate = nullptr;
1066 if(mode == DetailedListMode) {
1067 FolderViewTreeView* treeView = new FolderViewTreeView(this);
1068 if(scrollPerPixel_) {
1069 treeView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
1070 }
1071 treeView->setCustomColumnWidths(customColumnWidths_);
1072 treeView->setHiddenColumns(hiddenColumns_);
1073 treeView->setAlternatingRowColors(true);
1074 connect(treeView, &FolderViewTreeView::activatedFiltered, this, &FolderView::onItemActivated);
1075 // update the list of custom widhts when the user changes it
1076 connect(treeView, &FolderViewTreeView::columnResizedByUser, [this](int visualIndex, int newWidth) {
1077 if(visualIndex >= 0) {
1078 if(visualIndex < customColumnWidths_.size()){
1079 customColumnWidths_[visualIndex] = newWidth;
1080 }
1081 else {
1082 customColumnWidths_ << newWidth;
1083 }
1084 // complete the widths list with zeros if needed
1085 for(int i = customColumnWidths_.size(); i < FolderModel::NumOfColumns; ++i) {
1086 customColumnWidths_ << 0;
1087 }
1088 Q_EMIT columnResizedByUser();
1089 }
1090 });
1091 connect(treeView, &FolderViewTreeView::autoResizeEnabled, [this]() {
1092 customColumnWidths_.clear();
1093 Q_EMIT columnResizedByUser();
1094 });
1095 // update the list of hidden columns when the user changes it
1096 connect(treeView, &FolderViewTreeView::columnHiddenByUser, [this](int visibleIndex, bool hidden) {
1097 if(hidden) {
1098 hiddenColumns_ << visibleIndex;
1099 }
1100 else {
1101 hiddenColumns_.remove(visibleIndex);
1102 }
1103 Q_EMIT columnHiddenByUser();
1104 });
1105 setFocusProxy(treeView);
1106
1107 view = treeView;
1108 treeView->setItemsExpandable(false);
1109 treeView->setRootIsDecorated(false);
1110 treeView->setAllColumnsShowFocus(false);
1111
1112 // set our own custom delegate
1113 delegate = new FolderItemDelegate(treeView);
1114 delegate->setShadowHidden(shadowHidden_);
1115 treeView->setItemDelegateForColumn(FolderModel::ColumnFileName, delegate);
1116 }
1117 else {
1118 FolderViewListView* listView;
1119 if(view) {
1120 listView = static_cast<FolderViewListView*>(view);
1121 }
1122 else {
1123 listView = new FolderViewListView(this);
1124 connect(listView, &FolderViewListView::activatedFiltered, this, &FolderView::onItemActivated);
1125 view = listView;
1126 }
1127 if(scrollPerPixel_ && mode == CompactMode) {
1128 listView->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
1129 }
1130 setFocusProxy(listView);
1131
1132 // set our own custom delegate
1133 delegate = new FolderItemDelegate(listView);
1134 delegate->setShadowHidden(shadowHidden_);
1135 listView->setItemDelegateForColumn(FolderModel::ColumnFileName, delegate);
1136 listView->setResizeMode(QListView::Adjust);
1137 listView->setWrapping(true);
1138 switch(mode) {
1139 case IconMode: {
1140 listView->setViewMode(QListView::IconMode);
1141 listView->setWordWrap(true);
1142 listView->setFlow(QListView::LeftToRight);
1143 break;
1144 }
1145 case CompactMode: {
1146 listView->setViewMode(QListView::ListMode);
1147 listView->setWordWrap(false);
1148 listView->setFlow(QListView::QListView::TopToBottom);
1149 break;
1150 }
1151 case ThumbnailMode: {
1152 listView->setViewMode(QListView::IconMode);
1153 listView->setWordWrap(true);
1154 listView->setFlow(QListView::LeftToRight);
1155 break;
1156 }
1157 default:
1158 ;
1159 }
1160 updateGridSize();
1161 }
1162 if(view) {
1163 // we have to install the event filter on the viewport instead of the view itself.
1164 view->viewport()->installEventFilter(this);
1165 // we want the QEvent::HoverMove event for single click + auto-selection support
1166 view->viewport()->setAttribute(Qt::WA_Hover, true);
1167 view->setContextMenuPolicy(Qt::NoContextMenu); // defer the context menu handling to parent widgets
1168 view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1169 view->setIconSize(iconSize);
1170
1171 view->setSelectionMode(QAbstractItemView::ExtendedSelection);
1172 layout()->addWidget(view);
1173
1174 // enable dnd (the drop indicator is set at "FolderView::childDragMoveEvent()")
1175 view->setDragEnabled(true);
1176 view->setAcceptDrops(true);
1177 view->setDragDropMode(QAbstractItemView::DragDrop);
1178
1179 // inline renaming
1180 connect(delegate, &QAbstractItemDelegate::closeEditor, this, &FolderView::onClosingEditor);
1181
1182 if(model_) {
1183 // FIXME: preserve selections
1184 model_->setThumbnailSize(iconSize.width());
1185 view->setModel(model_);
1186 if(recreateView) {
1187 connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FolderView::onSelectionChanged);
1188 }
1189 }
1190 }
1191 }
1192
1193 // set proper grid size for the QListView based on current view mode, icon size, and font size.
updateGridSize()1194 void FolderView::updateGridSize() {
1195 if(mode == DetailedListMode || !view) {
1196 return;
1197 }
1198 FolderViewListView* listView = static_cast<FolderViewListView*>(view);
1199 QSize icon = iconSize(mode); // size of the icon
1200 QFontMetrics fm = fontMetrics(); // size of current font
1201 QSize grid; // the final grid size
1202 switch(mode) {
1203 case IconMode:
1204 case ThumbnailMode: {
1205 // NOTE by PCMan about finding the optimal text label size:
1206 // The average filename length on my root filesystem is roughly 18-20 chars.
1207 // So, a reasonable size for the text label is about 10 chars each line since string of this length
1208 // can be shown in two lines. If you consider word wrap, then the result is around 10 chars per word.
1209 // In average, 10 char per line should be enough to display a "word" in the filename without breaking.
1210 // The values can be estimated with this command:
1211 // > find / | xargs basename -a | sed -e s'/[_-]/ /g' | wc -mcw
1212 // However, this average only applies to English. For some Asian characters, such as Chinese chars,
1213 // each char actually takes doubled space. To be safe, we use 13 chars per line x average char width
1214 // to get a nearly optimal width for the text label. As most of the filenames have less than 40 chars
1215 // 13 chars x 3 lines should be enough to show the full filenames for most files.
1216 int textWidth = fm.averageCharWidth() * 13;
1217 int textHeight = fm.lineSpacing() * 3;
1218 grid.setWidth(qMax(icon.width(), textWidth) + 4); // a margin of 2 px for selection rects
1219 grid.setHeight(icon.height() + textHeight + 4); // a margin of 2 px for selection rects
1220 // grow to include margins
1221 grid += 2*itemDelegateMargins_;
1222 // let horizontal and vertical spacings be set only by itemDelegateMargins_
1223 listView->setSpacing(0);
1224
1225 break;
1226 }
1227 default:
1228 // FIXME: set proper item size
1229 listView->setSpacing(2);
1230 ; // do not use grid size
1231 }
1232
1233 FolderItemDelegate* delegate = static_cast<FolderItemDelegate*>(listView->itemDelegateForColumn(FolderModel::ColumnFileName));
1234 delegate->setItemSize(grid);
1235 delegate->setIconSize(icon);
1236 delegate->setMargins(itemDelegateMargins_);
1237 }
1238
setIconSize(ViewMode mode,QSize size)1239 void FolderView::setIconSize(ViewMode mode, QSize size) {
1240 Q_ASSERT(mode >= FirstViewMode && mode <= LastViewMode);
1241 iconSize_[mode - FirstViewMode] = size;
1242 if(viewMode() == mode) {
1243 view->setIconSize(size);
1244 if(model_) {
1245 model_->setThumbnailSize(size.width());
1246 }
1247 updateGridSize();
1248 }
1249 }
1250
iconSize(ViewMode mode) const1251 QSize FolderView::iconSize(ViewMode mode) const {
1252 Q_ASSERT(mode >= FirstViewMode && mode <= LastViewMode);
1253 return iconSize_[mode - FirstViewMode];
1254 }
1255
setMargins(QSize size)1256 void FolderView::setMargins(QSize size) {
1257 if(itemDelegateMargins_ != size.expandedTo(QSize(0, 0))) {
1258 itemDelegateMargins_ = size.expandedTo(QSize(0, 0));
1259 updateGridSize();
1260 }
1261 }
1262
setShadowHidden(bool shadowHidden)1263 void FolderView::setShadowHidden(bool shadowHidden) {
1264 if(view && shadowHidden != shadowHidden_) {
1265 shadowHidden_ = shadowHidden;
1266 FolderItemDelegate* delegate = nullptr;
1267 if(mode == DetailedListMode) {
1268 FolderViewTreeView* treeView = static_cast<FolderViewTreeView*>(view);
1269 delegate = static_cast<FolderItemDelegate*>(treeView->itemDelegateForColumn(FolderModel::ColumnFileName));
1270 }
1271 else {
1272 FolderViewListView* listView = static_cast<FolderViewListView*>(view);
1273 delegate = static_cast<FolderItemDelegate*>(listView->itemDelegateForColumn(FolderModel::ColumnFileName));
1274 }
1275 if(delegate) {
1276 delegate->setShadowHidden(shadowHidden);
1277 }
1278 }
1279 }
1280
setCtrlRightClick(bool ctrlRightClick)1281 void FolderView::setCtrlRightClick(bool ctrlRightClick) {
1282 ctrlRightClick_ = ctrlRightClick;
1283 }
1284
viewMode() const1285 FolderView::ViewMode FolderView::viewMode() const {
1286 return mode;
1287 }
1288
setAutoSelectionDelay(int delay)1289 void FolderView::setAutoSelectionDelay(int delay) {
1290 autoSelectionDelay_ = delay;
1291 if(autoSelectionDelay_ <= 0 && autoSelectionTimer_) {
1292 autoSelectionTimer_->stop();
1293 delete autoSelectionTimer_;
1294 autoSelectionTimer_ = nullptr;
1295 }
1296 }
1297
childView() const1298 QAbstractItemView* FolderView::childView() const {
1299 return view;
1300 }
1301
model() const1302 ProxyFolderModel* FolderView::model() const {
1303 return model_;
1304 }
1305
setModel(ProxyFolderModel * model)1306 void FolderView::setModel(ProxyFolderModel* model) {
1307 if(view) {
1308 view->setModel(model);
1309 QSize iconSize = iconSize_[mode - FirstViewMode];
1310 model->setThumbnailSize(iconSize.width());
1311 if(view->selectionModel()) {
1312 connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FolderView::onSelectionChanged);
1313 }
1314 }
1315 if(model_) {
1316 delete model_;
1317 }
1318 model_ = model;
1319 }
1320
event(QEvent * event)1321 bool FolderView::event(QEvent* event) {
1322 switch(event->type()) {
1323 case QEvent::StyleChange:
1324 break;
1325 case QEvent::FontChange:
1326 updateGridSize();
1327 break;
1328 case QEvent::KeyPress:
1329 // Pressing Enter activates only the current index. With no current index,
1330 // we activate the first selected index on pressing Enter (see onItemActivated).
1331 if(view && !view->selectionModel()->currentIndex().isValid()) {
1332 int k = static_cast<QKeyEvent*>(event)->key();
1333 if(k == Qt::Key_Return || k == Qt::Key_Enter) {
1334 onItemActivated(QModelIndex());
1335 }
1336 }
1337 break;
1338 default:
1339 break;
1340 }
1341 return QWidget::event(event);
1342 }
1343
contextMenuEvent(QContextMenuEvent * event)1344 void FolderView::contextMenuEvent(QContextMenuEvent* event) {
1345 QWidget::contextMenuEvent(event);
1346 QPoint pos = event->pos();
1347 QPoint view_pos = view->mapFromParent(pos);
1348 QPoint viewport_pos = view->viewport()->mapFromParent(view_pos);
1349 emitClickedAt(ContextMenuClick, viewport_pos);
1350 }
1351
childMousePressEvent(QMouseEvent * event)1352 void FolderView::childMousePressEvent(QMouseEvent* event) {
1353 // called from mousePressEvent() of child view
1354 Qt::MouseButton button = event->button();
1355 if(button == Qt::MiddleButton) {
1356 emitClickedAt(MiddleClick, event->pos());
1357 }
1358 else if(button == Qt::BackButton) {
1359 Q_EMIT clickedBack();
1360 }
1361 else if(button == Qt::ForwardButton) {
1362 Q_EMIT clickedForward();
1363 }
1364 }
1365
emitClickedAt(ClickType type,const QPoint & pos)1366 void FolderView::emitClickedAt(ClickType type, const QPoint& pos) {
1367 // indexAt() needs a point in "viewport" coordinates.
1368 QModelIndex index = view->indexAt(pos);
1369 if(index.isValid()
1370 && (!ctrlRightClick_ || QApplication::keyboardModifiers() != Qt::ControlModifier)) {
1371 QVariant data = index.data(FolderModel::FileInfoRole);
1372 auto info = data.value<std::shared_ptr<const Fm::FileInfo>>();
1373 Q_EMIT clicked(type, info);
1374 }
1375 else {
1376 // FIXME: should we show popup menu for the selected files instead
1377 // if there are selected files?
1378 if(type == ContextMenuClick) {
1379 // clear current selection if clicked outside selected files
1380 view->clearSelection();
1381 Q_EMIT clicked(type, nullptr);
1382 }
1383 }
1384 }
1385
selectedRows(int column) const1386 QModelIndexList FolderView::selectedRows(int column) const {
1387 QItemSelectionModel* selModel = selectionModel();
1388 if(selModel) {
1389 return selModel->selectedRows(column);
1390 }
1391 return QModelIndexList();
1392 }
1393
1394 // This returns all selected "cells", which means all cells of the same row are returned.
selectedIndexes() const1395 QModelIndexList FolderView::selectedIndexes() const {
1396 QItemSelectionModel* selModel = selectionModel();
1397 if(selModel) {
1398 return selModel->selectedIndexes();
1399 }
1400 return QModelIndexList();
1401 }
1402
selectionModel() const1403 QItemSelectionModel* FolderView::selectionModel() const {
1404 return view ? view->selectionModel() : nullptr;
1405 }
1406
selectedFilePaths() const1407 Fm::FilePathList FolderView::selectedFilePaths() const {
1408 if(model_) {
1409 QModelIndexList selIndexes = mode == DetailedListMode ? selectedRows() : selectedIndexes();
1410 if(!selIndexes.isEmpty()) {
1411 Fm::FilePathList paths;
1412 QModelIndexList::const_iterator it;
1413 for(it = selIndexes.constBegin(); it != selIndexes.constEnd(); ++it) {
1414 auto file = model_->fileInfoFromIndex(*it);
1415 paths.push_back(file->path());
1416 }
1417 return paths;
1418 }
1419 }
1420 return Fm::FilePathList();
1421 }
1422
hasSelection() const1423 bool FolderView::hasSelection() const {
1424 QItemSelectionModel* selModel = selectionModel();
1425 return selModel ? selModel->hasSelection() : false;
1426 }
1427
indexFromFolderPath(const Fm::FilePath & folderPath) const1428 QModelIndex FolderView::indexFromFolderPath(const Fm::FilePath& folderPath) const {
1429 if(!model_ || !folderPath.isValid()) {
1430 return QModelIndex();
1431 }
1432 QModelIndex index;
1433 int count = model_->rowCount();
1434 for(int row = 0; row < count; ++row) {
1435 index = model_->index(row, 0);
1436 auto info = model_->fileInfoFromIndex(index);
1437 if(info && info->isDir() && folderPath == info->path()) {
1438 return index;
1439 }
1440 }
1441 return QModelIndex();
1442 }
1443
selectFiles(const Fm::FileInfoList & files,bool add)1444 void FolderView::selectFiles(const Fm::FileInfoList& files, bool add) {
1445 if(!model_ || files.empty()) {
1446 return;
1447 }
1448 QModelIndex index, firstIndex;
1449 int count = model_->rowCount();
1450 Fm::FileInfoList list = files;
1451 bool singleFile(files.size() == 1);
1452 QItemSelectionModel::SelectionFlags flags = QItemSelectionModel::Select;
1453 if(mode == DetailedListMode) {
1454 flags |= QItemSelectionModel::Rows;
1455 }
1456 for(int row = 0; row < count; ++row) {
1457 if (list.empty()) {
1458 break;
1459 }
1460 index = model_->index(row, 0);
1461 auto info = model_->fileInfoFromIndex(index);
1462 for(auto it = list.cbegin(); it != list.cend(); ++it) {
1463 auto& item = *it;
1464 if(item == info) {
1465 if(model_->showHidden() || !info->isHidden()) {
1466 if (!firstIndex.isValid()) {
1467 firstIndex = index;
1468 if(!add) {
1469 selectionModel()->clear();
1470 }
1471 }
1472 selectionModel()->select(index, flags);
1473 }
1474 list.erase(it);
1475 break;
1476 }
1477 }
1478 }
1479 if (firstIndex.isValid()) {
1480 view->scrollTo(firstIndex, QAbstractItemView::EnsureVisible);
1481 if (singleFile) { // give focus to the single file
1482 selectionModel()->setCurrentIndex(firstIndex, QItemSelectionModel::Current);
1483 }
1484 }
1485 }
1486
selectedFiles() const1487 Fm::FileInfoList FolderView::selectedFiles() const {
1488 if(model_) {
1489 QModelIndexList selIndexes = mode == DetailedListMode ? selectedRows() : selectedIndexes();
1490 if(!selIndexes.isEmpty()) {
1491 Fm::FileInfoList files;
1492 QModelIndexList::const_iterator it;
1493 for(it = selIndexes.constBegin(); it != selIndexes.constEnd(); ++it) {
1494 auto file = model_->fileInfoFromIndex(*it);
1495 files.push_back(file);
1496 }
1497 return files;
1498 }
1499 }
1500 return Fm::FileInfoList();
1501 }
1502
selectAll()1503 void FolderView::selectAll() {
1504 view->selectAll();
1505 }
1506
invertSelection()1507 void FolderView::invertSelection() {
1508 if(model_) {
1509 QItemSelectionModel* selModel = view->selectionModel();
1510 QItemSelectionModel::SelectionFlags flags;
1511 if(mode == DetailedListMode) {
1512 flags |= QItemSelectionModel::Rows;
1513 }
1514 // we don't use a "for" loop on rows because it would be slow
1515 const QItemSelection _all{model_->index(0, 0), model_->index(model_->rowCount() - 1, 0)};
1516 const QItemSelection _old{selModel->selection()};
1517
1518 selModel->select(_all, flags | QItemSelectionModel::Select);
1519 selModel->select(_old, flags | QItemSelectionModel::Deselect);
1520 }
1521 }
1522
childDragEnterEvent(QDragEnterEvent * event)1523 void FolderView::childDragEnterEvent(QDragEnterEvent* event) {
1524 //qDebug("drag enter");
1525 if(event->mimeData()->hasFormat(QStringLiteral("text/uri-list"))) {
1526 event->accept();
1527 }
1528 else {
1529 event->ignore();
1530 }
1531 }
1532
childDragLeaveEvent(QDragLeaveEvent * e)1533 void FolderView::childDragLeaveEvent(QDragLeaveEvent* e) {
1534 //qDebug("drag leave");
1535 e->accept();
1536 }
1537
childDragMoveEvent(QDragMoveEvent * e)1538 void FolderView::childDragMoveEvent(QDragMoveEvent* e) {
1539 // Since it isn't possible to drop on a file (see "FolderModel::dropMimeData()"),
1540 // we enable the drop indicator only when the cursor is on a folder.
1541 QModelIndex index = view->indexAt(e->pos());
1542 if(index.isValid() && index.model()) {
1543 QVariant data = index.model()->data(index, FolderModel::FileInfoRole);
1544 auto info = data.value<std::shared_ptr<const Fm::FileInfo>>();
1545 if(info && !info->isDir()) {
1546 view->setDropIndicatorShown(false);
1547 return;
1548 }
1549 }
1550 view->setDropIndicatorShown(true);
1551 }
1552
childDropEvent(QDropEvent * e)1553 void FolderView::childDropEvent(QDropEvent* e) {
1554 // qDebug("drop");
1555 // Try to support XDS
1556 // NOTE: in theory, it's not possible to implement XDS with pure Qt.
1557 // We achieved this with some dirty XCB/XDND workarounds.
1558 // Please refer to XdndWorkaround::clientMessage() in xdndworkaround.cpp for details.
1559 if(QX11Info::isPlatformX11() && e->mimeData()->hasFormat(QStringLiteral("XdndDirectSave0"))) {
1560 e->setDropAction(Qt::CopyAction);
1561 const QWidget* targetWidget = childView()->viewport();
1562 // these are dynamic QObject property set by our XDND workarounds in xdndworkaround.cpp.
1563 xcb_window_t dndSource = xcb_window_t(targetWidget->property("xdnd::lastDragSource").toUInt());
1564 //xcb_timestamp_t dropTimestamp = (xcb_timestamp_t)targetWidget->property("xdnd::lastDropTime").toUInt();
1565 // qDebug() << "XDS: source window" << dndSource << dropTimestamp;
1566 if(dndSource != 0) {
1567 xcb_atom_t XdndDirectSaveAtom = XdndWorkaround::internAtom("XdndDirectSave0", 15);
1568 xcb_atom_t textAtom = XdndWorkaround::internAtom("text/plain", 10);
1569
1570 // 1. get the filename from XdndDirectSave property of the source window
1571 QByteArray basename = XdndWorkaround::windowProperty(dndSource, XdndDirectSaveAtom, textAtom, 1024);
1572
1573 // 2. construct the fill URI for the file, and update the source window property.
1574 Fm::FilePath filePath;
1575 if(model_) {
1576 QModelIndex index = view->indexAt(e->pos());
1577 auto info = model_->fileInfoFromIndex(index);
1578 if(info && info->isDir()) {
1579 filePath = info->path().child(basename.constData());
1580 }
1581 }
1582 if(!filePath.isValid()) {
1583 filePath = path().child(basename.constData());
1584 }
1585 QByteArray fileUri = filePath.uri().get();
1586 XdndWorkaround::setWindowProperty(dndSource, XdndDirectSaveAtom, textAtom, (void*)fileUri.constData(), fileUri.length());
1587
1588 // 3. send to XDS selection data request with type "XdndDirectSave" to the source window and
1589 // receive result from the source window. (S: success, E: error, or F: failure)
1590 QByteArray result = e->mimeData()->data(QStringLiteral("XdndDirectSave0"));
1591 // NOTE: there seems to be some bugs in file-roller so it always replies with "E" even if the
1592 // file extraction is finished successfully. Anyways, we ignore any error at the moment.
1593 }
1594 e->accept(); // yeah! we've done with XDS so stop Qt from further event propagation.
1595 return;
1596 }
1597
1598 if(e->keyboardModifiers() == Qt::NoModifier) {
1599 // if no key modifiers are used, popup a menu
1600 // to ask the user for the action he/she wants to perform.
1601 Qt::DropActions actions = Qt::IgnoreAction;
1602 std::shared_ptr<const Fm::FileInfo> info = nullptr;
1603 if(model_) {
1604 QModelIndex index = view->indexAt(e->pos());
1605 info = model_->fileInfoFromIndex(index);
1606 }
1607 if(!info || !info->isDir()) {
1608 info = folderInfo();
1609 }
1610 if(info && info->isWritableDirectory() && info->isWritable()) {
1611 actions = e->possibleActions();
1612 }
1613 Qt::DropAction action = DndActionMenu::askUser(actions, QCursor::pos());
1614 e->setDropAction(action);
1615 }
1616 }
1617
eventFilter(QObject * watched,QEvent * event)1618 bool FolderView::eventFilter(QObject* watched, QEvent* event) {
1619 // NOTE: Instead of simply filtering the drag and drop events of the child view in
1620 // the event filter, we overrided each event handler virtual methods in
1621 // both QListView and QTreeView and added some childXXXEvent() callbacks.
1622 // We did this because of a design flaw of Qt.
1623 // All QAbstractScrollArea derived widgets, including QAbstractItemView
1624 // contains an internal child widget, which is called a viewport.
1625 // The events actually comes from the child viewport, not the parent view itself.
1626 // Qt redirects the events of viewport to the viewportEvent() method of
1627 // QAbstractScrollArea and let the parent widget handle the events.
1628 // Qt implemented this using a event filter installed on the child viewport widget.
1629 // That means, when we try to install an event filter on the viewport,
1630 // there is already a filter installed by Qt which will be called before ours.
1631 // So we can never intercept the event handling of QAbstractItemView by using a filter.
1632 // That's why we override respective virtual methods for different events.
1633 if(view && watched == view->viewport()) {
1634 switch(event->type()) {
1635 case QEvent::HoverMove:
1636 case QEvent::HoverEnter:
1637 // activate items on single click
1638 if(style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick)) {
1639 QHoverEvent* hoverEvent = static_cast<QHoverEvent*>(event);
1640 QModelIndex index = view->indexAt(hoverEvent->pos()); // find out the hovered item
1641 if(index.isValid()) { // change the cursor to a hand when hovering on an item
1642 setCursor(Qt::PointingHandCursor);
1643 }
1644 else {
1645 setCursor(Qt::ArrowCursor);
1646 }
1647 // turn on auto-selection for hovered item when single click mode is used.
1648 if(autoSelectionDelay_ > 0 && model_) {
1649 if(!autoSelectionTimer_) {
1650 autoSelectionTimer_ = new QTimer(this);
1651 connect(autoSelectionTimer_, &QTimer::timeout, this, &FolderView::onAutoSelectionTimeout);
1652 lastAutoSelectionIndex_ = QModelIndex();
1653 }
1654 autoSelectionTimer_->start(autoSelectionDelay_);
1655 }
1656 }
1657 break;
1658 case QEvent::HoverLeave:
1659 if(style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick)) {
1660 setCursor(Qt::ArrowCursor);
1661 }
1662 break;
1663 case QEvent::Wheel: {
1664 bool horizontalListView(false);
1665
1666 // don't let the view scroll during an inline renaming
1667 FolderItemDelegate* delegate = nullptr;
1668 if(mode == DetailedListMode) {
1669 FolderViewTreeView* treeView = static_cast<FolderViewTreeView*>(view);
1670 delegate = static_cast<FolderItemDelegate*>(treeView->itemDelegateForColumn(FolderModel::ColumnFileName));
1671 }
1672 else {
1673 FolderViewListView* listView = static_cast<FolderViewListView*>(view);
1674 horizontalListView = (listView->flow() == QListView::TopToBottom);
1675 delegate = static_cast<FolderItemDelegate*>(listView->itemDelegateForColumn(FolderModel::ColumnFileName));
1676 }
1677 if(delegate && delegate->hasEditor()) {
1678 return true;
1679 }
1680
1681 // Smooth Scrolling
1682 // Some tricks are adapted from <https://github.com/zhou13/qsmoothscrollarea>.
1683 QWheelEvent* we = static_cast<QWheelEvent*>(event);
1684 QPoint angleDelta = we->angleDelta();
1685 bool horizontal(qAbs(angleDelta.x()) > qAbs(angleDelta.y()));
1686 if(event->spontaneous()
1687 && we->source() == Qt::MouseEventNotSynthesized
1688 && (scrollPerPixel_ || (mode != DetailedListMode && mode != CompactMode))
1689 // To have a simpler code, we enable horizontal smooth scrolling with mouse wheel
1690 // only in horizontal list views.
1691 && (horizontalListView || !horizontal)) {
1692 QScrollBar* sbar = horizontalListView ? view->horizontalScrollBar()
1693 : view->verticalScrollBar();
1694 if(sbar && sbar->isVisible()) {
1695 // first get the angle delta and customize it according to our needs
1696 int delta = horizontal ? angleDelta.x() : angleDelta.y();
1697 if(QApplication::wheelScrollLines() > 1) {
1698 /* Scroll with minimum speed when
1699 (1) The mode is compact (because columns can be wide), or
1700 (2) Shift is pressed, or
1701 (3) The angle delta is less than that of an ordinary mouse wheel, or
1702 (4) The view has large icons. */
1703 if(mode == CompactMode
1704 || (we->modifiers() & Qt::ShiftModifier)
1705 || qAbs(delta) < 120
1706 || iconSize(mode).height() >= 96) {
1707 if(qAbs(delta) >= QApplication::wheelScrollLines()) {
1708 delta = delta / QApplication::wheelScrollLines();
1709 // still slower scrolling with very large icons
1710 if(iconSize(mode).height() >= 256 && qAbs(delta) >= 2) {
1711 delta /= 2;
1712 }
1713 }
1714 }
1715 else if(iconSize(mode).height() >= 64
1716 && QApplication::wheelScrollLines() > 2
1717 && qAbs(delta * 2) >= QApplication::wheelScrollLines()) {
1718 // 2 rows per mouse turn for average icon sizes
1719 delta = delta * 2 / QApplication::wheelScrollLines();
1720 }
1721 }
1722
1723 if((delta > 0 && sbar->value() == sbar->minimum()) || (delta < 0 && sbar->value() == sbar->maximum())) {
1724 break; // the scrollbar can't move
1725 }
1726
1727 // NOTE: Some touchpad devices may trigger wheel events with angle deltas
1728 // less than "scrollAnimFrames", resulting in jumpy movements. Therefore,
1729 // we wait until the total delta value is enough.
1730 static int _delta = 0;
1731 _delta += delta;
1732 if(abs(_delta) < scrollAnimFrames) {
1733 return true;
1734 }
1735
1736 if(!smoothScrollTimer_) {
1737 smoothScrollTimer_ = new QTimer();
1738 connect(smoothScrollTimer_, &QTimer::timeout, this, &FolderView::scrollSmoothly);
1739 }
1740
1741 // set the data for smooth scrolling
1742 scrollData data;
1743 data.delta = _delta;
1744 data.leftFrames = scrollAnimFrames;
1745 queuedScrollSteps_.append(data);
1746 if(!smoothScrollTimer_->isActive()) {
1747 smoothScrollTimer_->start(1000 / SCROLL_FRAMES_PER_SEC);
1748 }
1749 _delta = 0;
1750 return true;
1751 }
1752 }
1753 break;
1754 }
1755 default:
1756 break;
1757 }
1758 }
1759 return QObject::eventFilter(watched, event);
1760 }
1761
scrollSmoothly()1762 void FolderView::scrollSmoothly() {
1763 QScrollBar* sbar = nullptr;
1764 if(mode == DetailedListMode) {
1765 sbar = view->verticalScrollBar();
1766 }
1767 else {
1768 FolderViewListView* listView = static_cast<FolderViewListView*>(view);
1769 if(listView->flow() == QListView::TopToBottom) {
1770 sbar = view->horizontalScrollBar();
1771 }
1772 else {
1773 sbar = view->verticalScrollBar();
1774 }
1775 }
1776 if(!sbar || !sbar->isVisible()) {
1777 queuedScrollSteps_.clear();
1778 smoothScrollTimer_->stop();
1779 return;
1780 }
1781
1782 int totalDelta = 0;
1783 QList<scrollData>::iterator it = queuedScrollSteps_.begin();
1784 while(it != queuedScrollSteps_.end()) {
1785 int delta = qRound((qreal)it->delta / (qreal)scrollAnimFrames);
1786 int remainingDelta = it->delta - (scrollAnimFrames - it->leftFrames) * delta;
1787 if((delta >= 0 && remainingDelta < 0) || (delta < 0 && remainingDelta >= 0)) {
1788 remainingDelta = 0;
1789 }
1790 if(qAbs(delta) >= qAbs(remainingDelta)) {
1791 // this is the last frame or, due to rounding, there can be no more frame
1792 totalDelta += remainingDelta;
1793 it = queuedScrollSteps_.erase(it);
1794 }
1795 else {
1796 totalDelta += delta;
1797 -- it->leftFrames;
1798 ++it;
1799 }
1800 }
1801 if(totalDelta != 0) {
1802 QWheelEvent e(QPointF(),
1803 QPointF(),
1804 QPoint(),
1805 QPoint (0, totalDelta),
1806 Qt::NoButton,
1807 Qt::NoModifier,
1808 Qt::NoScrollPhase,
1809 false);
1810
1811 QApplication::sendEvent(sbar, &e);
1812
1813 // update rubberband selection with smooth scrolling in icon view
1814 if ((mode == IconMode || mode == ThumbnailMode)
1815 && (QApplication::mouseButtons() & Qt::LeftButton)) {
1816 const QPoint globalPos = QCursor::pos();
1817 QPoint pos = view->viewport()->mapFromGlobal(globalPos);
1818 QMouseEvent ev(QEvent::MouseMove,
1819 pos,
1820 view->viewport()->mapTo(view->viewport()->topLevelWidget(), pos),
1821 globalPos,
1822 Qt::LeftButton,
1823 Qt::LeftButton,
1824 QApplication::keyboardModifiers());
1825 QApplication::sendEvent(view->viewport(), &ev);
1826 }
1827 }
1828
1829 if(queuedScrollSteps_.empty()) {
1830 smoothScrollTimer_->stop();
1831 }
1832 }
1833
1834 // this slot handles auto-selection of items.
onAutoSelectionTimeout()1835 void FolderView::onAutoSelectionTimeout() {
1836 if(QApplication::mouseButtons() != Qt::NoButton) {
1837 return;
1838 }
1839
1840 // If the cursor moves immediately after a (context) menu is shown, "QEvent::HoverMove"
1841 // might be sent, which will result in calling this function. That is a Qt issue. As a
1842 // workaround, we do not proceed when there is an active popup widget.
1843 if(QApplication::activePopupWidget()) {
1844 return;
1845 }
1846
1847 // don't do anything if the cursor is on selection corner icon
1848 if(mode != DetailedListMode) {
1849 FolderViewListView* listView = static_cast<FolderViewListView*>(view);
1850 if(listView->cursorOnSelectionCorner()) {
1851 return;
1852 }
1853 }
1854
1855 QPoint pos = view->viewport()->mapFromGlobal(QCursor::pos()); // convert to viewport coordinates
1856 QModelIndex index = view->indexAt(pos); // find out the hovered item
1857 if(!index.isValid()) {
1858 return;
1859 }
1860
1861 Qt::KeyboardModifiers mods = QApplication::keyboardModifiers();
1862 QItemSelectionModel::SelectionFlags flags = (mode == DetailedListMode ? QItemSelectionModel::Rows : QItemSelectionModel::NoUpdate);
1863 QItemSelectionModel* selModel = view->selectionModel();
1864
1865 if(mods & Qt::ControlModifier) { // Ctrl key is pressed
1866 if(selModel->isSelected(index) && index != lastAutoSelectionIndex_) {
1867 // unselect a previously selected item
1868 selModel->select(index, flags | QItemSelectionModel::Deselect);
1869 lastAutoSelectionIndex_ = QModelIndex();
1870 }
1871 else {
1872 // select an unselected item
1873 selModel->select(index, flags | QItemSelectionModel::Select);
1874 lastAutoSelectionIndex_ = index;
1875 }
1876 selModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate); // move the cursor
1877 }
1878 else if(mods & Qt::ShiftModifier) { // Shift key is pressed
1879 // select all items between current index and the hovered index.
1880 QModelIndex current = selModel->currentIndex();
1881 if(selModel->hasSelection() && current.isValid()) {
1882 selModel->clear(); // clear old selection
1883 selModel->setCurrentIndex(current, QItemSelectionModel::NoUpdate);
1884 int begin = current.row();
1885 int end = index.row();
1886 if(begin > end) {
1887 std::swap(begin, end);
1888 }
1889 for(int row = begin; row <= end; ++row) {
1890 QModelIndex sel = model_->index(row, 0);
1891 selModel->select(sel, flags | QItemSelectionModel::Select);
1892 }
1893 }
1894 else { // no items are selected, select the hovered item.
1895 if(index.isValid()) {
1896 selModel->select(index, flags | QItemSelectionModel::SelectCurrent);
1897 selModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
1898 }
1899 }
1900 lastAutoSelectionIndex_ = index;
1901 }
1902 else if(mods == Qt::NoModifier) { // no modifier keys are pressed.
1903 if(index.isValid()) {
1904 // select the hovered item
1905 view->clearSelection();
1906 selModel->select(index, flags | QItemSelectionModel::SelectCurrent);
1907 selModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
1908 }
1909 lastAutoSelectionIndex_ = index;
1910 }
1911
1912 autoSelectionTimer_->deleteLater();
1913 autoSelectionTimer_ = nullptr;
1914 }
1915
onFileClicked(int type,const std::shared_ptr<const Fm::FileInfo> & fileInfo)1916 void FolderView::onFileClicked(int type, const std::shared_ptr<const Fm::FileInfo> &fileInfo) {
1917 if(type == ActivatedClick) {
1918 if(fileLauncher_) {
1919 Fm::FileInfoList files;
1920 files.push_back(fileInfo);
1921 fileLauncher_->launchFiles(nullptr, std::move(files));
1922 }
1923 }
1924 else if(type == ContextMenuClick) {
1925 Fm::FilePath folderPath;
1926 bool isWritableDir(true);
1927 auto files = selectedFiles();
1928 if(!files.empty()) {
1929 auto& first = files.front();
1930 if(files.size() == 1 && first->isDir()) {
1931 folderPath = first->path();
1932 isWritableDir = first->isWritable();
1933 }
1934 }
1935 if(!folderPath.isValid()) {
1936 folderPath = path();
1937 if(auto info = folderInfo()) {
1938 isWritableDir = info->isWritable();
1939 }
1940 }
1941 QMenu* menu = nullptr;
1942 if(fileInfo) {
1943 // show context menu
1944 auto files = selectedFiles();
1945 if(!files.empty()) {
1946 Fm::FileMenu* fileMenu = new Fm::FileMenu(files, fileInfo, folderPath, isWritableDir, QString(), view);
1947 fileMenu->setFileLauncher(fileLauncher_);
1948 fileMenu->addTrustAction();
1949 prepareFileMenu(fileMenu);
1950 menu = fileMenu;
1951 }
1952 }
1953 if (!menu && folderInfo()) {
1954 Fm::FolderMenu* folderMenu = new Fm::FolderMenu(this);
1955 prepareFolderMenu(folderMenu);
1956 menu = folderMenu;
1957 }
1958 if(menu) {
1959 menu->exec(QCursor::pos());
1960 delete menu;
1961 }
1962 }
1963 }
1964
prepareFileMenu(FileMenu *)1965 void FolderView::prepareFileMenu(FileMenu* /*menu*/) {
1966 }
1967
prepareFolderMenu(FolderMenu *)1968 void FolderView::prepareFolderMenu(FolderMenu* /*menu*/) {
1969 }
1970