From 0c9c747d92b7f263b386ea89d82887dc59a24773 Mon Sep 17 00:00:00 2001 From: Eike Ziller Date: Wed, 29 Nov 2017 16:18:01 +0100 Subject: [PATCH] Add "Rename" to file system view Renames the file on disk, notifies editors, and tries to change the file in all open projects that had it (and support renaming). Task-number: QTCREATORBUG-19209 Change-Id: I165e9468c7235f9f503a3820bda3eb00f3c086d0 Reviewed-by: Tobias Hunger --- .../foldernavigationwidget.cpp | 125 ++++++++++++++++-- .../projectexplorer/foldernavigationwidget.h | 11 +- .../projectexplorer/projectexplorer.cpp | 1 - .../projectexplorerconstants.h | 2 + src/plugins/projectexplorer/projecttree.cpp | 11 ++ src/plugins/projectexplorer/projecttree.h | 2 + 6 files changed, 142 insertions(+), 10 deletions(-) diff --git a/src/plugins/projectexplorer/foldernavigationwidget.cpp b/src/plugins/projectexplorer/foldernavigationwidget.cpp index c89656c5fa7..92fbf52b65b 100644 --- a/src/plugins/projectexplorer/foldernavigationwidget.cpp +++ b/src/plugins/projectexplorer/foldernavigationwidget.cpp @@ -25,17 +25,22 @@ #include "foldernavigationwidget.h" #include "projectexplorer.h" +#include "projectexplorerconstants.h" #include "projectexplorericons.h" +#include "projectnodes.h" +#include "projecttree.h" +#include #include #include #include -#include -#include -#include #include #include +#include #include +#include +#include +#include #include @@ -44,8 +49,8 @@ #include #include #include -#include #include +#include #include #include @@ -57,6 +62,7 @@ #include #include #include +#include #include #include #include @@ -68,6 +74,7 @@ const int ID_ROLE = Qt::UserRole + 1; const int SORT_ROLE = Qt::UserRole + 2; const char PROJECTSDIRECTORYROOT_ID[] = "A.Projects"; +const char C_FOLDERNAVIGATIONWIDGET[] = "ProjectExplorer.FolderNavigationWidget"; namespace ProjectExplorer { namespace Internal { @@ -111,8 +118,10 @@ class FolderNavigationModel : public QFileSystemModel { public: explicit FolderNavigationModel(QObject *parent = nullptr); - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; - Qt::DropActions supportedDragActions() const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const final; + Qt::DropActions supportedDragActions() const final; + Qt::ItemFlags flags(const QModelIndex &index) const final; + bool setData(const QModelIndex &index, const QVariant &value, int role) final; }; FolderNavigationModel::FolderNavigationModel(QObject *parent) : QFileSystemModel(parent) @@ -131,6 +140,72 @@ Qt::DropActions FolderNavigationModel::supportedDragActions() const return Qt::MoveAction; } +Qt::ItemFlags FolderNavigationModel::flags(const QModelIndex &index) const +{ + if (index.isValid() && !fileInfo(index).isRoot()) + return QFileSystemModel::flags(index) | Qt::ItemIsEditable; + return QFileSystemModel::flags(index); +} + +static QVector renamableFolderNodes(const Utils::FileName &before, + const Utils::FileName &after) +{ + QVector folderNodes; + ProjectTree::forEachNode([&](Node *node) { + if (node->nodeType() == NodeType::File && node->filePath() == before + && node->parentFolderNode() + && node->parentFolderNode()->renameFile(before.toString(), after.toString())) { + folderNodes.append(node->parentFolderNode()); + } + }); + return folderNodes; +} + +bool FolderNavigationModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + QTC_ASSERT(index.isValid() && parent(index).isValid() && index.column() == 0 + && role == Qt::EditRole && value.canConvert(), + return false); + const QString afterFileName = value.toString(); + const QString beforeFilePath = filePath(index); + const QString parentPath = filePath(parent(index)); + const QString afterFilePath = parentPath + '/' + afterFileName; + if (beforeFilePath == afterFilePath) + return false; + // need to rename through file system model, which takes care of not changing our selection + const bool success = QFileSystemModel::setData(index, value, role); + // for files we can do more than just rename on disk, for directories the user is on his/her own + if (success && fileInfo(index).isFile()) { + Core::DocumentManager::renamedFile(beforeFilePath, afterFilePath); + const QVector folderNodes + = renamableFolderNodes(Utils::FileName::fromString(beforeFilePath), + Utils::FileName::fromString(afterFilePath)); + QVector failedNodes; + for (FolderNode *folder : folderNodes) { + if (!folder->canRenameFile(beforeFilePath, afterFilePath)) + failedNodes.append(folder); + } + if (!failedNodes.isEmpty()) { + const QString projects + = Utils::transform(failedNodes, + [](FolderNode *n) { + return n->managingProject()->filePath().fileName(); + }) + .join(", "); + const QString errorMessage + = tr("The file \"%1\" was renamed to \"%2\", " + "but the following projects could not be automatically changed: %3") + .arg(beforeFilePath, afterFilePath, projects); + QTimer::singleShot(0, Core::ICore::instance(), [errorMessage] { + QMessageBox::warning(Core::ICore::dialogParent(), + ProjectExplorerPlugin::tr("Project Editing Failed"), + errorMessage); + }); + } + } + return success; +} + static void showOnlyFirstColumn(QTreeView *view) { const int columnCount = view->header()->count(); @@ -166,6 +241,11 @@ FolderNavigationWidget::FolderNavigationWidget(QWidget *parent) : QWidget(parent m_rootSelector(new QComboBox), m_crumbLabel(new DelayedFileCrumbLabel(this)) { + m_context = new Core::IContext(this); + m_context->setContext(Core::Context(C_FOLDERNAVIGATIONWIDGET)); + m_context->setWidget(this); + Core::ICore::addContextObject(m_context); + setBackgroundRole(QPalette::Base); setAutoFillBackground(true); m_fileSystemModel->setResolveSymlinks(false); @@ -179,6 +259,7 @@ FolderNavigationWidget::FolderNavigationWidget(QWidget *parent) : QWidget(parent setHiddenFilesFilter(false); m_listView->setIconSize(QSize(16,16)); m_listView->setModel(m_fileSystemModel); + m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers); m_listView->setDragEnabled(true); m_listView->setDragDropMode(QAbstractItemView::DragOnly); showOnlyFirstColumn(m_listView); @@ -248,6 +329,11 @@ FolderNavigationWidget::FolderNavigationWidget(QWidget *parent) : QWidget(parent setAutoSynchronization(true); } +FolderNavigationWidget::~FolderNavigationWidget() +{ + Core::ICore::removeContextObject(m_context); +} + void FolderNavigationWidget::toggleAutoSynchronization() { setAutoSynchronization(!m_autoSync); @@ -302,6 +388,13 @@ void FolderNavigationWidget::removeRootDirectory(const QString &id) setCurrentEditor(Core::EditorManager::currentEditor()); } +void FolderNavigationWidget::editCurrentItem() +{ + const QModelIndex current = m_listView->currentIndex(); + if (m_fileSystemModel->flags(current) & Qt::ItemIsEditable) + m_listView->edit(current); +} + bool FolderNavigationWidget::autoSynchronization() const { return m_autoSync; @@ -465,8 +558,10 @@ void FolderNavigationWidget::contextMenuEvent(QContextMenuEvent *ev) fakeEntry.document = &document; Core::EditorManager::addNativeDirAndOpenWithActions(&menu, &fakeEntry); - if (hasCurrentItem && !isDir) { - if (Core::DiffService::instance()) { + if (hasCurrentItem) { + if (m_fileSystemModel->flags(current) & Qt::ItemIsEditable) + menu.addAction(Core::ActionManager::command(Constants::RENAMEFILE)->action()); + if (!isDir && Core::DiffService::instance()) { menu.addAction( TextEditor::TextDocument::createDiffAgainstCurrentFileAction(&menu, [filePath]() { return filePath; @@ -535,6 +630,7 @@ FolderNavigationWidgetFactory::FolderNavigationWidgetFactory() &Core::DocumentManager::projectsDirectoryChanged, this, &FolderNavigationWidgetFactory::updateProjectsDirectoryRoot); + registerActions(); } Core::NavigationView FolderNavigationWidgetFactory::createWidget() @@ -616,6 +712,19 @@ void FolderNavigationWidgetFactory::updateProjectsDirectoryRoot() Utils::Icons::PROJECT.icon()}); } +void FolderNavigationWidgetFactory::registerActions() +{ + Core::Context context(C_FOLDERNAVIGATIONWIDGET); + auto rename = new QAction(this); + Core::ActionManager::registerAction(rename, Constants::RENAMEFILE, context); + connect(rename, &QAction::triggered, Core::ICore::instance(), [] { + Core::IContext *context = Core::ICore::currentContextObject(); + QWidget *widget = context ? context->widget() : nullptr; + if (auto navWidget = qobject_cast(widget)) + navWidget->editCurrentItem(); + }); +} + int DelayedFileCrumbLabel::immediateHeightForWidth(int w) const { return Utils::FileCrumbLabel::heightForWidth(w); diff --git a/src/plugins/projectexplorer/foldernavigationwidget.h b/src/plugins/projectexplorer/foldernavigationwidget.h index 73d7fdc1955..3ea9781eafe 100644 --- a/src/plugins/projectexplorer/foldernavigationwidget.h +++ b/src/plugins/projectexplorer/foldernavigationwidget.h @@ -31,7 +31,10 @@ #include #include -namespace Core { class IEditor; } +namespace Core { +class IContext; +class IEditor; +} namespace Utils { class NavigationTreeView; @@ -79,6 +82,8 @@ signals: private: static int rootIndex(const QString &id); void updateProjectsDirectoryRoot(); + void registerActions(); + static QVector m_rootDirectories; }; @@ -88,6 +93,7 @@ class FolderNavigationWidget : public QWidget Q_PROPERTY(bool autoSynchronization READ autoSynchronization WRITE setAutoSynchronization) public: explicit FolderNavigationWidget(QWidget *parent = nullptr); + ~FolderNavigationWidget(); static QStringList projectFilesInDirectory(const QString &path); @@ -100,6 +106,8 @@ public: void insertRootDirectory(const FolderNavigationWidgetFactory::RootDirectory &directory); void removeRootDirectory(const QString &id); + void editCurrentItem(); + protected: void contextMenuEvent(QContextMenuEvent *ev) override; @@ -115,6 +123,7 @@ private: void openProjectsInDirectory(const QModelIndex &index); void setCrumblePath(const QModelIndex &index, const QModelIndex &); + Core::IContext *m_context = nullptr; Utils::NavigationTreeView *m_listView = nullptr; QFileSystemModel *m_fileSystemModel = nullptr; QAction *m_filterHiddenFilesAction = nullptr; diff --git a/src/plugins/projectexplorer/projectexplorer.cpp b/src/plugins/projectexplorer/projectexplorer.cpp index 7641eb3b99c..0cc375fe69a 100644 --- a/src/plugins/projectexplorer/projectexplorer.cpp +++ b/src/plugins/projectexplorer/projectexplorer.cpp @@ -211,7 +211,6 @@ const char OPENTERMIANLHERE[] = "ProjectExplorer.OpenTerminalHere"; const char REMOVEFILE[] = "ProjectExplorer.RemoveFile"; const char DUPLICATEFILE[] = "ProjectExplorer.DuplicateFile"; const char DELETEFILE[] = "ProjectExplorer.DeleteFile"; -const char RENAMEFILE[] = "ProjectExplorer.RenameFile"; const char DIFFFILE[] = "ProjectExplorer.DiffFile"; const char SETSTARTUP[] = "ProjectExplorer.SetStartup"; const char PROJECTTREE_COLLAPSE_ALL[] = "ProjectExplorer.CollapseAll"; diff --git a/src/plugins/projectexplorer/projectexplorerconstants.h b/src/plugins/projectexplorer/projectexplorerconstants.h index ba5cf543b39..41e6920283d 100644 --- a/src/plugins/projectexplorer/projectexplorerconstants.h +++ b/src/plugins/projectexplorer/projectexplorerconstants.h @@ -36,6 +36,8 @@ const char MODE_SESSION[] = "Project"; // Actions const char BUILD[] = "ProjectExplorer.Build"; const char STOP[] = "ProjectExplorer.Stop"; +const char RENAMEFILE[] = "ProjectExplorer.RenameFile"; + // Context const char C_PROJECT_TREE[] = "ProjectExplorer.ProjectTreeContext"; diff --git a/src/plugins/projectexplorer/projecttree.cpp b/src/plugins/projectexplorer/projecttree.cpp index b8db32c3974..997a449f153 100644 --- a/src/plugins/projectexplorer/projecttree.cpp +++ b/src/plugins/projectexplorer/projecttree.cpp @@ -392,6 +392,17 @@ bool ProjectTree::hasNode(const Node *node) }); } +void ProjectTree::forEachNode(const std::function &task) +{ + const QList projects = SessionManager::projects(); + for (Project *project : projects) { + if (ProjectNode *projectNode = project->rootProjectNode()) { + task(projectNode); + projectNode->forEachGenericNode(task); + } + } +} + void ProjectTree::hideContextMenu() { m_focusForContextMenu = nullptr; diff --git a/src/plugins/projectexplorer/projecttree.h b/src/plugins/projectexplorer/projecttree.h index f127b155f74..6691ccf45d5 100644 --- a/src/plugins/projectexplorer/projecttree.h +++ b/src/plugins/projectexplorer/projecttree.h @@ -73,6 +73,8 @@ public: static bool hasNode(const Node *node); + static void forEachNode(const std::function &task); + void collapseAll(); // for nodes to emit signals, do not call unless you are a node