forked from qt-creator/qt-creator
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:
@@ -32,17 +32,31 @@
|
|||||||
#include "session.h"
|
#include "session.h"
|
||||||
#include "target.h"
|
#include "target.h"
|
||||||
|
|
||||||
|
#include <coreplugin/documentmanager.h>
|
||||||
#include <coreplugin/fileiconprovider.h>
|
#include <coreplugin/fileiconprovider.h>
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
#include <coreplugin/iversioncontrol.h>
|
||||||
|
#include <coreplugin/vcsmanager.h>
|
||||||
#include <utils/utilsicons.h>
|
#include <utils/utilsicons.h>
|
||||||
#include <utils/algorithm.h>
|
#include <utils/algorithm.h>
|
||||||
#include <utils/dropsupport.h>
|
#include <utils/dropsupport.h>
|
||||||
|
#include <utils/pathchooser.h>
|
||||||
#include <utils/stringutils.h>
|
#include <utils/stringutils.h>
|
||||||
#include <utils/theme/theme.h>
|
#include <utils/theme/theme.h>
|
||||||
|
|
||||||
|
#include <QButtonGroup>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QRadioButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
@@ -188,6 +202,8 @@ Qt::ItemFlags FlatModel::flags(const QModelIndex &index) const
|
|||||||
// either folder or file node
|
// either folder or file node
|
||||||
if (node->supportsAction(Rename, node))
|
if (node->supportsAction(Rename, node))
|
||||||
f = f | Qt::ItemIsEditable;
|
f = f | Qt::ItemIsEditable;
|
||||||
|
} else if (node->supportsAction(ProjectAction::AddExistingFile, node)) {
|
||||||
|
f |= Qt::ItemIsDropEnabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return f;
|
return f;
|
||||||
@@ -408,7 +424,7 @@ QStringList FlatModel::mimeTypes() const
|
|||||||
|
|
||||||
QMimeData *FlatModel::mimeData(const QModelIndexList &indexes) const
|
QMimeData *FlatModel::mimeData(const QModelIndexList &indexes) const
|
||||||
{
|
{
|
||||||
auto data = new Utils::DropMimeData;
|
auto data = new DropMimeData;
|
||||||
foreach (const QModelIndex &index, indexes) {
|
foreach (const QModelIndex &index, indexes) {
|
||||||
if (Node *node = nodeForIndex(index)) {
|
if (Node *node = nodeForIndex(index)) {
|
||||||
if (node->asFileNode())
|
if (node->asFileNode())
|
||||||
@@ -419,6 +435,307 @@ QMimeData *FlatModel::mimeData(const QModelIndexList &indexes) const
|
|||||||
return data;
|
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
|
WrapperNode *FlatModel::wrapperForNode(const Node *node) const
|
||||||
{
|
{
|
||||||
return findNonRootItem([node](WrapperNode *item) {
|
return findNonRootItem([node](WrapperNode *item) {
|
||||||
|
@@ -67,6 +67,10 @@ public:
|
|||||||
Qt::DropActions supportedDragActions() const override;
|
Qt::DropActions supportedDragActions() const override;
|
||||||
QStringList mimeTypes() const override;
|
QStringList mimeTypes() const override;
|
||||||
QMimeData *mimeData(const QModelIndexList &indexes) 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;
|
Node *nodeForIndex(const QModelIndex &index) const;
|
||||||
WrapperNode *wrapperForNode(const Node *node) const;
|
WrapperNode *wrapperForNode(const Node *node) const;
|
||||||
|
@@ -149,7 +149,9 @@ public:
|
|||||||
setEditTriggers(QAbstractItemView::EditKeyPressed);
|
setEditTriggers(QAbstractItemView::EditKeyPressed);
|
||||||
setContextMenuPolicy(Qt::CustomContextMenu);
|
setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
setDragEnabled(true);
|
setDragEnabled(true);
|
||||||
setDragDropMode(QAbstractItemView::DragOnly);
|
setDragDropMode(QAbstractItemView::DragDrop);
|
||||||
|
viewport()->setAcceptDrops(true);
|
||||||
|
setDropIndicatorShown(true);
|
||||||
m_context = new IContext(this);
|
m_context = new IContext(this);
|
||||||
m_context->setContext(Context(ProjectExplorer::Constants::C_PROJECT_TREE));
|
m_context->setContext(Context(ProjectExplorer::Constants::C_PROJECT_TREE));
|
||||||
m_context->setWidget(this);
|
m_context->setWidget(this);
|
||||||
|
Reference in New Issue
Block a user