1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.0-only |
6 | */ |
7 | |
8 | #include "paste.h" |
9 | #include "kio_widgets_debug.h" |
10 | |
11 | #include "../utils_p.h" |
12 | #include "kio/copyjob.h" |
13 | #include "kio/deletejob.h" |
14 | #include "kio/global.h" |
15 | #include "kio/renamedialog.h" |
16 | #include "kio/statjob.h" |
17 | #include "pastedialog_p.h" |
18 | #include <kdirnotify.h> |
19 | #include <kfileitem.h> |
20 | #include <kfileitemlistproperties.h> |
21 | #include <kio/storedtransferjob.h> |
22 | |
23 | #include <KJobWidgets> |
24 | #include <KLocalizedString> |
25 | #include <KMessageBox> |
26 | #include <KUrlMimeData> |
27 | |
28 | #include <QApplication> |
29 | #include <QClipboard> |
30 | #include <QDebug> |
31 | #include <QFileInfo> |
32 | #include <QInputDialog> |
33 | #include <QMimeData> |
34 | #include <QMimeDatabase> |
35 | #include <QTemporaryFile> |
36 | |
37 | static QUrl getDestinationUrl(const QUrl &srcUrl, const QUrl &destUrl, QWidget *widget) |
38 | { |
39 | KIO::StatJob *job = KIO::stat(url: destUrl, flags: destUrl.isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags); |
40 | job->setDetails(KIO::StatBasic); |
41 | job->setSide(KIO::StatJob::DestinationSide); |
42 | KJobWidgets::setWindow(job, widget); |
43 | |
44 | // Check for existing destination file. |
45 | // When we were using CopyJob, we couldn't let it do that (would expose |
46 | // an ugly tempfile name as the source URL) |
47 | // And now we're using a put job anyway, no destination checking included. |
48 | if (job->exec()) { |
49 | KIO::RenameDialog dlg(widget, i18n("File Already Exists" ), srcUrl, destUrl, KIO::RenameDialog_Overwrite); |
50 | KIO::RenameDialog_Result res = static_cast<KIO::RenameDialog_Result>(dlg.exec()); |
51 | |
52 | if (res == KIO::Result_Rename) { |
53 | return dlg.newDestUrl(); |
54 | } else if (res == KIO::Result_Cancel) { |
55 | return QUrl(); |
56 | } else if (res == KIO::Result_Overwrite) { |
57 | return destUrl; |
58 | } |
59 | } |
60 | |
61 | return destUrl; |
62 | } |
63 | |
64 | static QUrl getNewFileName(const QUrl &u, const QString &text, const QString &suggestedFileName, QWidget *widget) |
65 | { |
66 | bool ok; |
67 | QString dialogText(text); |
68 | if (dialogText.isEmpty()) { |
69 | dialogText = i18n("Filename for clipboard content:" ); |
70 | } |
71 | QString file = QInputDialog::getText(parent: widget, title: QString(), label: dialogText, echo: QLineEdit::Normal, text: suggestedFileName, ok: &ok); |
72 | if (!ok) { |
73 | return QUrl(); |
74 | } |
75 | |
76 | QUrl myurl(u); |
77 | myurl.setPath(path: Utils::concatPaths(path1: myurl.path(), path2: file)); |
78 | |
79 | return getDestinationUrl(srcUrl: u, destUrl: myurl, widget); |
80 | } |
81 | |
82 | static KIO::Job *putDataAsyncTo(const QUrl &url, const QByteArray &data, QWidget *widget, KIO::JobFlags flags) |
83 | { |
84 | KIO::Job *job = KIO::storedPut(arr: data, url, permissions: -1, flags); |
85 | QObject::connect(sender: job, signal: &KIO::Job::result, slot: [url](KJob *job) { |
86 | if (job->error() == KJob::NoError) { |
87 | org::kde::KDirNotify::emitFilesAdded(directory: url.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash)); |
88 | } |
89 | }); |
90 | KJobWidgets::setWindow(job, widget); |
91 | return job; |
92 | } |
93 | |
94 | static QByteArray chooseFormatAndUrl(const QUrl &u, |
95 | const QMimeData *mimeData, |
96 | const QStringList &formats, |
97 | const QString &text, |
98 | const QString &suggestedFileName, |
99 | QWidget *widget, |
100 | bool clipboard, |
101 | QUrl *newUrl) |
102 | { |
103 | QMimeDatabase db; |
104 | QStringList formatLabels; |
105 | formatLabels.reserve(asize: formats.size()); |
106 | for (int i = 0; i < formats.size(); ++i) { |
107 | const QString &fmt = formats[i]; |
108 | QMimeType mime = db.mimeTypeForName(nameOrAlias: fmt); |
109 | if (mime.isValid()) { |
110 | formatLabels.append(i18n("%1 (%2)" , mime.comment(), fmt)); |
111 | } else { |
112 | formatLabels.append(t: fmt); |
113 | } |
114 | } |
115 | |
116 | QString dialogText(text); |
117 | if (dialogText.isEmpty()) { |
118 | dialogText = i18n("Filename for clipboard content:" ); |
119 | } |
120 | |
121 | KIO::PasteDialog dlg(QString(), dialogText, suggestedFileName, formatLabels, widget); |
122 | |
123 | if (dlg.exec() != QDialog::Accepted) { |
124 | return QByteArray(); |
125 | } |
126 | |
127 | const QString chosenFormat = formats[dlg.comboItem()]; |
128 | if (clipboard && !qApp->clipboard()->mimeData()->hasFormat(mimetype: chosenFormat)) { |
129 | KMessageBox::information(parent: widget, |
130 | i18n("The clipboard has changed since you used 'paste': " |
131 | "the chosen data format is no longer applicable. " |
132 | "Please copy again what you wanted to paste." )); |
133 | return QByteArray(); |
134 | } |
135 | |
136 | const QString result = dlg.lineEditText(); |
137 | |
138 | // qDebug() << " result=" << result << " chosenFormat=" << chosenFormat; |
139 | *newUrl = u; |
140 | newUrl->setPath(path: Utils::concatPaths(path1: newUrl->path(), path2: result)); |
141 | |
142 | const QUrl destUrl = getDestinationUrl(srcUrl: u, destUrl: *newUrl, widget); |
143 | *newUrl = destUrl; |
144 | |
145 | // In Qt3, the result of clipboard()->mimeData() only existed until the next |
146 | // event loop run (see dlg.exec() above), so we re-fetched it. |
147 | // TODO: This should not be necessary with Qt5; remove this conditional |
148 | // and test that it still works. |
149 | if (clipboard) { |
150 | mimeData = QApplication::clipboard()->mimeData(); |
151 | } |
152 | const QByteArray ba = mimeData->data(mimetype: chosenFormat); |
153 | return ba; |
154 | } |
155 | |
156 | static QStringList (const QMimeData *mimeData) |
157 | { |
158 | QStringList formats; |
159 | const QStringList allFormats = mimeData->formats(); |
160 | for (const QString &format : allFormats) { |
161 | if (format == QLatin1String("application/x-qiconlist" )) { // Q3IconView and kde4's libkonq |
162 | continue; |
163 | } |
164 | if (format == QLatin1String("application/x-kde-cutselection" )) { // see isClipboardDataCut |
165 | continue; |
166 | } |
167 | if (format == QLatin1String("application/x-kde-suggestedfilename" )) { |
168 | continue; |
169 | } |
170 | if (format.startsWith(s: QLatin1String("application/x-qt-" ))) { // Qt-internal |
171 | continue; |
172 | } |
173 | if (format.startsWith(s: QLatin1String("x-kmail-drag/" ))) { // app-internal |
174 | continue; |
175 | } |
176 | if (!format.contains(c: QLatin1Char('/'))) { // e.g. TARGETS, MULTIPLE, TIMESTAMP |
177 | continue; |
178 | } |
179 | formats.append(t: format); |
180 | } |
181 | return formats; |
182 | } |
183 | |
184 | KIOWIDGETS_EXPORT bool KIO::canPasteMimeData(const QMimeData *data) |
185 | { |
186 | return data->hasText() || !extractFormats(mimeData: data).isEmpty(); |
187 | } |
188 | |
189 | KIO::Job *pasteMimeDataImpl(const QMimeData *mimeData, const QUrl &destUrl, const QString &dialogText, QWidget *widget, bool clipboard) |
190 | { |
191 | QByteArray ba; |
192 | const QString suggestedFilename = QString::fromUtf8(ba: mimeData->data(QStringLiteral("application/x-kde-suggestedfilename" ))); |
193 | |
194 | // Now check for plain text |
195 | // We don't want to display a MIME type choice for a QTextDrag, those MIME type look ugly. |
196 | if (mimeData->hasText()) { |
197 | ba = mimeData->text().toLocal8Bit(); // encoding OK? |
198 | } else { |
199 | const QStringList formats = extractFormats(mimeData); |
200 | if (formats.isEmpty()) { |
201 | return nullptr; |
202 | } else if (formats.size() > 1) { |
203 | QUrl newUrl; |
204 | ba = chooseFormatAndUrl(u: destUrl, mimeData, formats, text: dialogText, suggestedFileName: suggestedFilename, widget, clipboard, newUrl: &newUrl); |
205 | if (ba.isEmpty() || newUrl.isEmpty()) { |
206 | return nullptr; |
207 | } |
208 | return putDataAsyncTo(url: newUrl, data: ba, widget, flags: KIO::Overwrite); |
209 | } |
210 | ba = mimeData->data(mimetype: formats.first()); |
211 | } |
212 | if (ba.isEmpty()) { |
213 | return nullptr; |
214 | } |
215 | |
216 | const QUrl newUrl = getNewFileName(u: destUrl, text: dialogText, suggestedFileName: suggestedFilename, widget); |
217 | if (newUrl.isEmpty()) { |
218 | return nullptr; |
219 | } |
220 | |
221 | return putDataAsyncTo(url: newUrl, data: ba, widget, flags: KIO::Overwrite); |
222 | } |
223 | |
224 | KIOWIDGETS_EXPORT QString KIO::pasteActionText(const QMimeData *mimeData, bool *enable, const KFileItem &destItem) |
225 | { |
226 | bool canPasteData = false; |
227 | QList<QUrl> urls; |
228 | |
229 | // mimeData can be 0 according to https://bugs.kde.org/show_bug.cgi?id=335053 |
230 | if (mimeData) { |
231 | canPasteData = KIO::canPasteMimeData(data: mimeData); |
232 | urls = KUrlMimeData::urlsFromMimeData(mimeData); |
233 | } else { |
234 | qCWarning(KIO_WIDGETS) << "QApplication::clipboard()->mimeData() is 0!" ; |
235 | } |
236 | |
237 | QString text; |
238 | if (!urls.isEmpty() || canPasteData) { |
239 | // disable the paste action if no writing is supported |
240 | if (!destItem.isNull()) { |
241 | if (destItem.url().isEmpty()) { |
242 | *enable = false; |
243 | } else { |
244 | *enable = destItem.isWritable(); |
245 | } |
246 | } else { |
247 | *enable = false; |
248 | } |
249 | |
250 | if (urls.count() == 1 && urls.first().isLocalFile()) { |
251 | const bool isDir = QFileInfo(urls.first().toLocalFile()).isDir(); |
252 | text = isDir ? i18nc("@action:inmenu" , "Paste One Folder" ) : i18nc("@action:inmenu" , "Paste One File" ); |
253 | } else if (!urls.isEmpty()) { |
254 | text = i18ncp("@action:inmenu" , "Paste One Item" , "Paste %1 Items" , urls.count()); |
255 | } else { |
256 | text = i18nc("@action:inmenu" , "Paste Clipboard Contents..." ); |
257 | } |
258 | } else { |
259 | *enable = false; |
260 | text = i18nc("@action:inmenu" , "Paste" ); |
261 | } |
262 | return text; |
263 | } |
264 | |
265 | KIOWIDGETS_EXPORT void KIO::setClipboardDataCut(QMimeData *mimeData, bool cut) |
266 | { |
267 | const QByteArray cutSelectionData = cut ? "1" : "0" ; |
268 | mimeData->setData(QStringLiteral("application/x-kde-cutselection" ), data: cutSelectionData); |
269 | } |
270 | |
271 | KIOWIDGETS_EXPORT bool KIO::isClipboardDataCut(const QMimeData *mimeData) |
272 | { |
273 | const QByteArray a = mimeData->data(QStringLiteral("application/x-kde-cutselection" )); |
274 | return (!a.isEmpty() && a.at(i: 0) == '1'); |
275 | } |
276 | |