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 &current, 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