File indexing completed on 2024-04-28 04:39:08

0001 /*
0002     SPDX-FileCopyrightText: 2017 Alexander Potashev <aspotashev@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "cutcopypastehelpers.h"
0008 
0009 #include <QTreeWidget>
0010 #include <QDialog>
0011 #include <QDialogButtonBox>
0012 #include <QVBoxLayout>
0013 #include <QHBoxLayout>
0014 #include <QLabel>
0015 #include <QStyle>
0016 #include <QStyleOption>
0017 #include <QPointer>
0018 #include <QAbstractButton>
0019 
0020 #include <KLocalizedString>
0021 #include <KIO/DeleteJob>
0022 
0023 #include <interfaces/icore.h>
0024 #include <interfaces/iproject.h>
0025 #include <interfaces/iprojectcontroller.h>
0026 #include <project/interfaces/iprojectfilemanager.h>
0027 #include <serialization/indexedstring.h>
0028 
0029 using namespace KDevelop;
0030 
0031 namespace CutCopyPasteHelpers
0032 {
0033 
0034 TaskInfo::TaskInfo(const TaskStatus status, const TaskType type,
0035                    const Path::List& src, const Path& dest)
0036     : m_status(status),
0037       m_type(type),
0038       m_src(src),
0039       m_dest(dest)
0040 {
0041 }
0042 
0043 TaskInfo TaskInfo::createMove(const bool ok, const Path::List& src, const Path& dest)
0044 {
0045     return TaskInfo(ok ? TaskStatus::SUCCESS : TaskStatus::FAILURE,
0046                     TaskType::MOVE, src, dest);
0047 }
0048 
0049 TaskInfo TaskInfo::createCopy(const bool ok, const Path::List& src, const Path& dest)
0050 {
0051     return TaskInfo(ok ? TaskStatus::SUCCESS : TaskStatus::FAILURE,
0052                     TaskType::COPY, src, dest);
0053 }
0054 
0055 TaskInfo TaskInfo::createDeletion(const bool ok, const Path::List& src, const Path& dest)
0056 {
0057     return TaskInfo(ok ? TaskStatus::SUCCESS : TaskStatus::FAILURE,
0058                     TaskType::DELETION, src, dest);
0059 }
0060 
0061 static QWidget* createPasteStatsWidget(QWidget *parent, const QVector<TaskInfo>& tasks)
0062 {
0063     // TODO: Create a model for the task list, and use it here instead of using QTreeWidget
0064     auto* treeWidget = new QTreeWidget(parent);
0065     QList<QTreeWidgetItem *> items;
0066     items.reserve(tasks.size());
0067     for (const TaskInfo& task : tasks) {
0068         int srcCount = task.m_src.size();
0069         const bool withChildren = srcCount != 1;
0070 
0071         const QString destPath = task.m_dest.pathOrUrl();
0072 
0073         QString text;
0074         if (withChildren) {
0075             // Multiple source items in the current suboperation
0076             switch (task.m_type) {
0077                 case TaskType::MOVE:
0078                     text = i18np("Move %1 item into %2", "Move %1 items into %2", srcCount, destPath);
0079                     break;
0080                 case TaskType::COPY:
0081                     text = i18np("Copy %1 item into %2", "Copy %1 items into %2", srcCount, destPath);
0082                     break;
0083                 case TaskType::DELETION:
0084                     text = i18np("Delete %1 item", "Delete %1 items", srcCount);
0085                     break;
0086             }
0087         } else {
0088             // One source item in the current suboperation
0089             const QString srcPath = task.m_src[0].pathOrUrl();
0090 
0091             switch (task.m_type) {
0092                 case TaskType::MOVE:
0093                     text = i18n("Move item %1 into %2", srcPath, destPath);
0094                     break;
0095                 case TaskType::COPY:
0096                     text = i18n("Copy item %1 into %2", srcPath, destPath);
0097                     break;
0098                 case TaskType::DELETION:
0099                     text = i18n("Delete item %1", srcPath);
0100                     break;
0101             }
0102         }
0103 
0104         QString tooltip;
0105         QString iconName;
0106         switch (task.m_status) {
0107             case TaskStatus::SUCCESS:
0108                 tooltip = i18nc("@info:tooltip", "Suboperation succeeded");
0109                 iconName = QStringLiteral("dialog-ok");
0110                 break;
0111             case TaskStatus::FAILURE:
0112                 tooltip = i18nc("@info:tooltip", "Suboperation failed");
0113                 iconName = QStringLiteral("dialog-error");
0114                 break;
0115             case TaskStatus::SKIPPED:
0116                 tooltip = i18nc("@info:tooltip", "Suboperation skipped to prevent data loss");
0117                 iconName = QStringLiteral("dialog-warning");
0118                 break;
0119         }
0120 
0121         auto* item = new QTreeWidgetItem;
0122         item->setText(0, text);
0123         item->setIcon(0, QIcon::fromTheme(iconName));
0124         item->setToolTip(0, tooltip);
0125         items.append(item);
0126 
0127         if (withChildren) {
0128             for (const Path& src : task.m_src) {
0129                 auto* childItem = new QTreeWidgetItem;
0130                 childItem->setText(0, src.pathOrUrl());
0131                 item->addChild(childItem);
0132             }
0133         }
0134     }
0135     treeWidget->insertTopLevelItems(0, items);
0136     treeWidget->headerItem()->setHidden(true);
0137 
0138     return treeWidget;
0139 }
0140 
0141 SourceToDestinationMap mapSourceToDestination(const Path::List& sourcePaths, const Path& destinationPath)
0142 {
0143     // For example you are moving the following items into /dest/
0144     //   * /tests/
0145     //   * /tests/abc.cpp
0146     // If you pass them as is, moveFilesAndFolders() will crash (see note:
0147     // "Do not attempt to move subitems along with their parents").
0148     // Thus we filter out subitems from "Path::List filteredPaths".
0149     //
0150     // /tests/abc.cpp will be implicitly moved to /dest/tests/abc.cpp, for
0151     // that reason we add "/dest/tests/abc.cpp" into "result.finalPaths" as well as
0152     // "/dest/tests".
0153     //
0154     // "result.finalPaths" will be used to highlight destination items after
0155     // copy/move.
0156     Path::List sortedPaths = sourcePaths;
0157     std::sort(sortedPaths.begin(), sortedPaths.end());
0158 
0159     SourceToDestinationMap result;
0160     for (const Path& path : sortedPaths) {
0161         if (!result.filteredPaths.isEmpty() && result.filteredPaths.back().isParentOf(path)) {
0162             // think: "/tests"
0163             const Path& previousPath = result.filteredPaths.back();
0164             // think: "/dest" + "/".relativePath("/tests/abc.cpp") = /dest/tests/abc.cpp
0165             result.finalPaths[previousPath].append(Path(destinationPath, previousPath.parent().relativePath(path)));
0166         } else {
0167             // think: "/tests"
0168             result.filteredPaths.append(path);
0169             // think: "/dest" + "tests" = "/dest/tests"
0170             result.finalPaths[path].append(Path(destinationPath, path.lastPathSegment()));
0171         }
0172     }
0173 
0174     return result;
0175 }
0176 
0177 struct ClassifiedPaths
0178 {
0179     // Items originating from projects open in this KDevelop session
0180     QHash<IProject*, QList<KDevelop::ProjectBaseItem*>> itemsPerProject;
0181     // Items that do not belong to known projects
0182     Path::List alienSrcPaths;
0183 };
0184 
0185 static ClassifiedPaths classifyPaths(const Path::List& paths, KDevelop::ProjectModel* projectModel)
0186 {
0187     ClassifiedPaths result;
0188     for (const Path& path : paths) {
0189         const QList<ProjectBaseItem*> items = projectModel->itemsForPath(IndexedString(path.path()));
0190         if (!items.empty()) {
0191             for (ProjectBaseItem* item : items) {
0192                 IProject* project = item->project();
0193                 auto itemsIt = result.itemsPerProject.find(project);
0194                 if (itemsIt == result.itemsPerProject.end()) {
0195                     itemsIt = result.itemsPerProject.insert(project, QList<KDevelop::ProjectBaseItem*>());
0196                 }
0197 
0198                 itemsIt->append(item);
0199             }
0200         } else {
0201             result.alienSrcPaths.append(path);
0202         }
0203     }
0204 
0205     return result;
0206 }
0207 
0208 QVector<TaskInfo> copyMoveItems(const Path::List& paths, ProjectBaseItem* destItem, const Operation operation)
0209 {
0210     KDevelop::ProjectModel* projectModel = KDevelop::ICore::self()->projectController()->projectModel();
0211     const ClassifiedPaths cl = classifyPaths(paths, projectModel);
0212 
0213     QVector<TaskInfo> tasks;
0214 
0215     IProject* destProject = destItem->project();
0216     IProjectFileManager* destProjectFileManager = destProject->projectFileManager();
0217     ProjectFolderItem* destFolder = destItem->folder();
0218     Path destPath = destFolder->path();
0219     const auto& srcProjects = cl.itemsPerProject.keys();
0220     for (IProject* srcProject : srcProjects) {
0221         const auto& itemsList = cl.itemsPerProject[srcProject];
0222 
0223         Path::List pathsList;
0224         pathsList.reserve(itemsList.size());
0225         for (KDevelop::ProjectBaseItem* item : itemsList) {
0226             pathsList.append(item->path());
0227         }
0228 
0229         if (srcProject == destProject) {
0230             if (operation == Operation::CUT) {
0231                 // Move inside project
0232                 const bool ok = destProjectFileManager->moveFilesAndFolders(itemsList, destFolder);
0233                 tasks.append(TaskInfo::createMove(ok, pathsList, destPath));
0234             } else {
0235                 // Copy inside project
0236                 const bool ok = destProjectFileManager->copyFilesAndFolders(pathsList, destFolder);
0237                 tasks.append(TaskInfo::createCopy(ok, pathsList, destPath));
0238             }
0239         } else {
0240             // Copy/move between projects:
0241             //  1. Copy and add into destination project;
0242             //  2. Remove from source project.
0243             const bool copy_ok = destProjectFileManager->copyFilesAndFolders(pathsList, destFolder);
0244             tasks.append(TaskInfo::createCopy(copy_ok, pathsList, destPath));
0245 
0246             if (operation == Operation::CUT) {
0247                 if (copy_ok) {
0248                     IProjectFileManager* srcProjectFileManager = srcProject->projectFileManager();
0249                     const bool deletion_ok = srcProjectFileManager->removeFilesAndFolders(itemsList);
0250                     tasks.append(TaskInfo::createDeletion(deletion_ok, pathsList, destPath));
0251                 } else {
0252                     tasks.append(TaskInfo(TaskStatus::SKIPPED, TaskType::DELETION, pathsList, destPath));
0253                 }
0254             }
0255         }
0256     }
0257 
0258     // Copy/move items from outside of all open projects
0259     if (!cl.alienSrcPaths.isEmpty()) {
0260         const bool alien_copy_ok = destProjectFileManager->copyFilesAndFolders(cl.alienSrcPaths, destFolder);
0261         tasks.append(TaskInfo::createCopy(alien_copy_ok, cl.alienSrcPaths, destPath));
0262 
0263         if (operation == Operation::CUT) {
0264             if (alien_copy_ok) {
0265                 QList<QUrl> urlsToDelete;
0266                 urlsToDelete.reserve(cl.alienSrcPaths.size());
0267                 for (const Path& path : cl.alienSrcPaths) {
0268                     urlsToDelete.append(path.toUrl());
0269                 }
0270 
0271                 KIO::DeleteJob* deleteJob = KIO::del(urlsToDelete);
0272                 const bool deletion_ok = deleteJob->exec();
0273                 tasks.append(TaskInfo::createDeletion(deletion_ok, cl.alienSrcPaths, destPath));
0274             } else {
0275                 tasks.append(TaskInfo(TaskStatus::SKIPPED, TaskType::DELETION, cl.alienSrcPaths, destPath));
0276             }
0277         }
0278     }
0279 
0280     return tasks;
0281 }
0282 
0283 void showWarningDialogForFailedPaste(QWidget* parent, const QVector<TaskInfo>& tasks)
0284 {
0285     auto* dialog = new QDialog(parent);
0286 
0287     dialog->setWindowTitle(i18nc("@title:window", "Paste Failed"));
0288 
0289     auto *buttonBox = new QDialogButtonBox(dialog);
0290     buttonBox->setStandardButtons(QDialogButtonBox::Ok);
0291     QObject::connect(buttonBox, &QDialogButtonBox::clicked, dialog, &QDialog::accept);
0292 
0293     dialog->setWindowModality(Qt::WindowModal);
0294     dialog->setModal(true);
0295 
0296     auto* mainWidget = new QWidget(dialog);
0297     auto* mainLayout = new QVBoxLayout(mainWidget);
0298     const int spacingHint = mainWidget->style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing);
0299     mainLayout->setSpacing(spacingHint * 2); // provide extra spacing
0300     mainLayout->setContentsMargins(0, 0, 0, 0);
0301 
0302     auto* hLayout = new QHBoxLayout;
0303     hLayout->setContentsMargins(0, 0, 0, 0);
0304     hLayout->setSpacing(-1); // use default spacing
0305     mainLayout->addLayout(hLayout, 0);
0306 
0307     auto* iconLabel = new QLabel(mainWidget);
0308 
0309     // Icon
0310     QStyleOption option;
0311     option.initFrom(mainWidget);
0312     QIcon icon = QIcon::fromTheme(QStringLiteral("dialog-warning"));
0313     iconLabel->setPixmap(icon.pixmap(mainWidget->style()->pixelMetric(QStyle::PM_MessageBoxIconSize, &option, mainWidget)));
0314 
0315     auto* iconLayout = new QVBoxLayout();
0316     iconLayout->addStretch(1);
0317     iconLayout->addWidget(iconLabel);
0318     iconLayout->addStretch(5);
0319 
0320     hLayout->addLayout(iconLayout, 0);
0321     hLayout->addSpacing(spacingHint);
0322 
0323     const QString text = i18n("Failed to paste. Below is a list of suboperations that have been attempted.");
0324     auto* messageLabel = new QLabel(text, mainWidget);
0325     messageLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
0326     hLayout->addWidget(messageLabel, 5);
0327 
0328     QWidget* statsWidget = createPasteStatsWidget(dialog, tasks);
0329 
0330     auto* topLayout = new QVBoxLayout;
0331     dialog->setLayout(topLayout);
0332     topLayout->addWidget(mainWidget);
0333     topLayout->addWidget(statsWidget, 1);
0334     topLayout->addWidget(buttonBox);
0335 
0336     dialog->setMinimumSize(300, qMax(150, qMax(iconLabel->sizeHint().height(), messageLabel->sizeHint().height())));
0337 
0338     dialog->setAttribute(Qt::WA_DeleteOnClose);
0339     dialog->show();
0340 }
0341 
0342 } // namespace CutCopyPasteHelpers