File indexing completed on 2023-09-24 04:09:28
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org> 0004 0005 SPDX-License-Identifier: LGPL-2.0-only 0006 */ 0007 0008 #include "paste.h" 0009 #include "kio_widgets_debug.h" 0010 0011 #include "../utils_p.h" 0012 #include "kio/copyjob.h" 0013 #include "kio/deletejob.h" 0014 #include "kio/global.h" 0015 #include "kio/renamedialog.h" 0016 #include "kio/statjob.h" 0017 #include "pastedialog_p.h" 0018 #include <kdirnotify.h> 0019 #include <kfileitem.h> 0020 #include <kfileitemlistproperties.h> 0021 #include <kio/storedtransferjob.h> 0022 0023 #include <KJobWidgets> 0024 #include <KLocalizedString> 0025 #include <KMessageBox> 0026 #include <KUrlMimeData> 0027 0028 #include <QApplication> 0029 #include <QClipboard> 0030 #include <QDebug> 0031 #include <QFileInfo> 0032 #include <QInputDialog> 0033 #include <QMimeData> 0034 #include <QMimeDatabase> 0035 #include <QTemporaryFile> 0036 0037 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 4) 0038 // This could be made a public method, if there's a need for pasting only urls 0039 // and not random data. 0040 /** 0041 * Pastes URLs from the clipboard. This results in a copy or move job, 0042 * depending on whether the user has copied or cut the items. 0043 * 0044 * @param mimeData the mimeData to paste, usually QApplication::clipboard()->mimeData() 0045 * @param destDir Destination directory where the items will be copied/moved. 0046 * @param flags the flags are passed to KIO::copy or KIO::move. 0047 * @return the copy or move job handling the operation, or @c nullptr if there is nothing to do 0048 * @since ... 0049 */ 0050 // KIOWIDGETS_EXPORT Job *pasteClipboardUrls(const QUrl& destDir, JobFlags flags = DefaultFlags); 0051 static KIO::Job *pasteClipboardUrls(const QMimeData *mimeData, const QUrl &destDir, KIO::JobFlags flags = KIO::DefaultFlags) 0052 { 0053 const QList<QUrl> urls = KUrlMimeData::urlsFromMimeData(mimeData, KUrlMimeData::PreferLocalUrls); 0054 if (!urls.isEmpty()) { 0055 const bool move = KIO::isClipboardDataCut(mimeData); 0056 KIO::Job *job = nullptr; 0057 if (move) { 0058 job = KIO::move(urls, destDir, flags); 0059 } else { 0060 job = KIO::copy(urls, destDir, flags); 0061 } 0062 return job; 0063 } 0064 return nullptr; 0065 } 0066 #endif 0067 0068 static QUrl getDestinationUrl(const QUrl &srcUrl, const QUrl &destUrl, QWidget *widget) 0069 { 0070 KIO::StatJob *job = KIO::stat(destUrl, destUrl.isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags); 0071 job->setDetails(KIO::StatBasic); 0072 job->setSide(KIO::StatJob::DestinationSide); 0073 KJobWidgets::setWindow(job, widget); 0074 0075 // Check for existing destination file. 0076 // When we were using CopyJob, we couldn't let it do that (would expose 0077 // an ugly tempfile name as the source URL) 0078 // And now we're using a put job anyway, no destination checking included. 0079 if (job->exec()) { 0080 KIO::RenameDialog dlg(widget, i18n("File Already Exists"), srcUrl, destUrl, KIO::RenameDialog_Overwrite); 0081 KIO::RenameDialog_Result res = static_cast<KIO::RenameDialog_Result>(dlg.exec()); 0082 0083 if (res == KIO::Result_Rename) { 0084 return dlg.newDestUrl(); 0085 } else if (res == KIO::Result_Cancel) { 0086 return QUrl(); 0087 } else if (res == KIO::Result_Overwrite) { 0088 return destUrl; 0089 } 0090 } 0091 0092 return destUrl; 0093 } 0094 0095 static QUrl getNewFileName(const QUrl &u, const QString &text, const QString &suggestedFileName, QWidget *widget) 0096 { 0097 bool ok; 0098 QString dialogText(text); 0099 if (dialogText.isEmpty()) { 0100 dialogText = i18n("Filename for clipboard content:"); 0101 } 0102 QString file = QInputDialog::getText(widget, QString(), dialogText, QLineEdit::Normal, suggestedFileName, &ok); 0103 if (!ok) { 0104 return QUrl(); 0105 } 0106 0107 QUrl myurl(u); 0108 myurl.setPath(Utils::concatPaths(myurl.path(), file)); 0109 0110 return getDestinationUrl(u, myurl, widget); 0111 } 0112 0113 static KIO::Job *putDataAsyncTo(const QUrl &url, const QByteArray &data, QWidget *widget, KIO::JobFlags flags) 0114 { 0115 KIO::Job *job = KIO::storedPut(data, url, -1, flags); 0116 QObject::connect(job, &KIO::Job::result, [url](KJob *job) { 0117 if (job->error() == KJob::NoError) { 0118 org::kde::KDirNotify::emitFilesAdded(url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash)); 0119 } 0120 }); 0121 KJobWidgets::setWindow(job, widget); 0122 return job; 0123 } 0124 0125 static QByteArray chooseFormatAndUrl(const QUrl &u, 0126 const QMimeData *mimeData, 0127 const QStringList &formats, 0128 const QString &text, 0129 const QString &suggestedFileName, 0130 QWidget *widget, 0131 bool clipboard, 0132 QUrl *newUrl) 0133 { 0134 QMimeDatabase db; 0135 QStringList formatLabels; 0136 formatLabels.reserve(formats.size()); 0137 for (int i = 0; i < formats.size(); ++i) { 0138 const QString &fmt = formats[i]; 0139 QMimeType mime = db.mimeTypeForName(fmt); 0140 if (mime.isValid()) { 0141 formatLabels.append(i18n("%1 (%2)", mime.comment(), fmt)); 0142 } else { 0143 formatLabels.append(fmt); 0144 } 0145 } 0146 0147 QString dialogText(text); 0148 if (dialogText.isEmpty()) { 0149 dialogText = i18n("Filename for clipboard content:"); 0150 } 0151 0152 KIO::PasteDialog dlg(QString(), dialogText, suggestedFileName, formatLabels, widget); 0153 0154 if (dlg.exec() != QDialog::Accepted) { 0155 return QByteArray(); 0156 } 0157 0158 const QString chosenFormat = formats[dlg.comboItem()]; 0159 if (clipboard && !qApp->clipboard()->mimeData()->hasFormat(chosenFormat)) { 0160 KMessageBox::information(widget, 0161 i18n("The clipboard has changed since you used 'paste': " 0162 "the chosen data format is no longer applicable. " 0163 "Please copy again what you wanted to paste.")); 0164 return QByteArray(); 0165 } 0166 0167 const QString result = dlg.lineEditText(); 0168 0169 // qDebug() << " result=" << result << " chosenFormat=" << chosenFormat; 0170 *newUrl = u; 0171 newUrl->setPath(Utils::concatPaths(newUrl->path(), result)); 0172 0173 const QUrl destUrl = getDestinationUrl(u, *newUrl, widget); 0174 *newUrl = destUrl; 0175 0176 // In Qt3, the result of clipboard()->mimeData() only existed until the next 0177 // event loop run (see dlg.exec() above), so we re-fetched it. 0178 // TODO: This should not be necessary with Qt5; remove this conditional 0179 // and test that it still works. 0180 if (clipboard) { 0181 mimeData = QApplication::clipboard()->mimeData(); 0182 } 0183 const QByteArray ba = mimeData->data(chosenFormat); 0184 return ba; 0185 } 0186 0187 static QStringList extractFormats(const QMimeData *mimeData) 0188 { 0189 QStringList formats; 0190 const QStringList allFormats = mimeData->formats(); 0191 for (const QString &format : allFormats) { 0192 if (format == QLatin1String("application/x-qiconlist")) { // Q3IconView and kde4's libkonq 0193 continue; 0194 } 0195 if (format == QLatin1String("application/x-kde-cutselection")) { // see isClipboardDataCut 0196 continue; 0197 } 0198 if (format == QLatin1String("application/x-kde-suggestedfilename")) { 0199 continue; 0200 } 0201 if (format.startsWith(QLatin1String("application/x-qt-"))) { // Qt-internal 0202 continue; 0203 } 0204 if (format.startsWith(QLatin1String("x-kmail-drag/"))) { // app-internal 0205 continue; 0206 } 0207 if (!format.contains(QLatin1Char('/'))) { // e.g. TARGETS, MULTIPLE, TIMESTAMP 0208 continue; 0209 } 0210 formats.append(format); 0211 } 0212 return formats; 0213 } 0214 0215 KIOWIDGETS_EXPORT bool KIO::canPasteMimeData(const QMimeData *data) 0216 { 0217 return data->hasText() || !extractFormats(data).isEmpty(); 0218 } 0219 0220 KIO::Job *pasteMimeDataImpl(const QMimeData *mimeData, const QUrl &destUrl, const QString &dialogText, QWidget *widget, bool clipboard) 0221 { 0222 QByteArray ba; 0223 const QString suggestedFilename = QString::fromUtf8(mimeData->data(QStringLiteral("application/x-kde-suggestedfilename"))); 0224 0225 // Now check for plain text 0226 // We don't want to display a MIME type choice for a QTextDrag, those MIME type look ugly. 0227 if (mimeData->hasText()) { 0228 ba = mimeData->text().toLocal8Bit(); // encoding OK? 0229 } else { 0230 const QStringList formats = extractFormats(mimeData); 0231 if (formats.isEmpty()) { 0232 return nullptr; 0233 } else if (formats.size() > 1) { 0234 QUrl newUrl; 0235 ba = chooseFormatAndUrl(destUrl, mimeData, formats, dialogText, suggestedFilename, widget, clipboard, &newUrl); 0236 if (ba.isEmpty() || newUrl.isEmpty()) { 0237 return nullptr; 0238 } 0239 return putDataAsyncTo(newUrl, ba, widget, KIO::Overwrite); 0240 } 0241 ba = mimeData->data(formats.first()); 0242 } 0243 if (ba.isEmpty()) { 0244 return nullptr; 0245 } 0246 0247 const QUrl newUrl = getNewFileName(destUrl, dialogText, suggestedFilename, widget); 0248 if (newUrl.isEmpty()) { 0249 return nullptr; 0250 } 0251 0252 return putDataAsyncTo(newUrl, ba, widget, KIO::Overwrite); 0253 } 0254 0255 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 4) 0256 // The main method for pasting 0257 KIOWIDGETS_EXPORT KIO::Job *KIO::pasteClipboard(const QUrl &destUrl, QWidget *widget, bool move) 0258 { 0259 Q_UNUSED(move); 0260 0261 if (!destUrl.isValid()) { 0262 KMessageBox::error(widget, i18n("Malformed URL\n%1", destUrl.errorString())); 0263 qCWarning(KIO_WIDGETS) << destUrl.errorString(); 0264 return nullptr; 0265 } 0266 0267 // TODO: if we passed mimeData as argument, we could write unittests that don't 0268 // mess up the clipboard and that don't need QtGui. 0269 const QMimeData *mimeData = QApplication::clipboard()->mimeData(); 0270 0271 if (mimeData->hasUrls()) { 0272 // We can ignore the bool move, KIO::paste decodes it 0273 KIO::Job *job = pasteClipboardUrls(mimeData, destUrl); 0274 if (job) { 0275 KJobWidgets::setWindow(job, widget); 0276 return job; 0277 } 0278 } 0279 0280 return pasteMimeDataImpl(mimeData, destUrl, QString(), widget, true /*clipboard*/); 0281 } 0282 #endif 0283 0284 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 4) 0285 QString KIO::pasteActionText() 0286 { 0287 const QMimeData *mimeData = QApplication::clipboard()->mimeData(); 0288 const QList<QUrl> urls = KUrlMimeData::urlsFromMimeData(mimeData); 0289 if (!urls.isEmpty()) { 0290 if (urls.first().isLocalFile()) { 0291 return i18np("&Paste File", "&Paste %1 Files", urls.count()); 0292 } else { 0293 return i18np("&Paste URL", "&Paste %1 URLs", urls.count()); 0294 } 0295 } else if (!mimeData->formats().isEmpty()) { 0296 return i18n("&Paste Clipboard Contents"); 0297 } else { 0298 return QString(); 0299 } 0300 } 0301 #endif 0302 0303 KIOWIDGETS_EXPORT QString KIO::pasteActionText(const QMimeData *mimeData, bool *enable, const KFileItem &destItem) 0304 { 0305 bool canPasteData = false; 0306 QList<QUrl> urls; 0307 0308 // mimeData can be 0 according to https://bugs.kde.org/show_bug.cgi?id=335053 0309 if (mimeData) { 0310 canPasteData = KIO::canPasteMimeData(mimeData); 0311 urls = KUrlMimeData::urlsFromMimeData(mimeData); 0312 } else { 0313 qCWarning(KIO_WIDGETS) << "QApplication::clipboard()->mimeData() is 0!"; 0314 } 0315 0316 QString text; 0317 if (!urls.isEmpty() || canPasteData) { 0318 // disable the paste action if no writing is supported 0319 if (!destItem.isNull()) { 0320 if (destItem.url().isEmpty()) { 0321 *enable = false; 0322 } else { 0323 *enable = destItem.isWritable(); 0324 } 0325 } else { 0326 *enable = false; 0327 } 0328 0329 if (urls.count() == 1 && urls.first().isLocalFile()) { 0330 const bool isDir = QFileInfo(urls.first().toLocalFile()).isDir(); 0331 text = isDir ? i18nc("@action:inmenu", "Paste One Folder") : i18nc("@action:inmenu", "Paste One File"); 0332 } else if (!urls.isEmpty()) { 0333 text = i18ncp("@action:inmenu", "Paste One Item", "Paste %1 Items", urls.count()); 0334 } else { 0335 text = i18nc("@action:inmenu", "Paste Clipboard Contents..."); 0336 } 0337 } else { 0338 *enable = false; 0339 text = i18nc("@action:inmenu", "Paste"); 0340 } 0341 return text; 0342 } 0343 0344 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 4) 0345 // The [new] main method for dropping 0346 KIOWIDGETS_EXPORT KIO::Job *KIO::pasteMimeData(const QMimeData *mimeData, const QUrl &destUrl, const QString &dialogText, QWidget *widget) 0347 { 0348 return pasteMimeDataImpl(mimeData, destUrl, dialogText, widget, false /*not clipboard*/); 0349 } 0350 #endif 0351 0352 KIOWIDGETS_EXPORT void KIO::setClipboardDataCut(QMimeData *mimeData, bool cut) 0353 { 0354 const QByteArray cutSelectionData = cut ? "1" : "0"; 0355 mimeData->setData(QStringLiteral("application/x-kde-cutselection"), cutSelectionData); 0356 } 0357 0358 KIOWIDGETS_EXPORT bool KIO::isClipboardDataCut(const QMimeData *mimeData) 0359 { 0360 const QByteArray a = mimeData->data(QStringLiteral("application/x-kde-cutselection")); 0361 return (!a.isEmpty() && a.at(0) == '1'); 0362 }