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