Change File System pane to a forest of trees

It shows the file system as a tree, where the user can change the root
directory of the view. Currently there are "Computer" (the default), and
all project directories.
If synchronization with the current editor is enabled, the view
automatically switches to the best fitting root directory.

Task-number: QTCREATORBUG-8305
Change-Id: Ic265eb49b1e8e0fd8cdeeb4fb1c64b8631f32e21
Reviewed-by: Tobias Hunger <tobias.hunger@qt.io>
This commit is contained in:
Eike Ziller
2017-09-15 11:03:45 +02:00
parent 083ff55abe
commit 3c988e5a0d
3 changed files with 197 additions and 225 deletions

View File

@@ -26,8 +26,6 @@
#include "foldernavigationwidget.h"
#include "projectexplorer.h"
#include <extensionsystem/pluginmanager.h>
#include <coreplugin/actionmanager/command.h>
#include <coreplugin/icore.h>
#include <coreplugin/idocument.h>
@@ -35,62 +33,34 @@
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/fileutils.h>
#include <coreplugin/find/findplugin.h>
#include <texteditor/findinfiles.h>
#include <utils/algorithm.h>
#include <utils/hostosinfo.h>
#include <utils/pathchooser.h>
#include <utils/qtcassert.h>
#include <utils/elidinglabel.h>
#include <utils/itemviews.h>
#include <utils/navigationtreeview.h>
#include <utils/utilsicons.h>
#include <QDebug>
#include <QComboBox>
#include <QHeaderView>
#include <QSize>
#include <QTimer>
#include <QFileSystemModel>
#include <QVBoxLayout>
#include <QToolButton>
#include <QSortFilterProxyModel>
#include <QAction>
#include <QMenu>
#include <QFileDialog>
#include <QContextMenuEvent>
#include <QDir>
#include <QFileInfo>
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::DirectoryEntry>
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;
}
static void showOnlyFirstColumn(QTreeView *view)
{
const int columnCount = view->header()->count();
for (int i = 1; i < columnCount; ++i)
view->setColumnHidden(i, true);
}
/*!
\class FolderNavigationWidget
Shows a file system folder
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<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
this,
[this](int index) {
const auto directory = m_rootSelector->itemData(index).value<Utils::FileName>();
m_rootSelector->setToolTip(directory.toString());
setRootDirectory(directory);
});
connect(m_rootSelector,
static_cast<void (QComboBox::*)(int)>(&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<Utils::FileName>() == 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 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());
const Utils::FileName filePath = editor->document()->filePath();
// switch to most fitting root
const int bestRootIndex = bestRootForFile(filePath);
m_rootSelector->setCurrentIndex(bestRootIndex);
// select
selectFile(filePath);
}
if (!pathOpened) // Default to home.
setCurrentDirectory(Utils::PathChooser::homePath());
// Select the current file.
if (pathOpened) {
const QModelIndex fileIndex = m_fileSystemModel->index(filePath);
void FolderNavigationWidget::selectFile(const Utils::FileName &filePath)
{
const QModelIndex fileIndex = m_fileSystemModel->index(filePath.toString());
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);
}
// 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);
});
}
}
bool FolderNavigationWidget::setCurrentDirectory(const QString &directory)
void FolderNavigationWidget::setRootDirectory(const Utils::FileName &directory)
{
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);
const QModelIndex index = m_fileSystemModel->setRootPath(directory.toString());
m_listView->setRootIndex(index);
}
return !directory.isEmpty();
}
QString FolderNavigationWidget::currentDirectory() const
int FolderNavigationWidget::bestRootForFile(const Utils::FileName &filePath)
{
return m_fileSystemModel->rootPath();
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<Utils::FileName>();
if (filePath.isChildOf(root) && root.length() > commonLength) {
index = i;
commonLength = root.length();
}
}
return index;
}
void FolderNavigationWidget::slotOpenItem(const QModelIndex &viewIndex)
void FolderNavigationWidget::openItem(const QModelIndex &index)
{
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;
} else {
// Open editor
Core::EditorManager::openEditor(path);
}
// Change to directory
setCurrentDirectory(path);
return;
}
// 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"

View File

@@ -29,15 +29,18 @@
#include <QWidget>
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<QString, Utils::FileName>;
static QVector<DirectoryEntry> m_rootDirectories;
};
} // namespace Internal

View File

@@ -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<Project *> remove)
m_instance, &SessionManager::clearProjectFileCache);
d->m_projectFileCache.remove(pro);
emit m_instance->projectRemoved(pro);
FolderNavigationWidgetFactory::removeRootDirectory(pro->projectFilePath().parentDir());
delete pro;
}