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