From 4079dd5cbccc6471005d5e5ab86a564de02aa5c0 Mon Sep 17 00:00:00 2001 From: Christian Kandeler Date: Mon, 11 Feb 2019 15:04:35 +0100 Subject: [PATCH] ProjectExplorer: Support Drag and Drop in the project tree E.g. moving a file from one pri file to another is much simpler for the user now. [ChangeLog] Source files can now be drag-and-dropped between project nodes in the project tree. Fixes: QTCREATORBUG-6446 Change-Id: I8bd4a7588fc5f2830f6585dfcb54ab4a547bc6b0 Reviewed-by: hjk --- src/plugins/projectexplorer/projectmodels.cpp | 319 +++++++++++++++++- src/plugins/projectexplorer/projectmodels.h | 4 + .../projectexplorer/projecttreewidget.cpp | 4 +- 3 files changed, 325 insertions(+), 2 deletions(-) diff --git a/src/plugins/projectexplorer/projectmodels.cpp b/src/plugins/projectexplorer/projectmodels.cpp index d90e3495244..46c98d164b1 100644 --- a/src/plugins/projectexplorer/projectmodels.cpp +++ b/src/plugins/projectexplorer/projectmodels.cpp @@ -32,17 +32,31 @@ #include "session.h" #include "target.h" +#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 @@ -188,6 +202,8 @@ Qt::ItemFlags FlatModel::flags(const QModelIndex &index) const // either folder or file node if (node->supportsAction(Rename, node)) f = f | Qt::ItemIsEditable; + } else if (node->supportsAction(ProjectAction::AddExistingFile, node)) { + f |= Qt::ItemIsDropEnabled; } } return f; @@ -408,7 +424,7 @@ QStringList FlatModel::mimeTypes() const QMimeData *FlatModel::mimeData(const QModelIndexList &indexes) const { - auto data = new Utils::DropMimeData; + auto data = new DropMimeData; foreach (const QModelIndex &index, indexes) { if (Node *node = nodeForIndex(index)) { if (node->asFileNode()) @@ -419,6 +435,307 @@ QMimeData *FlatModel::mimeData(const QModelIndexList &indexes) const return data; } +bool FlatModel::canDropMimeData(const QMimeData *data, Qt::DropAction, int, int, + const QModelIndex &) const +{ + // For now, we support only drops of Qt Creator file nodes. + const auto * const dropData = dynamic_cast(data); + if (!dropData) + return false; + QTC_ASSERT(!dropData->values().empty(), return false); + return dropData->files().size() == dropData->values().size(); +} + +enum class DropAction { Copy, CopyWithFiles, Move, MoveWithFiles }; + +class DropFileDialog : public QDialog +{ + Q_DECLARE_TR_FUNCTIONS(ProjectExplorer::Internal::FlatModel) +public: + DropFileDialog(const FileName &defaultTargetDir) + : m_buttonBox(new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel)), + m_buttonGroup(new QButtonGroup(this)) + { + setWindowTitle(tr("Please choose a drop action")); + const bool offerFileIo = !defaultTargetDir.isEmpty(); + auto * const layout = new QVBoxLayout(this); + layout->addWidget(new QLabel(tr("You just dragged some files from one project node to " + "another.\nWhat should Qt Creator do now?"), this)); + auto * const copyButton = new QRadioButton(this); + m_buttonGroup->addButton(copyButton, int(DropAction::Copy)); + layout->addWidget(copyButton); + auto * const moveButton = new QRadioButton(this); + m_buttonGroup->addButton(moveButton, int(DropAction::Move)); + layout->addWidget(moveButton); + if (offerFileIo) { + copyButton->setText(tr("Copy only the file references")); + moveButton->setText(tr("Move only the file references")); + auto * const copyWithFilesButton + = new QRadioButton(tr("Copy file references and files"), this); + m_buttonGroup->addButton(copyWithFilesButton, int(DropAction::CopyWithFiles)); + layout->addWidget(copyWithFilesButton); + auto * const moveWithFilesButton + = new QRadioButton(tr("Move file references and files"), this); + m_buttonGroup->addButton(moveWithFilesButton, int(DropAction::MoveWithFiles)); + layout->addWidget(moveWithFilesButton); + moveWithFilesButton->setChecked(true); + auto * const targetDirLayout = new QHBoxLayout; + layout->addLayout(targetDirLayout); + targetDirLayout->addWidget(new QLabel(tr("Target directory:"), this)); + m_targetDirChooser = new PathChooser(this); + m_targetDirChooser->setExpectedKind(PathChooser::ExistingDirectory); + m_targetDirChooser->setFileName(defaultTargetDir); + connect(m_targetDirChooser, &PathChooser::validChanged, this, [this](bool valid) { + m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid); + }); + targetDirLayout->addWidget(m_targetDirChooser); + connect(m_buttonGroup, + static_cast(&QButtonGroup::buttonClicked), + this, [this] { + switch (dropAction()) { + case DropAction::CopyWithFiles: + case DropAction::MoveWithFiles: + m_targetDirChooser->setEnabled(true); + m_buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(m_targetDirChooser->isValid()); + break; + case DropAction::Copy: + case DropAction::Move: + m_targetDirChooser->setEnabled(false); + m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + break; + } + }); + } else { + copyButton->setText(tr("Copy the file references")); + moveButton->setText(tr("Move the file references")); + moveButton->setChecked(true); + } + connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + layout->addWidget(m_buttonBox); + } + + DropAction dropAction() const { return static_cast(m_buttonGroup->checkedId()); } + FileName targetDir() const + { + return m_targetDirChooser ? m_targetDirChooser->fileName() : FileName(); + } + +private: + PathChooser *m_targetDirChooser = nullptr; + QDialogButtonBox * const m_buttonBox; + QButtonGroup * const m_buttonGroup; +}; + + +bool FlatModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, + const QModelIndex &parent) +{ + Q_UNUSED(action); + + const auto * const dropData = dynamic_cast(data); + QTC_ASSERT(dropData, return false); + + auto fileNodes = transform>(dropData->values(), + [](const QVariant &v) { return v.value(); }); + QTC_ASSERT(!fileNodes.empty(), return true); + + // The drag operation does not block event handling, so it's possible that the project + // was reparsed and the nodes in the drop data are now invalid. If that happens for any node, + // we chicken out and abort the entire operation. + // Note: In theory, it might be possible that the memory was reused in such an unlucky + // way that the pointers refer to different project nodes now, but... + if (!allOf(fileNodes, [](const Node *n) { return ProjectTree::hasNode(n); })) + return true; + + // We handle only proper file nodes, i.e. no project or folder nodes and no "pseudo" + // file nodes that represent the project file. + fileNodes = filtered(fileNodes, [](const Node *n) { + return n->asFileNode() && n->asFileNode()->fileType() != FileType::Project; + }); + if (fileNodes.empty()) + return true; + + // We can handle more than one file being dropped, as long as they have the same parent node. + ProjectNode * const sourceProjectNode = fileNodes.first()->parentProjectNode(); + QTC_ASSERT(sourceProjectNode, return true); + if (anyOf(fileNodes, [sourceProjectNode](const Node *n) { + return n->parentProjectNode() != sourceProjectNode; })) { + return true; + } + Node *targetNode = nodeForIndex(index(row, column, parent)); + if (!targetNode) + targetNode = nodeForIndex(parent); + QTC_ASSERT(targetNode, return true); + ProjectNode *targetProjectNode = targetNode->asProjectNode(); + if (!targetProjectNode) + targetProjectNode = targetNode->parentProjectNode(); + QTC_ASSERT(targetProjectNode, return true); + if (sourceProjectNode == targetProjectNode) + return true; + + // Node weirdness: Sometimes the "file path" is a directory, sometimes it's a file... + const auto dirForProjectNode = [](const ProjectNode *pNode) { + const FileName dir = pNode->filePath(); + if (dir.toFileInfo().isDir()) + return dir; + return FileName::fromString(dir.toFileInfo().path()); + }; + FileName targetDir = dirForProjectNode(targetProjectNode); + + // Ask the user what to do now: Copy or add? With or without file transfer? + DropFileDialog dlg(targetDir == dirForProjectNode(sourceProjectNode) ? FileName() : targetDir); + if (dlg.exec() != QDialog::Accepted) + return true; + if (!dlg.targetDir().isEmpty()) + targetDir = dlg.targetDir(); + + // Check the nodes again. + if (!allOf(fileNodes, [](const Node *n) { return ProjectTree::hasNode(n); })) + return true; + + // Some helper functions for the file operations. + const auto targetFilePath = [&targetDir](const QString &sourceFilePath) { + FileName targetFile = targetDir; + targetFile.appendPath(QFileInfo(sourceFilePath).fileName()); + return targetFile.toString(); + }; + + struct VcsInfo { + Core::IVersionControl *vcs = nullptr; + QString repoDir; + bool operator==(const VcsInfo &other) const { + return vcs == other.vcs && repoDir == other.repoDir; + } + }; + QHash vcsHash; + const auto vcsInfoForFile = [&vcsHash](const QString &filePath) { + const QString dir = QFileInfo(filePath).path(); + const auto it = vcsHash.constFind(dir); + if (it != vcsHash.constEnd()) + return it.value(); + VcsInfo vcsInfo; + vcsInfo.vcs = Core::VcsManager::findVersionControlForDirectory(dir, &vcsInfo.repoDir); + vcsHash.insert(dir, vcsInfo); + return vcsInfo; + }; + + // Now do the actual work. + const QStringList sourceFiles = transform(fileNodes, [](const Node *n) { + return n->filePath().toString(); + }); + QStringList failedRemoveFromProject; + QStringList failedAddToProject; + QStringList failedCopyOrMove; + QStringList failedDelete; + QStringList failedVcsOp; + switch (dlg.dropAction()) { + case DropAction::CopyWithFiles: { + QStringList filesToAdd; + Core::IVersionControl * const vcs = Core::VcsManager::findVersionControlForDirectory( + targetDir.toString()); + const bool addToVcs = vcs && vcs->supportsOperation(Core::IVersionControl::AddOperation); + for (const QString &sourceFile : sourceFiles) { + const QString targetFile = targetFilePath(sourceFile); + if (QFile::copy(sourceFile, targetFile)) { + filesToAdd << targetFile; + if (addToVcs && !vcs->vcsAdd(targetFile)) + failedVcsOp << targetFile; + } else { + failedCopyOrMove << sourceFile; + } + } + targetProjectNode->addFiles(filesToAdd, &failedAddToProject); + break; + } + case DropAction::Copy: + targetProjectNode->addFiles(sourceFiles, &failedAddToProject); + break; + case DropAction::MoveWithFiles: { + QStringList filesToAdd; + QStringList filesToRemove; + const VcsInfo targetVcs = vcsInfoForFile(targetDir.toString()); + const bool vcsAddPossible = targetVcs.vcs + && targetVcs.vcs->supportsOperation(Core::IVersionControl::AddOperation); + for (const QString &sourceFile : sourceFiles) { + const QString targetFile = targetFilePath(sourceFile); + const VcsInfo sourceVcs = vcsInfoForFile(sourceFile); + if (sourceVcs.vcs && targetVcs.vcs && sourceVcs == targetVcs + && sourceVcs.vcs->supportsOperation(Core::IVersionControl::MoveOperation)) { + if (sourceVcs.vcs->vcsMove(sourceFile, targetFile)) { + filesToAdd << targetFile; + filesToRemove << sourceFile; + } else { + failedCopyOrMove << sourceFile; + } + continue; + } + if (!QFile::copy(sourceFile, targetFile)) { + failedCopyOrMove << sourceFile; + continue; + } + filesToAdd << targetFile; + filesToRemove << sourceFile; + Core::FileChangeBlocker changeGuard(sourceFile); + if (sourceVcs.vcs && sourceVcs.vcs->supportsOperation( + Core::IVersionControl::DeleteOperation) + && !sourceVcs.vcs->vcsDelete(sourceFile)) { + failedVcsOp << sourceFile; + } + if (QFile::exists(sourceFile) && !QFile::remove(sourceFile)) + failedDelete << sourceFile; + if (vcsAddPossible && !targetVcs.vcs->vcsAdd(targetFile)) + failedVcsOp << targetFile; + } + sourceProjectNode->removeFiles(filesToRemove, &failedRemoveFromProject); + targetProjectNode->addFiles(filesToAdd, &failedAddToProject); + break; + } + case DropAction::Move: + sourceProjectNode->removeFiles(sourceFiles, &failedRemoveFromProject); + targetProjectNode->addFiles(sourceFiles, &failedAddToProject); + break; + } + + // Summary for the user in case anything went wrong. + const auto makeUserFileList = [](const QStringList &files) { + return transform(files, [](const QString &f) { return QDir::toNativeSeparators(f); }) + .join("\n "); + }; + if (!failedAddToProject.empty() || !failedRemoveFromProject.empty() + || !failedCopyOrMove.empty() || !failedDelete.empty() || !failedVcsOp.empty()) { + QString message = tr("Not all operations finished successfully."); + if (!failedCopyOrMove.empty()) { + message.append('\n').append(tr("The following files could not be copied or moved:")) + .append("\n ").append(makeUserFileList(failedCopyOrMove)); + } + if (!failedRemoveFromProject.empty()) { + message.append('\n').append(tr("The following files could not be removed from the " + "project file:")) + .append("\n ").append(makeUserFileList(failedRemoveFromProject)); + } + if (!failedAddToProject.empty()) { + message.append('\n').append(tr("The following files could not be added to the " + "project file:")) + .append("\n ").append(makeUserFileList(failedAddToProject)); + } + if (!failedDelete.empty()) { + message.append('\n').append(tr("The following files could not be deleted:")) + .append("\n ").append(makeUserFileList(failedDelete)); + } + if (!failedVcsOp.empty()) { + message.append('\n').append(tr("A version control operation failed for the following " + "files. Please check your repository.")) + .append("\n ").append(makeUserFileList(failedVcsOp)); + } + QMessageBox::warning(Core::ICore::mainWindow(), tr("Failure Updating Project"), + message); + } + + return true; +} + WrapperNode *FlatModel::wrapperForNode(const Node *node) const { return findNonRootItem([node](WrapperNode *item) { diff --git a/src/plugins/projectexplorer/projectmodels.h b/src/plugins/projectexplorer/projectmodels.h index 952fc096518..5805efe3eab 100644 --- a/src/plugins/projectexplorer/projectmodels.h +++ b/src/plugins/projectexplorer/projectmodels.h @@ -67,6 +67,10 @@ public: Qt::DropActions supportedDragActions() const override; QStringList mimeTypes() const override; QMimeData *mimeData(const QModelIndexList &indexes) const override; + bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, + const QModelIndex &parent) const override; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, + const QModelIndex &parent) override; Node *nodeForIndex(const QModelIndex &index) const; WrapperNode *wrapperForNode(const Node *node) const; diff --git a/src/plugins/projectexplorer/projecttreewidget.cpp b/src/plugins/projectexplorer/projecttreewidget.cpp index 641c1de40e4..d42f2758fa1 100644 --- a/src/plugins/projectexplorer/projecttreewidget.cpp +++ b/src/plugins/projectexplorer/projecttreewidget.cpp @@ -149,7 +149,9 @@ public: setEditTriggers(QAbstractItemView::EditKeyPressed); setContextMenuPolicy(Qt::CustomContextMenu); setDragEnabled(true); - setDragDropMode(QAbstractItemView::DragOnly); + setDragDropMode(QAbstractItemView::DragDrop); + viewport()->setAcceptDrops(true); + setDropIndicatorShown(true); m_context = new IContext(this); m_context->setContext(Context(ProjectExplorer::Constants::C_PROJECT_TREE)); m_context->setWidget(this);