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 "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) {
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user