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 <hjk@qt.io>
This commit is contained in:
Christian Kandeler
2019-02-11 15:04:35 +01:00
parent 46f54c517a
commit 4079dd5cbc
3 changed files with 325 additions and 2 deletions

View File

@@ -32,17 +32,31 @@
#include "session.h"
#include "target.h"
#include <coreplugin/documentmanager.h>
#include <coreplugin/fileiconprovider.h>
#include <coreplugin/icore.h>
#include <coreplugin/iversioncontrol.h>
#include <coreplugin/vcsmanager.h>
#include <utils/utilsicons.h>
#include <utils/algorithm.h>
#include <utils/dropsupport.h>
#include <utils/pathchooser.h>
#include <utils/stringutils.h>
#include <utils/theme/theme.h>
#include <QButtonGroup>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFileInfo>
#include <QFont>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QMimeData>
#include <QLoggingCategory>
#include <QPushButton>
#include <QRadioButton>
#include <QVBoxLayout>
#include <functional>
@@ -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<const DropMimeData *>(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<void (QButtonGroup::*)(int)>(&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<DropAction>(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<const DropMimeData *>(data);
QTC_ASSERT(dropData, return false);
auto fileNodes = transform<QList<const Node *>>(dropData->values(),
[](const QVariant &v) { return v.value<Node *>(); });
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<QString, VcsInfo> 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) {

View File

@@ -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;

View File

@@ -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);