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 "placesview.h"
22 #include "placesmodel.h"
23 #include "placesmodelitem.h"
24 #include "mountoperation.h"
25 #include "fileoperation.h"
26 #include <QMenu>
27 #include <QContextMenuEvent>
28 #include <QHeaderView>
29 #include <QDebug>
30 #include <QGuiApplication>
31 #include <QTimer>
32 #include "folderitemdelegate.h"
33 
34 namespace Fm {
35 
36 std::shared_ptr<PlacesProxyModel> PlacesView::proxyModel_;
37 
PlacesProxyModel(QObject * parent)38 PlacesProxyModel::PlacesProxyModel(QObject* parent) :
39     QSortFilterProxyModel(parent),
40     showAll_(false),
41     hiddenItemsRestored_(false) {
42 }
43 
~PlacesProxyModel()44 PlacesProxyModel::~PlacesProxyModel() {
45 }
46 
restoreHiddenItems(const QSet<QString> & items)47 void PlacesProxyModel::restoreHiddenItems(const QSet<QString>& items) {
48     // hidden items should be restored only once
49     if(!hiddenItemsRestored_ && !items.isEmpty()) {
50         hidden_.clear();
51         QSet<QString>::const_iterator i = items.constBegin();
52         while (i != items.constEnd()) {
53             if(!(*i).isEmpty()) {
54                 hidden_ << *i;
55             }
56             ++i;
57         }
58         hiddenItemsRestored_ = true;
59         invalidateFilter();
60     }
61 }
62 
setHidden(const QString & str,bool hide)63 void PlacesProxyModel::setHidden(const QString& str, bool hide) {
64     if(hide) {
65         if(!str.isEmpty()) {
66             hidden_ << str;
67         }
68     }
69     else {
70         hidden_.remove(str);
71     }
72     invalidateFilter();
73 }
74 
showAll(bool show)75 void PlacesProxyModel::showAll(bool show) {
76     showAll_ = show;
77     invalidateFilter();
78 }
79 
filterAcceptsRow(int source_row,const QModelIndex & source_parent) const80 bool PlacesProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const {
81     if(showAll_ || hidden_.isEmpty()) {
82         return true;
83     }
84     if(PlacesModel* srcModel = static_cast<PlacesModel*>(sourceModel())) {
85         QModelIndex index = srcModel->index(source_row, 0, source_parent);
86         if(PlacesModelItem* item = static_cast<PlacesModelItem*>(srcModel->itemFromIndex(index))) {
87             if(item->type() == PlacesModelItem::Places) {
88                 if(auto path = item->path()) {
89                     if(hidden_.contains(QString::fromUtf8(path.toString().get()))) {
90                         return false;
91                     }
92                 }
93             }
94             else if(item->type() == PlacesModelItem::Volume) {
95                 CStrPtr uuid{g_volume_get_uuid(static_cast<PlacesModelVolumeItem*>(item)->volume())};
96                 if(uuid && hidden_.contains(QString::fromUtf8(uuid.get()))) {
97                     return false;
98                 }
99             }
100             // show a root items only if, at least, one of its children is shown
101             else if((source_row == 0 || source_row == 1) && !source_parent.isValid()) {
102                 QModelIndex indx = index.model()->index(0, 0, index);
103                 while(PlacesModelItem* childItem = static_cast<PlacesModelItem*>(srcModel->itemFromIndex(indx))) {
104                     if(childItem->type() == PlacesModelItem::Places) {
105                         if(auto path = childItem->path()) {
106                             if(!hidden_.contains(QString::fromUtf8(path.toString().get()))) {
107                                 return true;
108                             }
109                         }
110                     }
111                     else if(childItem->type() == PlacesModelItem::Volume) {
112                         CStrPtr uuid{g_volume_get_uuid(static_cast<PlacesModelVolumeItem*>(childItem)->volume())};
113                         if(uuid == nullptr || !hidden_.contains(QString::fromUtf8(uuid.get()))) {
114                             return true;
115                         }
116                     }
117                     else {
118                         return true;
119                     }
120                     indx = indx.sibling(indx.row() + 1, 0);
121                 }
122                 return false;
123             }
124         }
125     }
126     return true;
127 }
128 
PlacesView(QWidget * parent)129 PlacesView::PlacesView(QWidget* parent):
130     QTreeView(parent) {
131     setRootIsDecorated(false);
132     setHeaderHidden(true);
133     setIndentation(12);
134 
135     connect(this, &QTreeView::clicked, this, &PlacesView::onClicked);
136     connect(this, &QTreeView::pressed, this, &PlacesView::onPressed);
137 
138     setIconSize(QSize(24, 24));
139 
140     FolderItemDelegate* delegate = new FolderItemDelegate(this, this);
141     delegate->setFileInfoRole(PlacesModel::FileInfoRole);
142     delegate->setIconInfoRole(PlacesModel::FmIconRole);
143     setItemDelegateForColumn(0, delegate);
144 
145     model_ = PlacesModel::globalInstance();
146     if(!proxyModel_) {
147         proxyModel_ = std::make_shared<PlacesProxyModel>();
148     }
149     if(!proxyModel_->sourceModel()) { // all places-views may have been closed
150         proxyModel_->setSourceModel(model_.get());
151     }
152     setModel(proxyModel_.get());
153 
154     // these 2 connections are needed to update filtering
155     connect(model_.get(), &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex&, int, int) {
156         proxyModel_->setHidden(QString()); // just invalidates filter
157         expandAll();
158         // for some reason (a Qt bug?), spanning is reset
159         spanFirstColumn();
160     });
161     connect(model_.get(), &QAbstractItemModel::rowsRemoved, this, [](const QModelIndex&, int, int) {
162         proxyModel_->setHidden(QString());
163     });
164 
165     QHeaderView* headerView = header();
166     // WARNING: Since Qt 5.11, if the minimum header section width isn't set,
167     // Qt will set it by considering the font size, even without text.
168     // See Qt doc and https://bugreports.qt.io/browse/QTBUG-68503
169     headerView->setMinimumSectionSize(1);
170     headerView->setSectionResizeMode(0, QHeaderView::Stretch);
171     headerView->setSectionResizeMode(1, QHeaderView::Fixed);
172     headerView->setStretchLastSection(false);
173     expandAll();
174 
175     spanFirstColumn();
176 
177     // the 2nd column is for the eject buttons
178     setSelectionBehavior(QAbstractItemView::SelectRows); // FIXME: why this does not work?
179     setAllColumnsShowFocus(false);
180 
181     setAcceptDrops(true);
182     setDragEnabled(true);
183 
184     // update the umount button's column width based on icon size
185     onIconSizeChanged(iconSize());
186     connect(this, &QAbstractItemView::iconSizeChanged, this, &PlacesView::onIconSizeChanged);
187 }
188 
~PlacesView()189 PlacesView::~PlacesView() {
190     // qDebug("delete PlacesView");
191 }
192 
spanFirstColumn()193 void PlacesView::spanFirstColumn() {
194     // FIXME: is there any better way to make the first column span the whole row?
195     setFirstColumnSpanned(0, QModelIndex(), true); // places root
196     setFirstColumnSpanned(1, QModelIndex(), true); // devices root
197     setFirstColumnSpanned(2, QModelIndex(), true); // bookmarks root
198     // NOTE: The first column of the devices children shouldn't be spanned
199     // because the second column contains eject buttons.
200     QModelIndex indx = proxyModel_->mapFromSource(model_->placesRoot->index());
201     if(indx.isValid()) {
202         for(int i = 0; i < indx.model()->rowCount(indx); ++i) {
203             setFirstColumnSpanned(i, indx, true);
204         }
205     }
206     indx = proxyModel_->mapFromSource(model_->bookmarksRoot->index());
207     if(indx.isValid()) {
208         for(int i = 0; i < indx.model()->rowCount(indx); ++i) {
209             setFirstColumnSpanned(i, indx, true);
210         }
211     }
212 }
213 
activateRow(int type,const QModelIndex & index)214 void PlacesView::activateRow(int type, const QModelIndex& index) {
215     if(!index.parent().isValid()) { // ignore root items
216         return;
217     }
218     PlacesModelItem* item = static_cast<PlacesModelItem*>(model_->itemFromIndex(proxyModel_->mapToSource(index)));
219     if(item) {
220         auto path = item->path();
221         if(!path) {
222             // check if mounting volumes is needed
223             if(item->type() == PlacesModelItem::Volume) {
224                 PlacesModelVolumeItem* volumeItem = static_cast<PlacesModelVolumeItem*>(item);
225                 if(!volumeItem->isMounted()) {
226                     // Mount the volume
227                     GVolume* volume = volumeItem->volume();
228                     MountOperation* op = new MountOperation(true, this);
229                     op->mount(volume);
230                     // WARNING: "QAbstractItemView::mouseReleaseEvent" will be dispatched only
231                     // after this function returns. Therefore, without a single-shot timer, if
232                     // the window is closed before the mount operation is finished, the operation
233                     // will be canceled, this function will return and "mouseReleaseEvent" will
234                     // be sent to a destroyed QAbstractItemView, resulting in a crash.
235                     QTimer::singleShot(0, op, [this, op, type, index] () {
236                         if(op->wait()) {
237                             if(PlacesModelItem* item = static_cast<PlacesModelItem*>(model_->itemFromIndex(proxyModel_->mapToSource(index)))) {
238                                 if(auto path = item->path()) {
239                                     Q_EMIT chdirRequested(type, path);
240                                 }
241                             }
242                         }
243                     });
244                 }
245             }
246         }
247         else {
248             Q_EMIT chdirRequested(type, path);
249         }
250     }
251 }
252 
253 // mouse button pressed
onPressed(const QModelIndex & index)254 void PlacesView::onPressed(const QModelIndex& index) {
255     // if middle button is pressed
256     if(QGuiApplication::mouseButtons() & Qt::MiddleButton) {
257         // the real item is at column 0
258         activateRow(1, 0 == index.column() ? index : index.sibling(index.row(), 0));
259     }
260 }
261 
onIconSizeChanged(const QSize & size)262 void PlacesView::onIconSizeChanged(const QSize& size) {
263     setColumnWidth(1, size.width() + 2 * (style()->pixelMetric(QStyle::PM_FocusFrameHMargin)
264                                             + 1)); // put it inside the focus rectangle, if any
265 }
266 
onEjectButtonClicked(PlacesModelItem * item)267 void PlacesView::onEjectButtonClicked(PlacesModelItem* item) {
268     // The eject button is clicked for a device item (volume or mount)
269     if(item->type() == PlacesModelItem::Volume) {
270         PlacesModelVolumeItem* volumeItem = static_cast<PlacesModelVolumeItem*>(item);
271         MountOperation* op = new MountOperation(true, this);
272         if(volumeItem->canEject()) { // do eject if applicable
273             op->eject(volumeItem->volume());
274         }
275         else { // otherwise, do unmount instead
276             op->unmount(volumeItem->volume());
277         }
278     }
279     else if(item->type() == PlacesModelItem::Mount) {
280         PlacesModelMountItem* mountItem = static_cast<PlacesModelMountItem*>(item);
281         MountOperation* op = new MountOperation(true, this);
282         op->unmount(mountItem->mount());
283     }
284     qDebug("PlacesView::onEjectButtonClicked");
285 }
286 
onClicked(const QModelIndex & index)287 void PlacesView::onClicked(const QModelIndex& index) {
288     if(!index.parent().isValid()) { // ignore root items
289         return;
290     }
291 
292     if(index.column() == 0) {
293         activateRow(0, index);
294     }
295     else if(index.column() == 1) { // column 1 contains eject buttons of the mounted devices
296         if(index.parent() == proxyModel_->mapFromSource(model_->devicesRoot->index())) { // this is a mounted device
297             // the eject button is clicked
298             QModelIndex itemIndex = index.sibling(index.row(), 0); // the real item is at column 0
299             PlacesModelItem* item = static_cast<PlacesModelItem*>(model_->itemFromIndex(proxyModel_->mapToSource(itemIndex)));
300             if(item) {
301                 // eject the volume or the mount
302                 onEjectButtonClicked(item);
303             }
304         }
305         else {
306             activateRow(0, index.sibling(index.row(), 0));
307         }
308     }
309 }
310 
setCurrentPath(Fm::FilePath path)311 void PlacesView::setCurrentPath(Fm::FilePath path) {
312     clearSelection();
313     currentPath_ = std::move(path);
314     if(currentPath_) {
315         // TODO: search for item with the path in model_ and select it.
316         PlacesModelItem* item = model_->itemFromPath(currentPath_);
317         if(item) {
318             selectionModel()->select(proxyModel_->mapFromSource(item->index()), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
319         }
320     }
321 }
322 
323 
dragMoveEvent(QDragMoveEvent * event)324 void PlacesView::dragMoveEvent(QDragMoveEvent* event) {
325     QTreeView::dragMoveEvent(event);
326 }
327 
dropEvent(QDropEvent * event)328 void PlacesView::dropEvent(QDropEvent* event) {
329     QTreeView::dropEvent(event);
330 }
331 
onEmptyTrash()332 void PlacesView::onEmptyTrash() {
333     Fm::FilePathList files;
334     files.push_back(Fm::FilePath::fromUri("trash:///"));
335     Fm::FileOperation::deleteFiles(std::move(files), true);
336 }
337 
onMoveBookmarkUp()338 void PlacesView::onMoveBookmarkUp() {
339     PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
340     if(!action->index().isValid()) {
341         return;
342     }
343     PlacesModelBookmarkItem* item = static_cast<PlacesModelBookmarkItem*>(model_->itemFromIndex(action->index()));
344 
345     int row = item->row();
346     if(row > 0) {
347         auto bookmarkItem = item->bookmark();
348         Fm::Bookmarks::globalInstance()->reorder(bookmarkItem, row - 1);
349     }
350 }
351 
onMoveBookmarkDown()352 void PlacesView::onMoveBookmarkDown() {
353     PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
354     if(!action->index().isValid()) {
355         return;
356     }
357     PlacesModelBookmarkItem* item = static_cast<PlacesModelBookmarkItem*>(model_->itemFromIndex(action->index()));
358 
359     int row = item->row();
360     if(row < model_->rowCount()) {
361         auto bookmarkItem = item->bookmark();
362         Fm::Bookmarks::globalInstance()->reorder(bookmarkItem, row + 1);
363     }
364 }
365 
onDeleteBookmark()366 void PlacesView::onDeleteBookmark() {
367     PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
368     if(!action->index().isValid()) {
369         return;
370     }
371     PlacesModelBookmarkItem* item = static_cast<PlacesModelBookmarkItem*>(model_->itemFromIndex(action->index()));
372     auto bookmarkItem = item->bookmark();
373     Fm::Bookmarks::globalInstance()->remove(bookmarkItem);
374 }
375 
376 // virtual
commitData(QWidget * editor)377 void PlacesView::commitData(QWidget* editor) {
378     QTreeView::commitData(editor);
379     PlacesModelBookmarkItem* item = static_cast<PlacesModelBookmarkItem*>(model_->itemFromIndex(proxyModel_->mapToSource(currentIndex())));
380     auto bookmarkItem = item->bookmark();
381     // rename bookmark
382     Fm::Bookmarks::globalInstance()->rename(bookmarkItem, item->text());
383 }
384 
onOpenNewTab()385 void PlacesView::onOpenNewTab() {
386     PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
387     if(!action->index().isValid()) {
388         return;
389     }
390     PlacesModelItem* item = static_cast<PlacesModelItem*>(model_->itemFromIndex(action->index()));
391     if(item) {
392         Q_EMIT chdirRequested(1, item->path());
393     }
394 }
395 
onOpenNewWindow()396 void PlacesView::onOpenNewWindow() {
397     PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
398     if(!action->index().isValid()) {
399         return;
400     }
401     PlacesModelItem* item = static_cast<PlacesModelItem*>(model_->itemFromIndex(action->index()));
402     if(item) {
403         Q_EMIT chdirRequested(2, item->path());
404     }
405 }
406 
onRenameBookmark()407 void PlacesView::onRenameBookmark() {
408     PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
409     if(!action->index().isValid()) {
410         return;
411     }
412     PlacesModelBookmarkItem* item = static_cast<PlacesModelBookmarkItem*>(model_->itemFromIndex(action->index()));
413     setFocus();
414     setCurrentIndex(proxyModel_->mapFromSource(item->index()));
415     edit(proxyModel_->mapFromSource(item->index()));
416 }
417 
onMountVolume()418 void PlacesView::onMountVolume() {
419     PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
420     if(!action->index().isValid()) {
421         return;
422     }
423     PlacesModelVolumeItem* item = static_cast<PlacesModelVolumeItem*>(model_->itemFromIndex(action->index()));
424     MountOperation* op = new MountOperation(true, this);
425     op->mount(item->volume());
426     op->wait();
427 }
428 
onUnmountVolume()429 void PlacesView::onUnmountVolume() {
430     PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
431     if(!action->index().isValid()) {
432         return;
433     }
434     PlacesModelVolumeItem* item = static_cast<PlacesModelVolumeItem*>(model_->itemFromIndex(action->index()));
435     MountOperation* op = new MountOperation(true, this);
436     op->unmount(item->volume());
437     op->wait();
438 }
439 
onUnmountMount()440 void PlacesView::onUnmountMount() {
441     PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
442     if(!action->index().isValid()) {
443         return;
444     }
445     PlacesModelMountItem* item = static_cast<PlacesModelMountItem*>(model_->itemFromIndex(action->index()));
446     GMount* mount = item->mount();
447     MountOperation* op = new MountOperation(true, this);
448     op->unmount(mount);
449     op->wait();
450 }
451 
onEjectVolume()452 void PlacesView::onEjectVolume() {
453     PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
454     if(!action->index().isValid()) {
455         return;
456     }
457     PlacesModelVolumeItem* item = static_cast<PlacesModelVolumeItem*>(model_->itemFromIndex(action->index()));
458     MountOperation* op = new MountOperation(true, this);
459     op->eject(item->volume());
460     op->wait();
461 }
462 
contextMenuEvent(QContextMenuEvent * event)463 void PlacesView::contextMenuEvent(QContextMenuEvent* event) {
464     QModelIndex index = indexAt(event->pos());
465     if(index.isValid()) {
466         if(index.column() != 0) { // the real item is at column 0
467             index = index.sibling(index.row(), 0);
468         }
469 
470         // Do not take the ownership of the menu since
471         // it will be deleted with deleteLater() upon hidden.
472         // This is possibly related to #145 - https://github.com/lxqt/pcmanfm-qt/issues/145
473         QMenu* menu = new QMenu();
474         QAction* action = nullptr;
475         PlacesModelItem* item = static_cast<PlacesModelItem*>(model_->itemFromIndex(proxyModel_->mapToSource(index)));
476 
477         if(index.parent().isValid()
478            && item->type() != PlacesModelItem::Mount
479            && (item->type() != PlacesModelItem::Volume
480                || static_cast<PlacesModelVolumeItem*>(item)->isMounted())) {
481             action = new PlacesModel::ItemAction(item->index(), tr("Open in New Tab"), menu);
482             connect(action, &QAction::triggered, this, &PlacesView::onOpenNewTab);
483             menu->addAction(action);
484             action = new PlacesModel::ItemAction(item->index(), tr("Open in New Window"), menu);
485             connect(action, &QAction::triggered, this, &PlacesView::onOpenNewWindow);
486             menu->addAction(action);
487         }
488 
489         switch(item->type()) {
490         case PlacesModelItem::Places: {
491             auto path = item->path();
492             // FIXME: inefficient
493             if(path) {
494                 auto path_str = path.toString();
495                 if(strcmp(path_str.get(), "trash:///") == 0) {
496                     action = new PlacesModel::ItemAction(item->index(), tr("Empty Trash"), menu);
497                     auto icn = item->icon();
498                     if(icn && icn->qicon().name() == QLatin1String("user-trash")) { // surely an empty trash
499                         action->setEnabled(false);
500                     }
501                     else {
502                         connect(action, &QAction::triggered, this, &PlacesView::onEmptyTrash);
503                     }
504                     // add the "Empty Trash" item on the top
505                     QList<QAction*> actions = menu->actions();
506                     if(!actions.isEmpty()) {
507                         menu->insertAction(actions.at(0), action);
508                         menu->insertSeparator(actions.at(0));
509                     }
510                     else { // impossible
511                         menu->addAction(action);
512                     }
513                 }
514                 // add a "Hide" action to the end
515                 menu->addSeparator();
516                 action = new PlacesModel::ItemAction(item->index(), tr("Hide"), menu);
517                 QString pathStr(QString::fromUtf8(path_str.get()));
518                 action->setCheckable(true);
519                 if(proxyModel_->isShowingAll()) {
520                     action->setChecked(proxyModel_->isHidden(pathStr));
521                 }
522                 connect(action, &QAction::triggered, [this, pathStr](bool checked) {
523                     proxyModel_->setHidden(pathStr, checked);
524                     Q_EMIT hiddenItemSet(pathStr, checked);
525                 });
526                 menu->addAction(action);
527             }
528             break;
529         }
530         case PlacesModelItem::Bookmark: {
531             // create context menu for bookmark item
532             if(item->index().row() > 0) {
533                 action = new PlacesModel::ItemAction(item->index(), tr("Move Bookmark Up"), menu);
534                 connect(action, &QAction::triggered, this, &PlacesView::onMoveBookmarkUp);
535                 menu->addAction(action);
536             }
537             if(item->index().row() < model_->rowCount()) {
538                 action = new PlacesModel::ItemAction(item->index(), tr("Move Bookmark Down"), menu);
539                 connect(action, &QAction::triggered, this, &PlacesView::onMoveBookmarkDown);
540                 menu->addAction(action);
541             }
542             action = new PlacesModel::ItemAction(item->index(), tr("Rename Bookmark"), menu);
543             connect(action, &QAction::triggered, this, &PlacesView::onRenameBookmark);
544             menu->addAction(action);
545             action = new PlacesModel::ItemAction(item->index(), tr("Remove Bookmark"), menu);
546             connect(action, &QAction::triggered, this, &PlacesView::onDeleteBookmark);
547             menu->addAction(action);
548             break;
549         }
550         case PlacesModelItem::Volume: {
551             PlacesModelVolumeItem* volumeItem = static_cast<PlacesModelVolumeItem*>(item);
552 
553             if(volumeItem->isMounted()) {
554                 action = new PlacesModel::ItemAction(item->index(), tr("Unmount"), menu);
555                 connect(action, &QAction::triggered, this, &PlacesView::onUnmountVolume);
556             }
557             else {
558                 action = new PlacesModel::ItemAction(item->index(), tr("Mount"), menu);
559                 connect(action, &QAction::triggered, this, &PlacesView::onMountVolume);
560             }
561             menu->addAction(action);
562 
563             if(volumeItem->canEject()) {
564                 action = new PlacesModel::ItemAction(item->index(), tr("Eject"), menu);
565                 connect(action, &QAction::triggered, this, &PlacesView::onEjectVolume);
566                 menu->addAction(action);
567             }
568             // add a "Hide" action to the end
569             CStrPtr uuid{g_volume_get_uuid(static_cast<PlacesModelVolumeItem*>(item)->volume())};
570             if(uuid) {
571                 QString str = QString::fromUtf8(uuid.get());
572                 menu->addSeparator();
573                 action = new PlacesModel::ItemAction(item->index(), tr("Hide"), menu);
574                 action->setCheckable(true);
575                 if(proxyModel_->isShowingAll()) {
576                     action->setChecked(proxyModel_->isHidden(str));
577                 }
578                 connect(action, &QAction::triggered, [this, str](bool checked) {
579                     proxyModel_->setHidden(str, checked);
580                     Q_EMIT hiddenItemSet(str, checked);
581                 });
582                 menu->addAction(action);
583             }
584             break;
585         }
586         case PlacesModelItem::Mount: {
587             action = new PlacesModel::ItemAction(item->index(), tr("Unmount"), menu);
588             connect(action, &QAction::triggered, this, &PlacesView::onUnmountMount);
589             menu->addAction(action);
590             break;
591         }
592         }
593 
594         // also add an acton for showing all hidden items
595         if(proxyModel_->hasHidden()) {
596             if(item->type() == PlacesModelItem::Bookmark) {
597                 menu->addSeparator();
598             }
599             action = new PlacesModel::ItemAction(item->index(), tr("Show All Entries"), menu);
600             action->setCheckable(true);
601             action->setChecked(proxyModel_->isShowingAll());
602             connect(action, &QAction::triggered, [this](bool checked) {
603                 showAll(checked);
604             });
605             menu->addAction(action);
606         }
607 
608         if(menu->actions().size()) {
609             menu->popup(mapToGlobal(event->pos()));
610             connect(menu, &QMenu::aboutToHide, menu, &QMenu::deleteLater);
611         }
612         else {
613             menu->deleteLater();
614         }
615     }
616 }
617 
keyPressEvent(QKeyEvent * event)618 void PlacesView::keyPressEvent(QKeyEvent* event) {
619     // prevent propagation of usual modifiers because
620     // they might be used elsewhere in shortcuts, especially with arrow keys
621     if(event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier)) {
622         return;
623     }
624     // activate child items and expand/collapse root items with Enter/Return
625     if(event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
626         QModelIndex index = currentIndex();
627         if(index.isValid()) {
628             if (index.column() != 0) {
629                 index = index.sibling(index.row(), 0); // the real item is at column 0
630             }
631             if (index.isValid()) {
632                 if (!index.parent().isValid()) { // root item
633                     setExpanded(index, !isExpanded(index));
634                 }
635                 else {
636                     // may have not been selected yet
637                     selectionModel()->select(index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
638                     activateRow(0, index);
639                 }
640                 return;
641             }
642         }
643     }
644     QTreeView::keyPressEvent(event);
645 }
646 
restoreHiddenItems(const QSet<QString> & items)647 void PlacesView::restoreHiddenItems(const QSet<QString>& items) {
648     proxyModel_->restoreHiddenItems(items);
649 }
650 
showAll(bool show)651 void PlacesView::showAll(bool show) {
652     proxyModel_->showAll(show);
653     if(show) {
654         expandAll();
655         // for some reason (a Qt bug?), spanning is reset
656         spanFirstColumn();
657     }
658 }
659 
660 } // namespace Fm
661