diff --git a/src/plugins/projectexplorer/foldernavigationwidget.cpp b/src/plugins/projectexplorer/foldernavigationwidget.cpp index 8742c885cf2..2733b891bae 100644 --- a/src/plugins/projectexplorer/foldernavigationwidget.cpp +++ b/src/plugins/projectexplorer/foldernavigationwidget.cpp @@ -26,8 +26,6 @@ #include "foldernavigationwidget.h" #include "projectexplorer.h" -#include - #include #include #include @@ -35,62 +33,34 @@ #include #include #include -#include - -#include +#include #include -#include #include -#include -#include +#include #include -#include +#include +#include #include +#include #include #include #include -#include #include #include -#include #include #include #include -enum { debug = 0 }; - namespace ProjectExplorer { namespace Internal { -// Hide the '.' entry. -class DotRemovalFilter : public QSortFilterProxyModel -{ - Q_OBJECT -public: - explicit DotRemovalFilter(QObject *parent = nullptr); -protected: - virtual bool filterAcceptsRow(int source_row, const QModelIndex &parent) const; - Qt::DropActions supportedDragActions() const; -}; +static FolderNavigationWidgetFactory *m_instance = nullptr; -DotRemovalFilter::DotRemovalFilter(QObject *parent) : QSortFilterProxyModel(parent) -{ } - -bool DotRemovalFilter::filterAcceptsRow(int source_row, const QModelIndex &parent) const -{ - const QVariant fileName = sourceModel()->data(parent.child(source_row, 0)); - if (Utils::HostOsInfo::isAnyUnixHost()) - if (sourceModel()->data(parent) == QLatin1String("/") && fileName == QLatin1String("..")) - return false; - return fileName != QLatin1String("."); -} - -Qt::DropActions DotRemovalFilter::supportedDragActions() const -{ - return sourceModel()->supportedDragActions(); -} +QVector + FolderNavigationWidgetFactory::m_rootDirectories = { + {FolderNavigationWidget::tr("Computer"), Utils::FileName()}}; // FolderNavigationModel: Shows path as tooltip. class FolderNavigationModel : public QFileSystemModel @@ -117,42 +87,46 @@ Qt::DropActions FolderNavigationModel::supportedDragActions() const return Qt::MoveAction; } -/*! - \class FolderNavigationWidget +static void showOnlyFirstColumn(QTreeView *view) +{ + const int columnCount = view->header()->count(); + for (int i = 1; i < columnCount; ++i) + view->setColumnHidden(i, true); +} - Shows a file system folder - */ +/*! + \class FolderNavigationWidget + + Shows a file system tree, with the root directory selectable from a dropdown. + + \internal +*/ FolderNavigationWidget::FolderNavigationWidget(QWidget *parent) : QWidget(parent), - m_listView(new Utils::ListView(this)), + m_listView(new Utils::NavigationTreeView(this)), m_fileSystemModel(new FolderNavigationModel(this)), m_filterHiddenFilesAction(new QAction(tr("Show Hidden Files"), this)), - m_filterModel(new DotRemovalFilter(this)), - m_title(new Utils::ElidingLabel(this)), - m_toggleSync(new QToolButton(this)) + m_toggleSync(new QToolButton(this)), + m_rootSelector(new QComboBox) { m_fileSystemModel->setResolveSymlinks(false); m_fileSystemModel->setIconProvider(Core::FileIconProvider::iconProvider()); - QDir::Filters filters = QDir::AllDirs | QDir::Files | QDir::Drives - | QDir::Readable| QDir::Writable - | QDir::Executable | QDir::Hidden; + QDir::Filters filters = QDir::AllEntries | QDir::NoDotAndDotDot; if (Utils::HostOsInfo::isWindowsHost()) // Symlinked directories can cause file watcher warnings on Win32. filters |= QDir::NoSymLinks; m_fileSystemModel->setFilter(filters); - m_filterModel->setSourceModel(m_fileSystemModel); + m_fileSystemModel->setRootPath(QString()); m_filterHiddenFilesAction->setCheckable(true); setHiddenFilesFilter(false); m_listView->setIconSize(QSize(16,16)); - m_listView->setModel(m_filterModel); - m_listView->setFrameStyle(QFrame::NoFrame); - m_listView->setAttribute(Qt::WA_MacShowFocusRect, false); + m_listView->setModel(m_fileSystemModel); m_listView->setDragEnabled(true); m_listView->setDragDropMode(QAbstractItemView::DragOnly); + showOnlyFirstColumn(m_listView); setFocusProxy(m_listView); auto layout = new QVBoxLayout(); - layout->addWidget(m_title); + layout->addWidget(m_rootSelector); layout->addWidget(m_listView); - m_title->setMargin(5); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); @@ -164,13 +138,26 @@ FolderNavigationWidget::FolderNavigationWidget(QWidget *parent) : QWidget(parent // connections connect(m_listView, &QAbstractItemView::activated, - this, &FolderNavigationWidget::slotOpenItem); + this, [this](const QModelIndex &index) { openItem(index); }); connect(m_filterHiddenFilesAction, &QAction::toggled, this, &FolderNavigationWidget::setHiddenFilesFilter); connect(m_toggleSync, &QAbstractButton::clicked, this, &FolderNavigationWidget::toggleAutoSynchronization); - connect(m_filterModel, &QAbstractItemModel::layoutChanged, - this, &FolderNavigationWidget::ensureCurrentIndex); + connect(m_rootSelector, + static_cast(&QComboBox::currentIndexChanged), + this, + [this](int index) { + const auto directory = m_rootSelector->itemData(index).value(); + m_rootSelector->setToolTip(directory.toString()); + setRootDirectory(directory); + }); + connect(m_rootSelector, + static_cast(&QComboBox::activated), + this, + [this] { + if (m_autoSync && Core::EditorManager::currentEditor()) + selectFile(Core::EditorManager::currentEditor()->document()->filePath()); + }); } void FolderNavigationWidget::toggleAutoSynchronization() @@ -178,6 +165,29 @@ void FolderNavigationWidget::toggleAutoSynchronization() setAutoSynchronization(!m_autoSync); } +void FolderNavigationWidget::addRootDirectory(const QString &displayName, + const Utils::FileName &directory) +{ + m_rootSelector->addItem(displayName, qVariantFromValue(directory)); + m_rootSelector->setItemData(m_rootSelector->count() - 1, + directory.toUserOutput(), + Qt::ToolTipRole); + if (m_autoSync) // we might find a better root for current selection now + setCurrentEditor(Core::EditorManager::currentEditor()); +} + +void FolderNavigationWidget::removeRootDirectory(const Utils::FileName &directory) +{ + for (int i = 0; i < m_rootSelector->count(); ++i) { + if (m_rootSelector->itemData(i).value() == directory) { + m_rootSelector->removeItem(i); + break; + } + } + if (m_autoSync) // we might need to find a new root for current selection + setCurrentEditor(Core::EditorManager::currentEditor()); +} + bool FolderNavigationWidget::autoSynchronization() const { return m_autoSync; @@ -193,148 +203,95 @@ void FolderNavigationWidget::setAutoSynchronization(bool sync) if (m_autoSync) { connect(Core::EditorManager::instance(), &Core::EditorManager::currentEditorChanged, - this, &FolderNavigationWidget::setCurrentFile); - setCurrentFile(Core::EditorManager::currentEditor()); + this, &FolderNavigationWidget::setCurrentEditor); + setCurrentEditor(Core::EditorManager::currentEditor()); } else { disconnect(Core::EditorManager::instance(), &Core::EditorManager::currentEditorChanged, - this, &FolderNavigationWidget::setCurrentFile); + this, &FolderNavigationWidget::setCurrentEditor); } } -void FolderNavigationWidget::setCurrentFile(Core::IEditor *editor) +void FolderNavigationWidget::setCurrentEditor(Core::IEditor *editor) { if (!editor) return; + const Utils::FileName filePath = editor->document()->filePath(); + // switch to most fitting root + const int bestRootIndex = bestRootForFile(filePath); + m_rootSelector->setCurrentIndex(bestRootIndex); + // select + selectFile(filePath); +} - const QString filePath = editor->document()->filePath().toString(); - // Try to find directory of current file - bool pathOpened = false; - if (!filePath.isEmpty()) { - const QFileInfo fi(filePath); - if (fi.exists()) - pathOpened = setCurrentDirectory(fi.absolutePath()); +void FolderNavigationWidget::selectFile(const Utils::FileName &filePath) +{ + const QModelIndex fileIndex = m_fileSystemModel->index(filePath.toString()); + if (fileIndex.isValid()) { + // TODO This only scrolls to the right position if all directory contents are loaded. + // Unfortunately listening to directoryLoaded was still not enough (there might also + // be some delayed sorting involved?). + // Use magic timer for scrolling. + m_listView->setCurrentIndex(fileIndex); + QTimer::singleShot(200, this, [this, filePath] { + const QModelIndex fileIndex = m_fileSystemModel->index(filePath.toString()); + m_listView->scrollTo(fileIndex); + }); } - if (!pathOpened) // Default to home. - setCurrentDirectory(Utils::PathChooser::homePath()); +} - // Select the current file. - if (pathOpened) { - const QModelIndex fileIndex = m_fileSystemModel->index(filePath); - if (fileIndex.isValid()) { - QItemSelectionModel *selections = m_listView->selectionModel(); - const QModelIndex mainIndex = m_filterModel->mapFromSource(fileIndex); - selections->setCurrentIndex(mainIndex, QItemSelectionModel::SelectCurrent - | QItemSelectionModel::Clear); - m_listView->scrollTo(mainIndex); +void FolderNavigationWidget::setRootDirectory(const Utils::FileName &directory) +{ + const QModelIndex index = m_fileSystemModel->setRootPath(directory.toString()); + m_listView->setRootIndex(index); +} + +int FolderNavigationWidget::bestRootForFile(const Utils::FileName &filePath) +{ + int index = 0; // Computer is default + int commonLength = 0; + for (int i = 1; i < m_rootSelector->count(); ++i) { + const auto root = m_rootSelector->itemData(i).value(); + if (filePath.isChildOf(root) && root.length() > commonLength) { + index = i; + commonLength = root.length(); } } + return index; } -bool FolderNavigationWidget::setCurrentDirectory(const QString &directory) +void FolderNavigationWidget::openItem(const QModelIndex &index) { - const QString newDirectory = directory.isEmpty() ? QDir::rootPath() : directory; - if (debug) - qDebug() << "setcurdir" << directory << newDirectory; - // Set the root path on the model instead of changing the top index - // of the view to cause the model to clean out its file watchers. - const QModelIndex index = m_fileSystemModel->setRootPath(newDirectory); - if (!index.isValid()) { - setCurrentTitle(QString(), QString()); - return false; - } - QModelIndex oldRootIndex = m_listView->rootIndex(); - QModelIndex newRootIndex = m_filterModel->mapFromSource(index); - m_listView->setRootIndex(newRootIndex); - const QDir current(QDir::cleanPath(newDirectory)); - setCurrentTitle(current.dirName(), - QDir::toNativeSeparators(current.absolutePath())); - if (oldRootIndex.parent() == newRootIndex) { // cdUp, so select the old directory - m_listView->setCurrentIndex(oldRootIndex); - m_listView->scrollTo(oldRootIndex, QAbstractItemView::EnsureVisible); - } - - return !directory.isEmpty(); -} - -QString FolderNavigationWidget::currentDirectory() const -{ - return m_fileSystemModel->rootPath(); -} - -void FolderNavigationWidget::slotOpenItem(const QModelIndex &viewIndex) -{ - if (viewIndex.isValid()) - openItem(m_filterModel->mapToSource(viewIndex)); -} - -void FolderNavigationWidget::openItem(const QModelIndex &srcIndex, bool openDirectoryAsProject) -{ - const QString fileName = m_fileSystemModel->fileName(srcIndex); - if (fileName == QLatin1String(".")) + if (!index.isValid()) return; - if (fileName == QLatin1String("..")) { - // cd up: Special behaviour: The fileInfo of ".." is that of the parent directory. - const QString parentPath = m_fileSystemModel->fileInfo(srcIndex).absoluteFilePath(); - setCurrentDirectory(parentPath); - return; - } - const QString path = m_fileSystemModel->filePath(srcIndex); - if (m_fileSystemModel->isDir(srcIndex)) { - const QFileInfo fi = m_fileSystemModel->fileInfo(srcIndex); + const QString path = m_fileSystemModel->filePath(index); + if (m_fileSystemModel->isDir(index)) { + const QFileInfo fi = m_fileSystemModel->fileInfo(index); if (!fi.isReadable() || !fi.isExecutable()) return; // Try to find project files in directory and open those. - if (openDirectoryAsProject) { - const QStringList projectFiles = FolderNavigationWidget::projectFilesInDirectory(path); - if (!projectFiles.isEmpty()) - Core::ICore::instance()->openFiles(projectFiles); - return; - } - // Change to directory - setCurrentDirectory(path); - return; + const QStringList projectFiles = FolderNavigationWidget::projectFilesInDirectory(path); + if (!projectFiles.isEmpty()) + Core::ICore::instance()->openFiles(projectFiles); + } else { + // Open editor + Core::EditorManager::openEditor(path); } - // Open file. - Core::ICore::instance()->openFiles(QStringList(path)); -} - -void FolderNavigationWidget::setCurrentTitle(QString dirName, const QString &fullPath) -{ - if (dirName.isEmpty()) - dirName = fullPath; - m_title->setText(dirName); - m_title->setToolTip(fullPath); -} - -QModelIndex FolderNavigationWidget::currentItem() const -{ - const QModelIndex current = m_listView->currentIndex(); - if (current.isValid()) - return m_filterModel->mapToSource(current); - return QModelIndex(); -} - -// Format the text for the "open" action of the context menu according -// to the selectect entry -static inline QString actionOpenText(const QFileSystemModel *model, - const QModelIndex &index) -{ - if (!index.isValid()) - return FolderNavigationWidget::tr("Open"); - const QString fileName = model->fileName(index); - if (fileName == QLatin1String("..")) - return FolderNavigationWidget::tr("Open Parent Folder"); - return FolderNavigationWidget::tr("Open \"%1\"").arg(fileName); } void FolderNavigationWidget::contextMenuEvent(QContextMenuEvent *ev) { QMenu menu; // Open current item - const QModelIndex current = currentItem(); + const QModelIndex current = m_listView->currentIndex(); const bool hasCurrentItem = current.isValid(); - QAction *actionOpen = menu.addAction(actionOpenText(m_fileSystemModel, current)); - actionOpen->setEnabled(hasCurrentItem); + QAction *actionOpen = nullptr; + if (hasCurrentItem) { + const QString fileName = m_fileSystemModel->fileName(current); + if (m_fileSystemModel->isDir(current)) + actionOpen = menu.addAction(tr("Open Project in \"%1\"").arg(fileName)); + else + actionOpen = menu.addAction(tr("Open \"%1\"").arg(fileName)); + } // we need dummy DocumentModel::Entry with absolute file path in it // to get EditorManager::addNativeDirAndOpenWithActions() working @@ -344,17 +301,6 @@ void FolderNavigationWidget::contextMenuEvent(QContextMenuEvent *ev) fakeEntry.document = &document; Core::EditorManager::addNativeDirAndOpenWithActions(&menu, &fakeEntry); - const bool isDirectory = hasCurrentItem && m_fileSystemModel->isDir(current); - QAction *actionOpenDirectoryAsProject = 0; - if (isDirectory && m_fileSystemModel->fileName(current) != QLatin1String("..")) { - actionOpenDirectoryAsProject = - menu.addAction(tr("Open Project in \"%1\"") - .arg(m_fileSystemModel->fileName(current))); - } - - // Open file dialog to choose a path starting from current - QAction *actionChooseFolder = menu.addAction(tr("Choose Folder...")); - QAction *action = menu.exec(ev->globalPos()); if (!action) return; @@ -362,12 +308,6 @@ void FolderNavigationWidget::contextMenuEvent(QContextMenuEvent *ev) ev->accept(); if (action == actionOpen) { // Handle open file. openItem(current); - } else if (action == actionOpenDirectoryAsProject) { - openItem(current, true); - } else if (action == actionChooseFolder) { // Open file dialog - const QString newPath = QFileDialog::getExistingDirectory(this, tr("Choose Folder"), currentDirectory()); - if (!newPath.isEmpty()) - setCurrentDirectory(newPath); } } @@ -387,17 +327,6 @@ bool FolderNavigationWidget::hiddenFilesFilter() const return m_filterHiddenFilesAction->isChecked(); } -void FolderNavigationWidget::ensureCurrentIndex() -{ - QModelIndex index = m_listView->currentIndex(); - if (!index.isValid() - || index.parent() != m_listView->rootIndex()) { - index = m_listView->rootIndex().child(0, 0); - m_listView->setCurrentIndex(index); - } - m_listView->scrollTo(index); -} - QStringList FolderNavigationWidget::projectFilesInDirectory(const QString &path) { QDir dir(path); @@ -410,6 +339,7 @@ QStringList FolderNavigationWidget::projectFilesInDirectory(const QString &path) // --------------------FolderNavigationWidgetFactory FolderNavigationWidgetFactory::FolderNavigationWidgetFactory() { + m_instance = this; setDisplayName(tr("File System")); setPriority(400); setId("File System"); @@ -418,8 +348,19 @@ FolderNavigationWidgetFactory::FolderNavigationWidgetFactory() Core::NavigationView FolderNavigationWidgetFactory::createWidget() { - Core::NavigationView n; auto fnw = new FolderNavigationWidget; + for (const DirectoryEntry &root : m_rootDirectories) + fnw->addRootDirectory(root.first, root.second); + connect(this, + &FolderNavigationWidgetFactory::rootDirectoryAdded, + fnw, + &FolderNavigationWidget::addRootDirectory); + connect(this, + &FolderNavigationWidgetFactory::rootDirectoryRemoved, + fnw, + &FolderNavigationWidget::removeRootDirectory); + + Core::NavigationView n; n.widget = fnw; auto filter = new QToolButton; filter->setIcon(Utils::Icons::FILTER.icon()); @@ -450,7 +391,23 @@ void FolderNavigationWidgetFactory::restoreSettings(QSettings *settings, int pos fnw->setHiddenFilesFilter(settings->value(baseKey + QLatin1String(".HiddenFilesFilter"), false).toBool()); fnw->setAutoSynchronization(settings->value(baseKey + QLatin1String(".SyncWithEditor"), true).toBool()); } + +void FolderNavigationWidgetFactory::addRootDirectory(const QString &displayName, + const Utils::FileName &directory) +{ + m_rootDirectories.append(DirectoryEntry(displayName, directory)); + emit m_instance->rootDirectoryAdded(displayName, directory); +} + +void FolderNavigationWidgetFactory::removeRootDirectory(const Utils::FileName &directory) +{ + const int index = Utils::indexOf(m_rootDirectories, [directory](const DirectoryEntry &entry) { + return entry.second == directory; + }); + QTC_ASSERT(index >= 0, return); + m_rootDirectories.removeAt(index); + emit m_instance->rootDirectoryRemoved(directory); +} + } // namespace Internal } // namespace ProjectExplorer - -#include "foldernavigationwidget.moc" diff --git a/src/plugins/projectexplorer/foldernavigationwidget.h b/src/plugins/projectexplorer/foldernavigationwidget.h index 3b4b37fa2fb..22f88f43485 100644 --- a/src/plugins/projectexplorer/foldernavigationwidget.h +++ b/src/plugins/projectexplorer/foldernavigationwidget.h @@ -29,15 +29,18 @@ #include -namespace Utils { class ListView; } namespace Core { class IEditor; } +namespace Utils { +class FileName; +class NavigationTreeView; +} + QT_BEGIN_NAMESPACE -class QLabel; -class QSortFilterProxyModel; -class QModelIndex; -class QFileSystemModel; class QAction; +class QComboBox; +class QFileSystemModel; +class QModelIndex; QT_END_NAMESPACE namespace ProjectExplorer { @@ -58,29 +61,26 @@ public: void setAutoSynchronization(bool sync); void toggleAutoSynchronization(); -private: - void setCurrentFile(Core::IEditor *editor); - void slotOpenItem(const QModelIndex &viewIndex); - void setHiddenFilesFilter(bool filter); - void ensureCurrentIndex(); + void addRootDirectory(const QString &displayName, const Utils::FileName &directory); + void removeRootDirectory(const Utils::FileName &directory); protected: void contextMenuEvent(QContextMenuEvent *ev) override; private: - void setCurrentTitle(QString dirName, const QString &fullPath); - bool setCurrentDirectory(const QString &directory); - void openItem(const QModelIndex &srcIndex, bool openDirectoryAsProject = false); - QModelIndex currentItem() const; - QString currentDirectory() const; + void setHiddenFilesFilter(bool filter); + void setCurrentEditor(Core::IEditor *editor); + void selectFile(const Utils::FileName &filePath); + void setRootDirectory(const Utils::FileName &directory); + int bestRootForFile(const Utils::FileName &filePath); + void openItem(const QModelIndex &index); - Utils::ListView *m_listView; - QFileSystemModel *m_fileSystemModel; - QAction *m_filterHiddenFilesAction; - QSortFilterProxyModel *m_filterModel; - QLabel *m_title; + Utils::NavigationTreeView *m_listView = nullptr; + QFileSystemModel *m_fileSystemModel = nullptr; + QAction *m_filterHiddenFilesAction = nullptr; bool m_autoSync = false; - QToolButton *m_toggleSync; + QToolButton *m_toggleSync = nullptr; + QComboBox *m_rootSelector = nullptr; // FolderNavigationWidgetFactory needs private members to build a menu friend class FolderNavigationWidgetFactory; @@ -96,6 +96,17 @@ public: Core::NavigationView createWidget() override; void saveSettings(QSettings *settings, int position, QWidget *widget) override; void restoreSettings(QSettings *settings, int position, QWidget *widget) override; + + static void addRootDirectory(const QString &displayName, const Utils::FileName &directory); + static void removeRootDirectory(const Utils::FileName &directory); + +signals: + void rootDirectoryAdded(const QString &displayName, const Utils::FileName &directory); + void rootDirectoryRemoved(const Utils::FileName &directory); + +private: + using DirectoryEntry = std::pair; + static QVector m_rootDirectories; }; } // namespace Internal diff --git a/src/plugins/projectexplorer/session.cpp b/src/plugins/projectexplorer/session.cpp index f17c40a197c..458bc525469 100644 --- a/src/plugins/projectexplorer/session.cpp +++ b/src/plugins/projectexplorer/session.cpp @@ -30,6 +30,7 @@ #include "kit.h" #include "buildconfiguration.h" #include "deployconfiguration.h" +#include "foldernavigationwidget.h" #include "projectexplorer.h" #include "projectnodes.h" #include "editorconfiguration.h" @@ -385,6 +386,8 @@ void SessionManager::addProject(Project *pro) m_instance, [pro]() { m_instance->projectDisplayNameChanged(pro); }); emit m_instance->projectAdded(pro); + FolderNavigationWidgetFactory::addRootDirectory(pro->displayName(), + pro->projectFilePath().parentDir()); configureEditors(pro); connect(pro, &Project::fileListChanged, [pro](){ configureEditors(pro); }); } @@ -739,6 +742,7 @@ void SessionManager::removeProjects(QList remove) m_instance, &SessionManager::clearProjectFileCache); d->m_projectFileCache.remove(pro); emit m_instance->projectRemoved(pro); + FolderNavigationWidgetFactory::removeRootDirectory(pro->projectFilePath().parentDir()); delete pro; }