1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "foldernavigationwidget.h"
27 #include "projectexplorer.h"
28 #include "projectexplorerconstants.h"
29 #include "projectexplorericons.h"
30 #include "projectnodes.h"
31 #include "projecttree.h"
32 
33 #include <coreplugin/actionmanager/actionmanager.h>
34 #include <coreplugin/actionmanager/command.h>
35 #include <coreplugin/diffservice.h>
36 #include <coreplugin/documentmanager.h>
37 #include <coreplugin/editormanager/editormanager.h>
38 #include <coreplugin/editormanager/ieditor.h>
39 #include <coreplugin/fileiconprovider.h>
40 #include <coreplugin/fileutils.h>
41 #include <coreplugin/icontext.h>
42 #include <coreplugin/icore.h>
43 #include <coreplugin/idocument.h>
44 #include <coreplugin/iwizardfactory.h>
45 
46 #include <extensionsystem/pluginmanager.h>
47 
48 #include <texteditor/textdocument.h>
49 
50 #include <utils/algorithm.h>
51 #include <utils/filecrumblabel.h>
52 #include <utils/fileutils.h>
53 #include <utils/hostosinfo.h>
54 #include <utils/navigationtreeview.h>
55 #include <utils/qtcassert.h>
56 #include <utils/removefiledialog.h>
57 #include <utils/stringutils.h>
58 #include <utils/styledbar.h>
59 #include <utils/utilsicons.h>
60 
61 #include <QAction>
62 #include <QApplication>
63 #include <QComboBox>
64 #include <QContextMenuEvent>
65 #include <QDir>
66 #include <QFileInfo>
67 #include <QFileSystemModel>
68 #include <QHeaderView>
69 #include <QMenu>
70 #include <QMessageBox>
71 #include <QScrollBar>
72 #include <QSize>
73 #include <QSortFilterProxyModel>
74 #include <QTimer>
75 #include <QToolButton>
76 #include <QVBoxLayout>
77 
78 const int PATH_ROLE = Qt::UserRole;
79 const int ID_ROLE = Qt::UserRole + 1;
80 const int SORT_ROLE = Qt::UserRole + 2;
81 
82 const char PROJECTSDIRECTORYROOT_ID[] = "A.Projects";
83 const char C_FOLDERNAVIGATIONWIDGET[] = "ProjectExplorer.FolderNavigationWidget";
84 
85 const char kSettingsBase[] = "FolderNavigationWidget.";
86 const char kHiddenFilesKey[] = ".HiddenFilesFilter";
87 const char kSyncKey[] = ".SyncWithEditor";
88 const char kShowBreadCrumbs[] = ".ShowBreadCrumbs";
89 const char kSyncRootWithEditor[] = ".SyncRootWithEditor";
90 
91 namespace ProjectExplorer {
92 namespace Internal {
93 
94 static FolderNavigationWidgetFactory *m_instance = nullptr;
95 
96 QVector<FolderNavigationWidgetFactory::RootDirectory>
97     FolderNavigationWidgetFactory::m_rootDirectories;
98 
99 
createHLine()100 static QWidget *createHLine()
101 {
102     auto widget = new QFrame;
103     widget->setFrameStyle(QFrame::Plain | QFrame::HLine);
104     return widget;
105 }
106 
107 // Call delayLayoutOnce to delay reporting the new heightForWidget by the double-click interval.
108 // Call setScrollBarOnce to set a scroll bar's value once during layouting (where heightForWidget
109 // is called).
110 class DelayedFileCrumbLabel : public Utils::FileCrumbLabel
111 {
112 public:
DelayedFileCrumbLabel(QWidget * parent)113     DelayedFileCrumbLabel(QWidget *parent) : Utils::FileCrumbLabel(parent) {}
114 
115     int immediateHeightForWidth(int w) const;
116     int heightForWidth(int w) const final;
117     void delayLayoutOnce();
118     void setScrollBarOnce(QScrollBar *bar, int value);
119 
120 private:
121     void setScrollBarOnce() const;
122 
123     QPointer<QScrollBar> m_bar;
124     int m_barValue = 0;
125     bool m_delaying = false;
126 };
127 
128 // FolderNavigationModel: Shows path as tooltip.
129 class FolderNavigationModel : public QFileSystemModel
130 {
131 public:
132     enum Roles {
133         IsFolderRole = Qt::UserRole + 50 // leave some gap for the custom roles in QFileSystemModel
134     };
135 
136     explicit FolderNavigationModel(QObject *parent = nullptr);
137     QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const final;
138     Qt::DropActions supportedDragActions() const final;
139     Qt::ItemFlags flags(const QModelIndex &index) const final;
140     bool setData(const QModelIndex &index, const QVariant &value, int role) final;
141 };
142 
143 // Sorts folders on top if wanted
144 class FolderSortProxyModel : public QSortFilterProxyModel
145 {
146 public:
147     FolderSortProxyModel(QObject *parent = nullptr);
148 
149 protected:
150     bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
151 };
152 
FolderSortProxyModel(QObject * parent)153 FolderSortProxyModel::FolderSortProxyModel(QObject *parent)
154     : QSortFilterProxyModel(parent)
155 {
156 }
157 
lessThan(const QModelIndex & source_left,const QModelIndex & source_right) const158 bool FolderSortProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
159 {
160     const QAbstractItemModel *src = sourceModel();
161     if (sortRole() == FolderNavigationModel::IsFolderRole) {
162         const bool leftIsFolder = src->data(source_left, FolderNavigationModel::IsFolderRole)
163                                       .toBool();
164         const bool rightIsFolder = src->data(source_right, FolderNavigationModel::IsFolderRole)
165                                        .toBool();
166         if (leftIsFolder != rightIsFolder)
167             return leftIsFolder;
168     }
169     const QString leftName = src->data(source_left, QFileSystemModel::FileNameRole).toString();
170     const QString rightName = src->data(source_right, QFileSystemModel::FileNameRole).toString();
171     return Utils::FilePath::fromString(leftName) < Utils::FilePath::fromString(rightName);
172 }
173 
FolderNavigationModel(QObject * parent)174 FolderNavigationModel::FolderNavigationModel(QObject *parent) : QFileSystemModel(parent)
175 { }
176 
data(const QModelIndex & index,int role) const177 QVariant FolderNavigationModel::data(const QModelIndex &index, int role) const
178 {
179     if (role == Qt::ToolTipRole)
180         return QDir::toNativeSeparators(QDir::cleanPath(filePath(index)));
181     else if (role == IsFolderRole)
182         return isDir(index);
183     else
184         return QFileSystemModel::data(index, role);
185 }
186 
supportedDragActions() const187 Qt::DropActions FolderNavigationModel::supportedDragActions() const
188 {
189     return Qt::MoveAction;
190 }
191 
flags(const QModelIndex & index) const192 Qt::ItemFlags FolderNavigationModel::flags(const QModelIndex &index) const
193 {
194     if (index.isValid() && !fileInfo(index).isRoot())
195         return QFileSystemModel::flags(index) | Qt::ItemIsEditable;
196     return QFileSystemModel::flags(index);
197 }
198 
renamableFolderNodes(const Utils::FilePath & before,const Utils::FilePath & after)199 static QVector<FolderNode *> renamableFolderNodes(const Utils::FilePath &before,
200                                                   const Utils::FilePath &after)
201 {
202     QVector<FolderNode *> folderNodes;
203     ProjectTree::forEachNode([&](Node *node) {
204         if (node->asFileNode()
205                 && node->filePath() == before
206                 && node->parentFolderNode()
207                 && node->parentFolderNode()->canRenameFile(before, after)) {
208             folderNodes.append(node->parentFolderNode());
209         }
210     });
211     return folderNodes;
212 }
213 
projectNames(const QVector<FolderNode * > & folders)214 static QStringList projectNames(const QVector<FolderNode *> &folders)
215 {
216     const QStringList names = Utils::transform<QList>(folders, [](FolderNode *n) {
217         return n->managingProject()->filePath().fileName();
218     });
219     return Utils::filteredUnique(names);
220 }
221 
setData(const QModelIndex & index,const QVariant & value,int role)222 bool FolderNavigationModel::setData(const QModelIndex &index, const QVariant &value, int role)
223 {
224     QTC_ASSERT(index.isValid() && parent(index).isValid() && index.column() == 0
225                    && role == Qt::EditRole && value.canConvert<QString>(),
226                return false);
227     const QString afterFileName = value.toString();
228     const Utils::FilePath beforeFilePath = Utils::FilePath::fromString(filePath(index));
229     const Utils::FilePath parentPath = Utils::FilePath::fromString(filePath(parent(index)));
230     const Utils::FilePath afterFilePath = parentPath.pathAppended(afterFileName);
231     if (beforeFilePath == afterFilePath)
232         return false;
233     // need to rename through file system model, which takes care of not changing our selection
234     const bool success = QFileSystemModel::setData(index, value, role);
235     // for files we can do more than just rename on disk, for directories the user is on his/her own
236     if (success && fileInfo(index).isFile()) {
237         Core::DocumentManager::renamedFile(beforeFilePath, afterFilePath);
238         const QVector<FolderNode *> folderNodes = renamableFolderNodes(beforeFilePath,
239                                                                        afterFilePath);
240         QVector<FolderNode *> failedNodes;
241         for (FolderNode *folder : folderNodes) {
242             if (!folder->renameFile(beforeFilePath, afterFilePath))
243                 failedNodes.append(folder);
244         }
245         if (!failedNodes.isEmpty()) {
246             const QString projects = projectNames(failedNodes).join(", ");
247             const QString errorMessage
248                 = FolderNavigationWidget::tr("The file \"%1\" was renamed to \"%2\", "
249                      "but the following projects could not be automatically changed: %3")
250                       .arg(beforeFilePath.toUserOutput(), afterFilePath.toUserOutput(), projects);
251             QTimer::singleShot(0, Core::ICore::instance(), [errorMessage] {
252                 QMessageBox::warning(Core::ICore::dialogParent(),
253                                      ProjectExplorerPlugin::tr("Project Editing Failed"),
254                                      errorMessage);
255             });
256         }
257     }
258     return success;
259 }
260 
showOnlyFirstColumn(QTreeView * view)261 static void showOnlyFirstColumn(QTreeView *view)
262 {
263     const int columnCount = view->header()->count();
264     for (int i = 1; i < columnCount; ++i)
265         view->setColumnHidden(i, true);
266 }
267 
isChildOf(const QModelIndex & index,const QModelIndex & parent)268 static bool isChildOf(const QModelIndex &index, const QModelIndex &parent)
269 {
270     if (index == parent)
271         return true;
272     QModelIndex current = index;
273     while (current.isValid()) {
274         current = current.parent();
275         if (current == parent)
276             return true;
277     }
278     return false;
279 }
280 
281 /*!
282     \class FolderNavigationWidget
283 
284     Shows a file system tree, with the root directory selectable from a dropdown.
285 
286     \internal
287 */
FolderNavigationWidget(QWidget * parent)288 FolderNavigationWidget::FolderNavigationWidget(QWidget *parent) : QWidget(parent),
289     m_listView(new Utils::NavigationTreeView(this)),
290     m_fileSystemModel(new FolderNavigationModel(this)),
291     m_sortProxyModel(new FolderSortProxyModel(m_fileSystemModel)),
292     m_filterHiddenFilesAction(new QAction(tr("Show Hidden Files"), this)),
293     m_showBreadCrumbsAction(new QAction(tr("Show Bread Crumbs"), this)),
294     m_showFoldersOnTopAction(new QAction(tr("Show Folders on Top"), this)),
295     m_toggleSync(new QToolButton(this)),
296     m_toggleRootSync(new QToolButton(this)),
297     m_rootSelector(new QComboBox),
298     m_crumbContainer(new QWidget(this)),
299     m_crumbLabel(new DelayedFileCrumbLabel(this))
300 {
301     auto context = new Core::IContext(this);
302     context->setContext(Core::Context(C_FOLDERNAVIGATIONWIDGET));
303     context->setWidget(this);
304     Core::ICore::addContextObject(context);
305 
306     setBackgroundRole(QPalette::Base);
307     setAutoFillBackground(true);
308     m_sortProxyModel->setSourceModel(m_fileSystemModel);
309     m_sortProxyModel->setSortRole(FolderNavigationModel::IsFolderRole);
310     m_sortProxyModel->sort(0);
311     m_fileSystemModel->setResolveSymlinks(false);
312     m_fileSystemModel->setIconProvider(Core::FileIconProvider::iconProvider());
313     QDir::Filters filters = QDir::AllEntries | QDir::NoDotAndDotDot;
314     if (Utils::HostOsInfo::isWindowsHost()) // Symlinked directories can cause file watcher warnings on Win32.
315         filters |= QDir::NoSymLinks;
316     m_fileSystemModel->setFilter(filters);
317     m_fileSystemModel->setRootPath(QString());
318     m_filterHiddenFilesAction->setCheckable(true);
319     setHiddenFilesFilter(false);
320     m_showBreadCrumbsAction->setCheckable(true);
321     setShowBreadCrumbs(true);
322     m_showFoldersOnTopAction->setCheckable(true);
323     setShowFoldersOnTop(true);
324     m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
325     m_listView->setIconSize(QSize(16,16));
326     m_listView->setModel(m_sortProxyModel);
327     m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
328     m_listView->setDragEnabled(true);
329     m_listView->setDragDropMode(QAbstractItemView::DragOnly);
330     showOnlyFirstColumn(m_listView);
331     setFocusProxy(m_listView);
332 
333     auto selectorWidget = new Utils::StyledBar(this);
334     selectorWidget->setLightColored(true);
335     auto selectorLayout = new QHBoxLayout(selectorWidget);
336     selectorWidget->setLayout(selectorLayout);
337     selectorLayout->setSpacing(0);
338     selectorLayout->setContentsMargins(0, 0, 0, 0);
339     selectorLayout->addWidget(m_rootSelector, 10);
340 
341     auto crumbContainerLayout = new QVBoxLayout;
342     crumbContainerLayout->setSpacing(0);
343     crumbContainerLayout->setContentsMargins(0, 0, 0, 0);
344     m_crumbContainer->setLayout(crumbContainerLayout);
345     auto crumbLayout = new QVBoxLayout;
346     crumbLayout->setSpacing(0);
347     crumbLayout->setContentsMargins(4, 4, 4, 4);
348     crumbLayout->addWidget(m_crumbLabel);
349     crumbContainerLayout->addLayout(crumbLayout);
350     crumbContainerLayout->addWidget(createHLine());
351     m_crumbLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop);
352 
353     auto layout = new QVBoxLayout();
354     layout->addWidget(selectorWidget);
355     layout->addWidget(m_crumbContainer);
356     layout->addWidget(m_listView);
357     layout->setSpacing(0);
358     layout->setContentsMargins(0, 0, 0, 0);
359     setLayout(layout);
360 
361     m_toggleSync->setIcon(Utils::Icons::LINK_TOOLBAR.icon());
362     m_toggleSync->setCheckable(true);
363     m_toggleSync->setToolTip(tr("Synchronize with Editor"));
364 
365     m_toggleRootSync->setIcon(Utils::Icons::LINK.icon());
366     m_toggleRootSync->setCheckable(true);
367     m_toggleRootSync->setToolTip(tr("Synchronize Root Directory with Editor"));
368     selectorLayout->addWidget(m_toggleRootSync);
369 
370     // connections
371     connect(Core::EditorManager::instance(), &Core::EditorManager::currentEditorChanged,
372             this, &FolderNavigationWidget::handleCurrentEditorChanged);
373     connect(m_listView, &QAbstractItemView::activated, this, [this](const QModelIndex &index) {
374         openItem(m_sortProxyModel->mapToSource(index));
375     });
376     // Delay updating crumble path by event loop cylce, because that can scroll, which doesn't
377     // work well when done directly in currentChanged (the wrong item can get highlighted).
378     // We cannot use Qt::QueuedConnection directly, because the QModelIndex could get invalidated
379     // in the meantime, so use a queued invokeMethod instead.
380     connect(m_listView->selectionModel(),
381             &QItemSelectionModel::currentChanged,
382             this,
383             [this](const QModelIndex &index) {
384                 const QModelIndex sourceIndex = m_sortProxyModel->mapToSource(index);
385                 const auto filePath = Utils::FilePath::fromString(
386                     m_fileSystemModel->filePath(sourceIndex));
387                 // QTimer::singleShot only posts directly onto the event loop if you use the SLOT("...")
388                 // notation, so using a singleShot with a lambda would flicker
389                 // QTimer::singleShot(0, this, [this, filePath]() { setCrumblePath(filePath); });
390                 QMetaObject::invokeMethod(this, [this, filePath] { setCrumblePath(filePath); },
391                                           Qt::QueuedConnection);
392             });
393     connect(m_crumbLabel, &Utils::FileCrumbLabel::pathClicked, [this](const Utils::FilePath &path) {
394         const QModelIndex rootIndex = m_sortProxyModel->mapToSource(m_listView->rootIndex());
395         const QModelIndex fileIndex = m_fileSystemModel->index(path.toString());
396         if (!isChildOf(fileIndex, rootIndex))
397             selectBestRootForFile(path);
398         selectFile(path);
399     });
400     connect(m_filterHiddenFilesAction,
401             &QAction::toggled,
402             this,
403             &FolderNavigationWidget::setHiddenFilesFilter);
404     connect(m_showBreadCrumbsAction,
405             &QAction::toggled,
406             this,
407             &FolderNavigationWidget::setShowBreadCrumbs);
408     connect(m_showFoldersOnTopAction,
409             &QAction::toggled,
410             this,
411             &FolderNavigationWidget::setShowFoldersOnTop);
412     connect(m_toggleSync,
413             &QAbstractButton::clicked,
414             this,
415             &FolderNavigationWidget::toggleAutoSynchronization);
416     connect(m_toggleRootSync, &QAbstractButton::clicked,
417             this, [this]() { setRootAutoSynchronization(!m_rootAutoSync); });
418     connect(m_rootSelector,
419             QOverload<int>::of(&QComboBox::currentIndexChanged),
420             this,
421             [this](int index) {
422                 const auto directory = m_rootSelector->itemData(index).value<Utils::FilePath>();
423                 m_rootSelector->setToolTip(directory.toUserOutput());
424                 setRootDirectory(directory);
425                 const QModelIndex rootIndex = m_sortProxyModel->mapToSource(m_listView->rootIndex());
426                 const QModelIndex fileIndex = m_sortProxyModel->mapToSource(m_listView->currentIndex());
427                 if (!isChildOf(fileIndex, rootIndex))
428                     selectFile(directory);
429             });
430 
431     setAutoSynchronization(true);
432     setRootAutoSynchronization(true);
433 }
434 
toggleAutoSynchronization()435 void FolderNavigationWidget::toggleAutoSynchronization()
436 {
437     setAutoSynchronization(!m_autoSync);
438 }
439 
setShowBreadCrumbs(bool show)440 void FolderNavigationWidget::setShowBreadCrumbs(bool show)
441 {
442     m_showBreadCrumbsAction->setChecked(show);
443     m_crumbContainer->setVisible(show);
444 }
445 
setShowFoldersOnTop(bool onTop)446 void FolderNavigationWidget::setShowFoldersOnTop(bool onTop)
447 {
448     m_showFoldersOnTopAction->setChecked(onTop);
449     m_sortProxyModel->setSortRole(onTop ? int(FolderNavigationModel::IsFolderRole)
450                                         : int(QFileSystemModel::FileNameRole));
451 }
452 
itemLessThan(QComboBox * combo,int index,const FolderNavigationWidgetFactory::RootDirectory & directory)453 static bool itemLessThan(QComboBox *combo,
454                          int index,
455                          const FolderNavigationWidgetFactory::RootDirectory &directory)
456 {
457     return combo->itemData(index, SORT_ROLE).toInt() < directory.sortValue
458            || (combo->itemData(index, SORT_ROLE).toInt() == directory.sortValue
459                && combo->itemData(index, Qt::DisplayRole).toString() < directory.displayName);
460 }
461 
insertRootDirectory(const FolderNavigationWidgetFactory::RootDirectory & directory)462 void FolderNavigationWidget::insertRootDirectory(
463     const FolderNavigationWidgetFactory::RootDirectory &directory)
464 {
465     // Find existing. Do not remove yet, to not mess up the current selection.
466     int previousIndex = 0;
467     while (previousIndex < m_rootSelector->count()
468            && m_rootSelector->itemData(previousIndex, ID_ROLE).toString() != directory.id)
469         ++previousIndex;
470     // Insert sorted.
471     int index = 0;
472     while (index < m_rootSelector->count() && itemLessThan(m_rootSelector, index, directory))
473         ++index;
474     m_rootSelector->insertItem(index, directory.displayName);
475     if (index <= previousIndex) // item was inserted, update previousIndex
476         ++previousIndex;
477     m_rootSelector->setItemData(index, QVariant::fromValue(directory.path), PATH_ROLE);
478     m_rootSelector->setItemData(index, directory.id, ID_ROLE);
479     m_rootSelector->setItemData(index, directory.sortValue, SORT_ROLE);
480     m_rootSelector->setItemData(index, directory.path.toUserOutput(), Qt::ToolTipRole);
481     m_rootSelector->setItemIcon(index, directory.icon);
482     if (m_rootSelector->currentIndex() == previousIndex)
483         m_rootSelector->setCurrentIndex(index);
484     if (previousIndex < m_rootSelector->count())
485         m_rootSelector->removeItem(previousIndex);
486     if (m_autoSync) // we might find a better root for current selection now
487         handleCurrentEditorChanged(Core::EditorManager::currentEditor());
488 }
489 
removeRootDirectory(const QString & id)490 void FolderNavigationWidget::removeRootDirectory(const QString &id)
491 {
492     for (int i = 0; i < m_rootSelector->count(); ++i) {
493         if (m_rootSelector->itemData(i, ID_ROLE).toString() == id) {
494             m_rootSelector->removeItem(i);
495             break;
496         }
497     }
498     if (m_autoSync) // we might need to find a new root for current selection
499         handleCurrentEditorChanged(Core::EditorManager::currentEditor());
500 }
501 
addNewItem()502 void FolderNavigationWidget::addNewItem()
503 {
504     const QModelIndex current = m_sortProxyModel->mapToSource(m_listView->currentIndex());
505     if (!current.isValid())
506         return;
507     const auto filePath = Utils::FilePath::fromString(m_fileSystemModel->filePath(current));
508     const Utils::FilePath path = filePath.isDir() ? filePath : filePath.parentDir();
509     Core::ICore::showNewItemDialog(ProjectExplorerPlugin::tr("New File", "Title of dialog"),
510                                    Utils::filtered(Core::IWizardFactory::allWizardFactories(),
511                                                    Utils::equal(&Core::IWizardFactory::kind,
512                                                                 Core::IWizardFactory::FileWizard)),
513                                    path.toString());
514 }
515 
editCurrentItem()516 void FolderNavigationWidget::editCurrentItem()
517 {
518     const QModelIndex current = m_listView->currentIndex();
519     if (m_listView->model()->flags(current) & Qt::ItemIsEditable)
520         m_listView->edit(current);
521 }
522 
removableFolderNodes(const Utils::FilePath & filePath)523 static QVector<FolderNode *> removableFolderNodes(const Utils::FilePath &filePath)
524 {
525     QVector<FolderNode *> folderNodes;
526     ProjectTree::forEachNode([&](Node *node) {
527         if (node->asFileNode()
528                 && node->filePath() == filePath
529                 && node->parentFolderNode()
530                 && node->parentFolderNode()->supportsAction(RemoveFile, node)) {
531             folderNodes.append(node->parentFolderNode());
532         }
533     });
534     return folderNodes;
535 }
536 
removeCurrentItem()537 void FolderNavigationWidget::removeCurrentItem()
538 {
539     const QModelIndex current = m_sortProxyModel->mapToSource(m_listView->currentIndex());
540     if (!current.isValid() || m_fileSystemModel->isDir(current))
541         return;
542     const Utils::FilePath filePath = Utils::FilePath::fromString(m_fileSystemModel->filePath(current));
543     Utils::RemoveFileDialog dialog(filePath.toString(), Core::ICore::dialogParent());
544     dialog.setDeleteFileVisible(false);
545     if (dialog.exec() == QDialog::Accepted) {
546         const QVector<FolderNode *> folderNodes = removableFolderNodes(filePath);
547         const QVector<FolderNode *> failedNodes = Utils::filtered(folderNodes,
548                 [filePath](FolderNode *folder) {
549                     return folder->removeFiles({filePath}) != RemovedFilesFromProject::Ok;
550         });
551         Core::FileChangeBlocker changeGuard(filePath);
552         Core::FileUtils::removeFiles({filePath}, true /*delete from disk*/);
553         if (!failedNodes.isEmpty()) {
554             const QString projects = projectNames(failedNodes).join(", ");
555             const QString errorMessage
556                 = tr("The following projects failed to automatically remove the file: %1")
557                       .arg(projects);
558             QTimer::singleShot(0, Core::ICore::instance(), [errorMessage] {
559                 QMessageBox::warning(Core::ICore::dialogParent(),
560                                      ProjectExplorerPlugin::tr("Project Editing Failed"),
561                                      errorMessage);
562             });
563         }
564     }
565 }
566 
autoSynchronization() const567 bool FolderNavigationWidget::autoSynchronization() const
568 {
569     return m_autoSync;
570 }
571 
setAutoSynchronization(bool sync)572 void FolderNavigationWidget::setAutoSynchronization(bool sync)
573 {
574     m_toggleSync->setChecked(sync);
575     m_toggleRootSync->setEnabled(sync);
576     m_toggleRootSync->setChecked(sync ? m_rootAutoSync : false);
577     if (sync == m_autoSync)
578         return;
579     m_autoSync = sync;
580     if (m_autoSync)
581         handleCurrentEditorChanged(Core::EditorManager::currentEditor());
582 }
583 
setRootAutoSynchronization(bool sync)584 void FolderNavigationWidget::setRootAutoSynchronization(bool sync)
585 {
586     m_toggleRootSync->setChecked(sync);
587     if (sync == m_rootAutoSync)
588         return;
589     m_rootAutoSync = sync;
590     if (m_rootAutoSync)
591         handleCurrentEditorChanged(Core::EditorManager::currentEditor());
592 }
593 
handleCurrentEditorChanged(Core::IEditor * editor)594 void FolderNavigationWidget::handleCurrentEditorChanged(Core::IEditor *editor)
595 {
596     if (!m_autoSync || !editor || editor->document()->filePath().isEmpty()
597             || editor->document()->isTemporary())
598         return;
599     const Utils::FilePath filePath = editor->document()->filePath();
600     if (m_rootAutoSync)
601         selectBestRootForFile(filePath);
602     selectFile(filePath);
603 }
604 
selectBestRootForFile(const Utils::FilePath & filePath)605 void FolderNavigationWidget::selectBestRootForFile(const Utils::FilePath &filePath)
606 {
607     const int bestRootIndex = bestRootForFile(filePath);
608     m_rootSelector->setCurrentIndex(bestRootIndex);
609 }
610 
selectFile(const Utils::FilePath & filePath)611 void FolderNavigationWidget::selectFile(const Utils::FilePath &filePath)
612 {
613     const QModelIndex fileIndex = m_sortProxyModel->mapFromSource(
614         m_fileSystemModel->index(filePath.toString()));
615     if (fileIndex.isValid() || filePath.isEmpty() /* Computer root */) {
616         // TODO This only scrolls to the right position if all directory contents are loaded.
617         // Unfortunately listening to directoryLoaded was still not enough (there might also
618         // be some delayed sorting involved?).
619         // Use magic timer for scrolling.
620         m_listView->setCurrentIndex(fileIndex);
621         QTimer::singleShot(200, this, [this, filePath] {
622             const QModelIndex fileIndex = m_sortProxyModel->mapFromSource(
623                 m_fileSystemModel->index(filePath.toString()));
624             if (fileIndex == m_listView->rootIndex()) {
625                 m_listView->horizontalScrollBar()->setValue(0);
626                 m_listView->verticalScrollBar()->setValue(0);
627             } else {
628                 m_listView->scrollTo(fileIndex);
629             }
630             setCrumblePath(filePath);
631         });
632     }
633 }
634 
setRootDirectory(const Utils::FilePath & directory)635 void FolderNavigationWidget::setRootDirectory(const Utils::FilePath &directory)
636 {
637     const QModelIndex index = m_sortProxyModel->mapFromSource(
638         m_fileSystemModel->setRootPath(directory.toString()));
639     m_listView->setRootIndex(index);
640 }
641 
bestRootForFile(const Utils::FilePath & filePath)642 int FolderNavigationWidget::bestRootForFile(const Utils::FilePath &filePath)
643 {
644     int index = 0; // Computer is default
645     int commonLength = 0;
646     for (int i = 1; i < m_rootSelector->count(); ++i) {
647         const auto root = m_rootSelector->itemData(i).value<Utils::FilePath>();
648         if (filePath.isChildOf(root) && root.toString().size() > commonLength) {
649             index = i;
650             commonLength = root.toString().size();
651         }
652     }
653     return index;
654 }
655 
openItem(const QModelIndex & index)656 void FolderNavigationWidget::openItem(const QModelIndex &index)
657 {
658     QTC_ASSERT(index.isValid(), return);
659     // signal "activate" is also sent when double-clicking folders
660     // but we don't want to do anything in that case
661     if (m_fileSystemModel->isDir(index))
662         return;
663     const QString path = m_fileSystemModel->filePath(index);
664     Core::EditorManager::openEditor(path);
665 }
666 
projectsInDirectory(const QModelIndex & index) const667 QStringList FolderNavigationWidget::projectsInDirectory(const QModelIndex &index) const
668 {
669     QTC_ASSERT(index.isValid() && m_fileSystemModel->isDir(index), return {});
670     const QFileInfo fi = m_fileSystemModel->fileInfo(index);
671     if (!fi.isReadable() || !fi.isExecutable())
672         return {};
673     const QString path = m_fileSystemModel->filePath(index);
674     // Try to find project files in directory and open those.
675     return FolderNavigationWidget::projectFilesInDirectory(path);
676 }
677 
openProjectsInDirectory(const QModelIndex & index)678 void FolderNavigationWidget::openProjectsInDirectory(const QModelIndex &index)
679 {
680     const QStringList projectFiles = projectsInDirectory(index);
681     if (!projectFiles.isEmpty())
682         Core::ICore::openFiles(projectFiles);
683 }
684 
createNewFolder(const QModelIndex & parent)685 void FolderNavigationWidget::createNewFolder(const QModelIndex &parent)
686 {
687     static const QString baseName = tr("New Folder");
688     // find non-existing name
689     const QDir dir(m_fileSystemModel->filePath(parent));
690     const QSet<Utils::FilePath> existingItems
691         = Utils::transform<QSet>(dir.entryList({baseName + '*'}, QDir::AllEntries),
692                                  [](const QString &entry) {
693                                      return Utils::FilePath::fromString(entry);
694                                  });
695     const Utils::FilePath name = Utils::makeUniquelyNumbered(Utils::FilePath::fromString(baseName),
696                                                    existingItems);
697     // create directory and edit
698     const QModelIndex index = m_sortProxyModel->mapFromSource(
699         m_fileSystemModel->mkdir(parent, name.toString()));
700     if (!index.isValid())
701         return;
702     m_listView->setCurrentIndex(index);
703     m_listView->edit(index);
704 }
705 
setCrumblePath(const Utils::FilePath & filePath)706 void FolderNavigationWidget::setCrumblePath(const Utils::FilePath &filePath)
707 {
708     const QModelIndex index = m_fileSystemModel->index(filePath.toString());
709     const int width = m_crumbLabel->width();
710     const int previousHeight = m_crumbLabel->immediateHeightForWidth(width);
711     m_crumbLabel->setPath(filePath);
712     const int currentHeight = m_crumbLabel->immediateHeightForWidth(width);
713     const int diff = currentHeight - previousHeight;
714     if (diff != 0 && m_crumbLabel->isVisible()) {
715         // try to fix scroll position, otherwise delay layouting
716         QScrollBar *bar = m_listView->verticalScrollBar();
717         const int newBarValue = bar ? bar->value() + diff : 0;
718         const QRect currentItemRect = m_listView->visualRect(index);
719         const int currentItemVStart = currentItemRect.y();
720         const int currentItemVEnd = currentItemVStart + currentItemRect.height();
721         const bool currentItemStillVisibleAsBefore = (diff < 0 || currentItemVStart > diff
722                                                       || currentItemVEnd <= 0);
723         if (bar && bar->minimum() <= newBarValue && bar->maximum() >= newBarValue
724                 && currentItemStillVisibleAsBefore) {
725             // we need to set the scroll bar when the layout request from the crumble path is
726             // handled, otherwise it will flicker
727             m_crumbLabel->setScrollBarOnce(bar, newBarValue);
728         } else {
729             m_crumbLabel->delayLayoutOnce();
730         }
731     }
732 }
733 
contextMenuEvent(QContextMenuEvent * ev)734 void FolderNavigationWidget::contextMenuEvent(QContextMenuEvent *ev)
735 {
736     QMenu menu;
737     // Open current item
738     const QModelIndex current = m_sortProxyModel->mapToSource(m_listView->currentIndex());
739     const bool hasCurrentItem = current.isValid();
740     QAction *actionOpenFile = nullptr;
741     QAction *actionOpenProjects = nullptr;
742     QAction *actionOpenAsProject = nullptr;
743     QAction *newFolder = nullptr;
744     const bool isDir = m_fileSystemModel->isDir(current);
745     const Utils::FilePath filePath = hasCurrentItem ? Utils::FilePath::fromString(
746                                                           m_fileSystemModel->filePath(current))
747                                                     : Utils::FilePath();
748     if (hasCurrentItem) {
749         const QString fileName = m_fileSystemModel->fileName(current);
750         if (isDir) {
751             actionOpenProjects = menu.addAction(tr("Open Project in \"%1\"").arg(fileName));
752             if (projectsInDirectory(current).isEmpty())
753                 actionOpenProjects->setEnabled(false);
754         } else {
755             actionOpenFile = menu.addAction(tr("Open \"%1\"").arg(fileName));
756             if (ProjectExplorerPlugin::isProjectFile(Utils::FilePath::fromString(fileName)))
757                 actionOpenAsProject = menu.addAction(tr("Open Project \"%1\"").arg(fileName));
758         }
759     }
760 
761     // we need dummy DocumentModel::Entry with absolute file path in it
762     // to get EditorManager::addNativeDirAndOpenWithActions() working
763     Core::DocumentModel::Entry fakeEntry;
764     Core::IDocument document;
765     document.setFilePath(filePath);
766     fakeEntry.document = &document;
767     Core::EditorManager::addNativeDirAndOpenWithActions(&menu, &fakeEntry);
768 
769     if (hasCurrentItem) {
770         menu.addAction(Core::ActionManager::command(Constants::ADDNEWFILE)->action());
771         if (!isDir)
772             menu.addAction(Core::ActionManager::command(Constants::REMOVEFILE)->action());
773         if (m_fileSystemModel->flags(current) & Qt::ItemIsEditable)
774             menu.addAction(Core::ActionManager::command(Constants::RENAMEFILE)->action());
775         newFolder = menu.addAction(tr("New Folder"));
776         if (!isDir && Core::DiffService::instance()) {
777             menu.addAction(
778                 TextEditor::TextDocument::createDiffAgainstCurrentFileAction(&menu, [filePath]() {
779                     return filePath;
780                 }));
781         }
782     }
783 
784     menu.addSeparator();
785     QAction * const collapseAllAction = menu.addAction(ProjectExplorerPlugin::tr("Collapse All"));
786 
787     QAction *action = menu.exec(ev->globalPos());
788     if (!action)
789         return;
790 
791     ev->accept();
792     if (action == actionOpenFile) {
793         openItem(current);
794     } else if (action == actionOpenAsProject) {
795         ProjectExplorerPlugin::openProject(filePath.toString());
796     } else if (action == actionOpenProjects)
797         openProjectsInDirectory(current);
798     else if (action == newFolder) {
799         if (isDir)
800             createNewFolder(current);
801         else
802             createNewFolder(current.parent());
803     } else if (action == collapseAllAction) {
804         m_listView->collapseAll();
805     }
806 }
807 
rootAutoSynchronization() const808 bool FolderNavigationWidget::rootAutoSynchronization() const
809 {
810     return m_rootAutoSync;
811 }
812 
setHiddenFilesFilter(bool filter)813 void FolderNavigationWidget::setHiddenFilesFilter(bool filter)
814 {
815     QDir::Filters filters = m_fileSystemModel->filter();
816     if (filter)
817         filters |= QDir::Hidden;
818     else
819         filters &= ~(QDir::Hidden);
820     m_fileSystemModel->setFilter(filters);
821     m_filterHiddenFilesAction->setChecked(filter);
822 }
823 
hiddenFilesFilter() const824 bool FolderNavigationWidget::hiddenFilesFilter() const
825 {
826     return m_filterHiddenFilesAction->isChecked();
827 }
828 
isShowingBreadCrumbs() const829 bool FolderNavigationWidget::isShowingBreadCrumbs() const
830 {
831     return m_showBreadCrumbsAction->isChecked();
832 }
833 
isShowingFoldersOnTop() const834 bool FolderNavigationWidget::isShowingFoldersOnTop() const
835 {
836     return m_showFoldersOnTopAction->isChecked();
837 }
838 
projectFilesInDirectory(const QString & path)839 QStringList FolderNavigationWidget::projectFilesInDirectory(const QString &path)
840 {
841     QDir dir(path);
842     QStringList projectFiles;
843     foreach (const QFileInfo &i, dir.entryInfoList(ProjectExplorerPlugin::projectFileGlobs(), QDir::Files))
844         projectFiles.append(i.absoluteFilePath());
845     return projectFiles;
846 }
847 
848 // --------------------FolderNavigationWidgetFactory
FolderNavigationWidgetFactory()849 FolderNavigationWidgetFactory::FolderNavigationWidgetFactory()
850 {
851     m_instance = this;
852     setDisplayName(tr("File System"));
853     setPriority(400);
854     setId("File System");
855     setActivationSequence(QKeySequence(Core::useMacShortcuts ? tr("Meta+Y,Meta+F")
856                                                              : tr("Alt+Y,Alt+F")));
857     insertRootDirectory({QLatin1String("A.Computer"),
858                          0 /*sortValue*/,
859                          FolderNavigationWidget::tr("Computer"),
860                          Utils::FilePath(),
861                          Icons::DESKTOP_DEVICE_SMALL.icon()});
862     insertRootDirectory({QLatin1String("A.Home"),
863                          10 /*sortValue*/,
864                          FolderNavigationWidget::tr("Home"),
865                          Utils::FilePath::fromString(QDir::homePath()),
866                          Utils::Icons::HOME.icon()});
867     updateProjectsDirectoryRoot();
868     connect(Core::DocumentManager::instance(),
869             &Core::DocumentManager::projectsDirectoryChanged,
870             this,
871             &FolderNavigationWidgetFactory::updateProjectsDirectoryRoot);
872     registerActions();
873 }
874 
createWidget()875 Core::NavigationView FolderNavigationWidgetFactory::createWidget()
876 {
877     auto fnw = new FolderNavigationWidget;
878     for (const RootDirectory &root : qAsConst(m_rootDirectories))
879         fnw->insertRootDirectory(root);
880     connect(this,
881             &FolderNavigationWidgetFactory::rootDirectoryAdded,
882             fnw,
883             &FolderNavigationWidget::insertRootDirectory);
884     connect(this,
885             &FolderNavigationWidgetFactory::rootDirectoryRemoved,
886             fnw,
887             &FolderNavigationWidget::removeRootDirectory);
888 
889     Core::NavigationView n;
890     n.widget = fnw;
891     auto filter = new QToolButton;
892     filter->setIcon(Utils::Icons::FILTER.icon());
893     filter->setToolTip(tr("Options"));
894     filter->setPopupMode(QToolButton::InstantPopup);
895     filter->setProperty("noArrow", true);
896     auto filterMenu = new QMenu(filter);
897     filterMenu->addAction(fnw->m_filterHiddenFilesAction);
898     filterMenu->addAction(fnw->m_showBreadCrumbsAction);
899     filterMenu->addAction(fnw->m_showFoldersOnTopAction);
900     filter->setMenu(filterMenu);
901     n.dockToolBarWidgets << filter << fnw->m_toggleSync;
902     return n;
903 }
904 
905 const bool kHiddenFilesDefault = false;
906 const bool kAutoSyncDefault = true;
907 const bool kShowBreadCrumbsDefault = true;
908 const bool kRootAutoSyncDefault = true;
909 
saveSettings(Utils::QtcSettings * settings,int position,QWidget * widget)910 void FolderNavigationWidgetFactory::saveSettings(Utils::QtcSettings *settings,
911                                                  int position,
912                                                  QWidget *widget)
913 {
914     auto fnw = qobject_cast<FolderNavigationWidget *>(widget);
915     QTC_ASSERT(fnw, return);
916     const QString base = kSettingsBase + QString::number(position);
917     settings->setValueWithDefault(base + kHiddenFilesKey,
918                                   fnw->hiddenFilesFilter(),
919                                   kHiddenFilesDefault);
920     settings->setValueWithDefault(base + kSyncKey, fnw->autoSynchronization(), kAutoSyncDefault);
921     settings->setValueWithDefault(base + kShowBreadCrumbs,
922                                   fnw->isShowingBreadCrumbs(),
923                                   kShowBreadCrumbsDefault);
924     settings->setValueWithDefault(base + kSyncRootWithEditor,
925                                   fnw->rootAutoSynchronization(),
926                                   kRootAutoSyncDefault);
927 }
928 
restoreSettings(QSettings * settings,int position,QWidget * widget)929 void FolderNavigationWidgetFactory::restoreSettings(QSettings *settings, int position, QWidget *widget)
930 {
931     auto fnw = qobject_cast<FolderNavigationWidget *>(widget);
932     QTC_ASSERT(fnw, return);
933     const QString base = kSettingsBase + QString::number(position);
934     fnw->setHiddenFilesFilter(settings->value(base + kHiddenFilesKey, kHiddenFilesDefault).toBool());
935     fnw->setAutoSynchronization(settings->value(base + kSyncKey, kAutoSyncDefault).toBool());
936     fnw->setShowBreadCrumbs(
937         settings->value(base + kShowBreadCrumbs, kShowBreadCrumbsDefault).toBool());
938     fnw->setRootAutoSynchronization(
939         settings->value(base + kSyncRootWithEditor, kRootAutoSyncDefault).toBool());
940 }
941 
insertRootDirectory(const RootDirectory & directory)942 void FolderNavigationWidgetFactory::insertRootDirectory(const RootDirectory &directory)
943 {
944     const int index = rootIndex(directory.id);
945     if (index < 0)
946         m_rootDirectories.append(directory);
947     else
948         m_rootDirectories[index] = directory;
949     emit m_instance->rootDirectoryAdded(directory);
950 }
951 
removeRootDirectory(const QString & id)952 void FolderNavigationWidgetFactory::removeRootDirectory(const QString &id)
953 {
954     const int index = rootIndex(id);
955     QTC_ASSERT(index >= 0, return );
956     m_rootDirectories.removeAt(index);
957     emit m_instance->rootDirectoryRemoved(id);
958 }
959 
rootIndex(const QString & id)960 int FolderNavigationWidgetFactory::rootIndex(const QString &id)
961 {
962     return Utils::indexOf(m_rootDirectories,
963                           [id](const RootDirectory &entry) { return entry.id == id; });
964 }
965 
updateProjectsDirectoryRoot()966 void FolderNavigationWidgetFactory::updateProjectsDirectoryRoot()
967 {
968     insertRootDirectory({QLatin1String(PROJECTSDIRECTORYROOT_ID),
969                          20 /*sortValue*/,
970                          FolderNavigationWidget::tr("Projects"),
971                          Core::DocumentManager::projectsDirectory(),
972                          Utils::Icons::PROJECT.icon()});
973 }
974 
currentFolderNavigationWidget()975 static FolderNavigationWidget *currentFolderNavigationWidget()
976 {
977     return qobject_cast<FolderNavigationWidget *>(Core::ICore::currentContextWidget());
978 }
979 
registerActions()980 void FolderNavigationWidgetFactory::registerActions()
981 {
982     Core::Context context(C_FOLDERNAVIGATIONWIDGET);
983 
984     auto add = new QAction(tr("Add New..."), this);
985     Core::ActionManager::registerAction(add, Constants::ADDNEWFILE, context);
986     connect(add, &QAction::triggered, Core::ICore::instance(), [] {
987         if (auto navWidget = currentFolderNavigationWidget())
988             navWidget->addNewItem();
989     });
990 
991     auto rename = new QAction(tr("Rename..."), this);
992     Core::ActionManager::registerAction(rename, Constants::RENAMEFILE, context);
993     connect(rename, &QAction::triggered, Core::ICore::instance(), [] {
994         if (auto navWidget = currentFolderNavigationWidget())
995             navWidget->editCurrentItem();
996     });
997 
998     auto remove = new QAction(tr("Remove..."), this);
999     Core::ActionManager::registerAction(remove, Constants::REMOVEFILE, context);
1000     connect(remove, &QAction::triggered, Core::ICore::instance(), [] {
1001         if (auto navWidget = currentFolderNavigationWidget())
1002             navWidget->removeCurrentItem();
1003     });
1004 }
1005 
immediateHeightForWidth(int w) const1006 int DelayedFileCrumbLabel::immediateHeightForWidth(int w) const
1007 {
1008     return Utils::FileCrumbLabel::heightForWidth(w);
1009 }
1010 
heightForWidth(int w) const1011 int DelayedFileCrumbLabel::heightForWidth(int w) const
1012 {
1013     static const int doubleDefaultInterval = 800;
1014     static QHash<int, int> oldHeight;
1015     setScrollBarOnce();
1016     int newHeight = Utils::FileCrumbLabel::heightForWidth(w);
1017     if (!m_delaying || !oldHeight.contains(w)) {
1018         oldHeight.insert(w, newHeight);
1019     } else if (oldHeight.value(w) != newHeight){
1020         auto that = const_cast<DelayedFileCrumbLabel *>(this);
1021         QTimer::singleShot(std::max(2 * QApplication::doubleClickInterval(), doubleDefaultInterval),
1022                            that,
1023                            [that, w, newHeight] {
1024                                oldHeight.insert(w, newHeight);
1025                                that->m_delaying = false;
1026                                that->updateGeometry();
1027                            });
1028     }
1029     return oldHeight.value(w);
1030 }
1031 
delayLayoutOnce()1032 void DelayedFileCrumbLabel::delayLayoutOnce()
1033 {
1034     m_delaying = true;
1035 }
1036 
setScrollBarOnce(QScrollBar * bar,int value)1037 void DelayedFileCrumbLabel::setScrollBarOnce(QScrollBar *bar, int value)
1038 {
1039     m_bar = bar;
1040     m_barValue = value;
1041 }
1042 
setScrollBarOnce() const1043 void DelayedFileCrumbLabel::setScrollBarOnce() const
1044 {
1045     if (!m_bar)
1046         return;
1047     auto that = const_cast<DelayedFileCrumbLabel *>(this);
1048     that->m_bar->setValue(m_barValue);
1049     that->m_bar.clear();
1050 }
1051 
1052 } // namespace Internal
1053 } // namespace ProjectExplorer
1054