1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org> |
4 | SPDX-FileCopyrightText: 1999-2008 David Faure <faure@kde.org> |
5 | SPDX-FileCopyrightText: 2001, 2006 Holger Freyther <freyther@kde.org> |
6 | |
7 | SPDX-License-Identifier: LGPL-2.0-or-later |
8 | */ |
9 | |
10 | #include "kio/renamedialog.h" |
11 | #include "../utils_p.h" |
12 | #include "kio_widgets_debug.h" |
13 | #include "kshell.h" |
14 | |
15 | #include <QApplication> |
16 | #include <QCheckBox> |
17 | #include <QDate> |
18 | #include <QLabel> |
19 | #include <QLayout> |
20 | #include <QLineEdit> |
21 | #include <QMenu> |
22 | #include <QMimeDatabase> |
23 | #include <QPixmap> |
24 | #include <QPushButton> |
25 | #include <QScreen> |
26 | #include <QScrollArea> |
27 | #include <QScrollBar> |
28 | #include <QToolButton> |
29 | |
30 | #include <KFileUtils> |
31 | #include <KGuiItem> |
32 | #include <KIconLoader> |
33 | #include <KLocalizedString> |
34 | #include <KMessageBox> |
35 | #include <KSeparator> |
36 | #include <KSqueezedTextLabel> |
37 | #include <KStandardGuiItem> |
38 | #include <KStringHandler> |
39 | #include <kfileitem.h> |
40 | #include <kio/udsentry.h> |
41 | #include <previewjob.h> |
42 | |
43 | using namespace KIO; |
44 | |
45 | static QLabel *createLabel(QWidget *parent, const QString &text, bool containerTitle = false) |
46 | { |
47 | auto *label = new QLabel(parent); |
48 | |
49 | if (containerTitle) { |
50 | QFont font = label->font(); |
51 | font.setBold(true); |
52 | label->setFont(font); |
53 | } |
54 | |
55 | label->setAlignment(Qt::AlignHCenter); |
56 | label->setSizePolicy(hor: QSizePolicy::MinimumExpanding, ver: QSizePolicy::Fixed); |
57 | label->setText(text); |
58 | return label; |
59 | } |
60 | |
61 | static QLabel *createDateLabel(QWidget *parent, const KFileItem &item) |
62 | { |
63 | const bool hasDate = item.entry().contains(field: KIO::UDSEntry::UDS_MODIFICATION_TIME); |
64 | const QString text = hasDate ? i18n("Date: %1" , item.timeString(KFileItem::ModificationTime)) : QString(); |
65 | QLabel *dateLabel = createLabel(parent, text); |
66 | dateLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); |
67 | return dateLabel; |
68 | } |
69 | |
70 | static QLabel *createSizeLabel(QWidget *parent, const KFileItem &item) |
71 | { |
72 | const bool hasSize = item.entry().contains(field: KIO::UDSEntry::UDS_SIZE); |
73 | const QString text = hasSize ? i18n("Size: %1" , KIO::convertSize(item.size())) : QString(); |
74 | QLabel *sizeLabel = createLabel(parent, text); |
75 | sizeLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); |
76 | return sizeLabel; |
77 | } |
78 | |
79 | static KSqueezedTextLabel *createSqueezedLabel(QWidget *parent, const QString &text) |
80 | { |
81 | auto *label = new KSqueezedTextLabel(text, parent); |
82 | label->setAlignment(Qt::AlignHCenter); |
83 | label->setSizePolicy(hor: QSizePolicy::Ignored, ver: QSizePolicy::Fixed); |
84 | return label; |
85 | } |
86 | |
87 | enum CompareFilesResult { |
88 | Identical, |
89 | PartiallyIdentical, |
90 | Different, |
91 | }; |
92 | static CompareFilesResult compareFiles(const QString &filepath, const QString &secondFilePath) |
93 | { |
94 | const qint64 bufferSize = 4096; // 4kb |
95 | QFile f(filepath); |
96 | QFile f2(secondFilePath); |
97 | const auto fileSize = f.size(); |
98 | |
99 | if (fileSize != f2.size()) { |
100 | return CompareFilesResult::Different; |
101 | } |
102 | if (!f.open(flags: QFile::ReadOnly)) { |
103 | qCWarning(KIO_WIDGETS) << "Could not open file for comparison:" << f.fileName(); |
104 | return CompareFilesResult::Different; |
105 | } |
106 | if (!f2.open(flags: QFile::ReadOnly)) { |
107 | f.close(); |
108 | qCWarning(KIO_WIDGETS) << "Could not open file for comparison:" << f2.fileName(); |
109 | return CompareFilesResult::Different; |
110 | } |
111 | |
112 | QByteArray buffer(bufferSize, 0); |
113 | QByteArray buffer2(bufferSize, 0); |
114 | |
115 | auto seekFillBuffer = [bufferSize](qint64 pos, QFile &f, QByteArray &buffer) { |
116 | auto ioresult = f.seek(offset: pos); |
117 | if (ioresult) { |
118 | const int bytesRead = f.read(data: buffer.data(), maxlen: bufferSize); |
119 | ioresult = bytesRead != -1; |
120 | } |
121 | if (!ioresult) { |
122 | qCWarning(KIO_WIDGETS) << "Could not read file for comparison:" << f.fileName(); |
123 | return false; |
124 | } |
125 | return true; |
126 | }; |
127 | |
128 | // compare at the beginning of the files |
129 | bool result = seekFillBuffer(0, f, buffer); |
130 | result = result && seekFillBuffer(0, f2, buffer2); |
131 | result = result && buffer == buffer2; |
132 | |
133 | if (result && fileSize > 2 * bufferSize) { |
134 | // compare the contents in the middle of the files |
135 | result = seekFillBuffer(fileSize / 2 - bufferSize / 2, f, buffer); |
136 | result = result && seekFillBuffer(fileSize / 2 - bufferSize / 2, f2, buffer2); |
137 | result = result && buffer == buffer2; |
138 | } |
139 | |
140 | if (result && fileSize > bufferSize) { |
141 | // compare the contents at the end of the files |
142 | result = seekFillBuffer(fileSize - bufferSize, f, buffer); |
143 | result = result && seekFillBuffer(fileSize - bufferSize, f2, buffer2); |
144 | result = result && buffer == buffer2; |
145 | } |
146 | |
147 | if (!result) { |
148 | return CompareFilesResult::Different; |
149 | } |
150 | |
151 | if (fileSize <= bufferSize * 3) { |
152 | // for files smaller than bufferSize * 3, we in fact compared fully the files |
153 | return CompareFilesResult::Identical; |
154 | } else { |
155 | return CompareFilesResult::PartiallyIdentical; |
156 | } |
157 | } |
158 | |
159 | /** @internal */ |
160 | class Q_DECL_HIDDEN RenameDialog::RenameDialogPrivate |
161 | { |
162 | public: |
163 | RenameDialogPrivate() |
164 | { |
165 | } |
166 | |
167 | void setRenameBoxText(const QString &fileName) |
168 | { |
169 | // sets the text in file name line edit box, selecting the filename (but not the extension if there is one). |
170 | QMimeDatabase db; |
171 | const QString extension = db.suffixForFileName(fileName); |
172 | m_pLineEdit->setText(fileName); |
173 | |
174 | if (!extension.isEmpty()) { |
175 | const int selectionLength = fileName.length() - extension.length() - 1; |
176 | m_pLineEdit->setSelection(0, selectionLength); |
177 | } else { |
178 | m_pLineEdit->selectAll(); |
179 | } |
180 | } |
181 | |
182 | QPushButton *bCancel = nullptr; |
183 | QPushButton *bRename = nullptr; |
184 | QPushButton *bSkip = nullptr; |
185 | QToolButton *bOverwrite = nullptr; |
186 | QAction *bOverwriteWhenOlder = nullptr; |
187 | QPushButton *bResume = nullptr; |
188 | QPushButton *bSuggestNewName = nullptr; |
189 | QCheckBox *bApplyAll = nullptr; |
190 | QLineEdit *m_pLineEdit = nullptr; |
191 | QUrl src; |
192 | QUrl dest; |
193 | bool m_srcPendingPreview = false; |
194 | bool m_destPendingPreview = false; |
195 | QLabel *m_srcPreview = nullptr; |
196 | QLabel *m_destPreview = nullptr; |
197 | QLabel *m_srcDateLabel = nullptr; |
198 | QLabel *m_destDateLabel = nullptr; |
199 | KFileItem srcItem; |
200 | KFileItem destItem; |
201 | }; |
202 | |
203 | RenameDialog::RenameDialog(QWidget *parent, |
204 | const QString &title, |
205 | const QUrl &_src, |
206 | const QUrl &_dest, |
207 | RenameDialog_Options _options, |
208 | KIO::filesize_t sizeSrc, |
209 | KIO::filesize_t sizeDest, |
210 | const QDateTime &ctimeSrc, |
211 | const QDateTime &ctimeDest, |
212 | const QDateTime &mtimeSrc, |
213 | const QDateTime &mtimeDest) |
214 | : QDialog(parent) |
215 | , d(new RenameDialogPrivate) |
216 | { |
217 | setObjectName(QStringLiteral("KIO::RenameDialog" )); |
218 | |
219 | d->src = _src; |
220 | d->dest = _dest; |
221 | |
222 | setWindowTitle(title); |
223 | |
224 | d->bCancel = new QPushButton(this); |
225 | KGuiItem::assign(button: d->bCancel, item: KStandardGuiItem::cancel()); |
226 | connect(sender: d->bCancel, signal: &QAbstractButton::clicked, context: this, slot: &RenameDialog::cancelPressed); |
227 | |
228 | if (_options & RenameDialog_MultipleItems) { |
229 | d->bApplyAll = new QCheckBox(i18n("Appl&y to All" ), this); |
230 | d->bApplyAll->setToolTip((_options & RenameDialog_DestIsDirectory) ? i18n("When this is checked the button pressed will be applied to all " |
231 | "subsequent folder conflicts for the remainder of the current job.\n" |
232 | "Unless you press Skip you will still be prompted in case of a " |
233 | "conflict with an existing file in the directory." ) |
234 | : i18n("When this is checked the button pressed will be applied to " |
235 | "all subsequent conflicts for the remainder of the current job." )); |
236 | connect(sender: d->bApplyAll, signal: &QAbstractButton::clicked, context: this, slot: &RenameDialog::applyAllPressed); |
237 | } |
238 | |
239 | if (!(_options & RenameDialog_NoRename)) { |
240 | d->bRename = new QPushButton(i18n("&Rename" ), this); |
241 | d->bRename->setEnabled(false); |
242 | d->bSuggestNewName = new QPushButton(i18n("Suggest New &Name" ), this); |
243 | connect(sender: d->bSuggestNewName, signal: &QAbstractButton::clicked, context: this, slot: &RenameDialog::suggestNewNamePressed); |
244 | connect(sender: d->bRename, signal: &QAbstractButton::clicked, context: this, slot: &RenameDialog::renamePressed); |
245 | } |
246 | |
247 | if ((_options & RenameDialog_MultipleItems) && (_options & RenameDialog_Skip)) { |
248 | d->bSkip = new QPushButton(i18n("&Skip" ), this); |
249 | d->bSkip->setToolTip((_options & RenameDialog_DestIsDirectory) ? i18n("Do not copy or move this folder, skip to the next item instead" ) |
250 | : i18n("Do not copy or move this file, skip to the next item instead" )); |
251 | connect(sender: d->bSkip, signal: &QAbstractButton::clicked, context: this, slot: &RenameDialog::skipPressed); |
252 | } |
253 | |
254 | if (_options & RenameDialog_Overwrite) { |
255 | d->bOverwrite = new QToolButton(this); |
256 | d->bOverwrite->setText(KStandardGuiItem::overwrite().text()); |
257 | d->bOverwrite->setIcon(QIcon::fromTheme(name: KStandardGuiItem::overwrite().iconName())); |
258 | d->bOverwrite->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); |
259 | |
260 | if (_options & RenameDialog_DestIsDirectory) { |
261 | d->bOverwrite->setText(i18nc("Write files into an existing folder" , "&Write Into" )); |
262 | d->bOverwrite->setIcon(QIcon()); |
263 | d->bOverwrite->setToolTip( |
264 | i18n("Files and folders will be copied into the existing directory, alongside its existing contents.\nYou will be prompted again in case of a " |
265 | "conflict with an existing file in the directory." )); |
266 | |
267 | } else if ((_options & RenameDialog_MultipleItems) && mtimeSrc.isValid() && mtimeDest.isValid()) { |
268 | d->bOverwriteWhenOlder = new QAction(QIcon::fromTheme(name: KStandardGuiItem::overwrite().iconName()), |
269 | i18nc("Overwrite files into an existing folder when files are older" , "&Overwrite older files" ), |
270 | this); |
271 | d->bOverwriteWhenOlder->setEnabled(false); |
272 | d->bOverwriteWhenOlder->setToolTip( |
273 | i18n("Destination files which have older modification times will be overwritten by the source, skipped otherwise." )); |
274 | connect(sender: d->bOverwriteWhenOlder, signal: &QAction::triggered, context: this, slot: &RenameDialog::overwriteWhenOlderPressed); |
275 | |
276 | auto * = new QMenu(); |
277 | overwriteMenu->addAction(action: d->bOverwriteWhenOlder); |
278 | d->bOverwrite->setMenu(overwriteMenu); |
279 | d->bOverwrite->setPopupMode(QToolButton::MenuButtonPopup); |
280 | } |
281 | connect(sender: d->bOverwrite, signal: &QAbstractButton::clicked, context: this, slot: &RenameDialog::overwritePressed); |
282 | } |
283 | |
284 | if (_options & RenameDialog_Resume) { |
285 | d->bResume = new QPushButton(i18n("&Resume" ), this); |
286 | connect(sender: d->bResume, signal: &QAbstractButton::clicked, context: this, slot: &RenameDialog::resumePressed); |
287 | } |
288 | |
289 | auto *pLayout = new QVBoxLayout(this); |
290 | pLayout->addStrut(400); // makes dlg at least that wide |
291 | |
292 | // User tries to overwrite a file with itself ? |
293 | if (_options & RenameDialog_OverwriteItself) { |
294 | auto *lb = new QLabel(i18n("This action would overwrite '%1' with itself.\n" |
295 | "Please enter a new file name:" , |
296 | KStringHandler::csqueeze(d->src.toDisplayString(QUrl::PreferLocalFile), 100)), |
297 | this); |
298 | lb->setTextFormat(Qt::PlainText); |
299 | |
300 | d->bRename->setText(i18n("C&ontinue" )); |
301 | pLayout->addWidget(lb); |
302 | } else if (_options & RenameDialog_Overwrite) { |
303 | if (d->src.isLocalFile()) { |
304 | d->srcItem = KFileItem(d->src); |
305 | } else { |
306 | UDSEntry srcUds; |
307 | |
308 | srcUds.reserve(size: 4); |
309 | srcUds.fastInsert(field: UDSEntry::UDS_NAME, value: d->src.fileName()); |
310 | if (mtimeSrc.isValid()) { |
311 | srcUds.fastInsert(field: UDSEntry::UDS_MODIFICATION_TIME, l: mtimeSrc.toMSecsSinceEpoch() / 1000); |
312 | } |
313 | if (ctimeSrc.isValid()) { |
314 | srcUds.fastInsert(field: UDSEntry::UDS_CREATION_TIME, l: ctimeSrc.toMSecsSinceEpoch() / 1000); |
315 | } |
316 | if (sizeSrc != KIO::filesize_t(-1)) { |
317 | srcUds.fastInsert(field: UDSEntry::UDS_SIZE, l: sizeSrc); |
318 | } |
319 | |
320 | d->srcItem = KFileItem(srcUds, d->src); |
321 | } |
322 | |
323 | if (d->dest.isLocalFile()) { |
324 | d->destItem = KFileItem(d->dest); |
325 | } else { |
326 | UDSEntry destUds; |
327 | |
328 | destUds.reserve(size: 4); |
329 | destUds.fastInsert(field: UDSEntry::UDS_NAME, value: d->dest.fileName()); |
330 | if (mtimeDest.isValid()) { |
331 | destUds.fastInsert(field: UDSEntry::UDS_MODIFICATION_TIME, l: mtimeDest.toMSecsSinceEpoch() / 1000); |
332 | } |
333 | if (ctimeDest.isValid()) { |
334 | destUds.fastInsert(field: UDSEntry::UDS_CREATION_TIME, l: ctimeDest.toMSecsSinceEpoch() / 1000); |
335 | } |
336 | if (sizeDest != KIO::filesize_t(-1)) { |
337 | destUds.fastInsert(field: UDSEntry::UDS_SIZE, l: sizeDest); |
338 | } |
339 | |
340 | d->destItem = KFileItem(destUds, d->dest); |
341 | } |
342 | |
343 | d->m_srcPreview = createLabel(parent: this, text: QString()); |
344 | d->m_destPreview = createLabel(parent: this, text: QString()); |
345 | |
346 | d->m_srcPreview->setMinimumHeight(KIconLoader::SizeHuge); |
347 | d->m_srcPreview->setMinimumWidth(KIconLoader::SizeHuge); |
348 | d->m_destPreview->setMinimumHeight(KIconLoader::SizeHuge); |
349 | d->m_destPreview->setMinimumWidth(KIconLoader::SizeHuge); |
350 | |
351 | d->m_srcPreview->setAlignment(Qt::AlignCenter); |
352 | d->m_destPreview->setAlignment(Qt::AlignCenter); |
353 | |
354 | d->m_srcPendingPreview = true; |
355 | d->m_destPendingPreview = true; |
356 | |
357 | // create layout |
358 | auto *gridLayout = new QGridLayout(); |
359 | pLayout->addLayout(layout: gridLayout); |
360 | |
361 | int gridRow = 0; |
362 | auto question = i18n("Would you like to overwrite the destination?" ); |
363 | if (d->srcItem.isDir() && d->destItem.isDir()) { |
364 | question = i18n("Would you like to merge the contents of '%1' into '%2'?" , |
365 | KShell::tildeCollapse(d->src.toDisplayString(QUrl::PreferLocalFile)), |
366 | KShell::tildeCollapse(d->dest.toDisplayString(QUrl::PreferLocalFile))); |
367 | } |
368 | auto *questionLabel = new QLabel(question, this); |
369 | questionLabel->setAlignment(Qt::AlignHCenter); |
370 | gridLayout->addWidget(questionLabel, row: gridRow, column: 0, rowSpan: 1, columnSpan: 4); // takes the complete first line |
371 | |
372 | QLabel *srcTitle = createLabel(parent: this, i18n("Source" ), containerTitle: true); |
373 | gridLayout->addWidget(srcTitle, row: ++gridRow, column: 0, rowSpan: 1, columnSpan: 2); |
374 | QLabel *destTitle = createLabel(parent: this, i18n("Destination" ), containerTitle: true); |
375 | gridLayout->addWidget(destTitle, row: gridRow, column: 2, rowSpan: 1, columnSpan: 2); |
376 | |
377 | // The labels containing src and dest path |
378 | QLabel *srcUrlLabel = createSqueezedLabel(parent: this, text: d->src.toDisplayString(options: QUrl::PreferLocalFile)); |
379 | srcUrlLabel->setTextFormat(Qt::PlainText); |
380 | gridLayout->addWidget(srcUrlLabel, row: ++gridRow, column: 0, rowSpan: 1, columnSpan: 2); |
381 | QLabel *destUrlLabel = createSqueezedLabel(parent: this, text: d->dest.toDisplayString(options: QUrl::PreferLocalFile)); |
382 | destUrlLabel->setTextFormat(Qt::PlainText); |
383 | gridLayout->addWidget(destUrlLabel, row: gridRow, column: 2, rowSpan: 1, columnSpan: 2); |
384 | |
385 | gridRow++; |
386 | |
387 | // src container (preview, size, date) |
388 | QLabel *srcSizeLabel = createSizeLabel(parent: this, item: d->srcItem); |
389 | d->m_srcDateLabel = createDateLabel(parent: this, item: d->srcItem); |
390 | QWidget *srcContainer = createContainerWidget(preview: d->m_srcPreview, SizeLabel: srcSizeLabel, DateLabel: d->m_srcDateLabel); |
391 | gridLayout->addWidget(srcContainer, row: gridRow, column: 0, rowSpan: 1, columnSpan: 2); |
392 | |
393 | // dest container (preview, size, date) |
394 | QLabel *destSizeLabel = createSizeLabel(parent: this, item: d->destItem); |
395 | d->m_destDateLabel = createDateLabel(parent: this, item: d->destItem); |
396 | QWidget *destContainer = createContainerWidget(preview: d->m_destPreview, SizeLabel: destSizeLabel, DateLabel: d->m_destDateLabel); |
397 | gridLayout->addWidget(destContainer, row: gridRow, column: 2, rowSpan: 1, columnSpan: 2); |
398 | |
399 | // Verdicts |
400 | auto *hbox_verdicts = new QHBoxLayout(); |
401 | pLayout->addLayout(layout: hbox_verdicts); |
402 | hbox_verdicts->addStretch(stretch: 1); |
403 | |
404 | if (mtimeSrc > mtimeDest) { |
405 | hbox_verdicts->addWidget(createLabel(parent: this, i18n("The source is <b>more recent</b>." ))); |
406 | } else if (mtimeDest > mtimeSrc) { |
407 | hbox_verdicts->addWidget(createLabel(parent: this, i18n("The source is <b>older</b>." ))); |
408 | }; |
409 | |
410 | if (d->srcItem.entry().contains(field: KIO::UDSEntry::UDS_SIZE) && d->destItem.entry().contains(field: KIO::UDSEntry::UDS_SIZE) |
411 | && d->srcItem.size() != d->destItem.size()) { |
412 | QString text; |
413 | KIO::filesize_t diff = 0; |
414 | if (d->destItem.size() > d->srcItem.size()) { |
415 | diff = d->destItem.size() - d->srcItem.size(); |
416 | text = i18n("The source is <b>smaller by %1</b>." , KIO::convertSize(diff)); |
417 | } else { |
418 | diff = d->srcItem.size() - d->destItem.size(); |
419 | text = i18n("The source is <b>bigger by %1</b>." , KIO::convertSize(diff)); |
420 | } |
421 | hbox_verdicts->addWidget(createLabel(parent: this, text)); |
422 | } |
423 | |
424 | // check files contents for local files |
425 | if ((d->dest.isLocalFile() && !(_options & RenameDialog_DestIsDirectory)) && (d->src.isLocalFile() && !(_options & RenameDialog_SourceIsDirectory)) |
426 | && (d->srcItem.size() == d->destItem.size())) { |
427 | const CompareFilesResult CompareFilesResult = compareFiles(filepath: d->src.toLocalFile(), secondFilePath: d->dest.toLocalFile()); |
428 | |
429 | QString text; |
430 | switch (CompareFilesResult) { |
431 | case CompareFilesResult::Identical: |
432 | text = i18n("The files are <b>identical</b>." ); |
433 | break; |
434 | case CompareFilesResult::PartiallyIdentical: |
435 | text = i18n("The files <b>seem identical</b>." ); |
436 | break; |
437 | case CompareFilesResult::Different: |
438 | text = i18n("The files are <b>different</b>." ); |
439 | break; |
440 | } |
441 | QLabel *filesIdenticalLabel = createLabel(parent: this, text); |
442 | if (CompareFilesResult == CompareFilesResult::PartiallyIdentical) { |
443 | auto *pixmapLabel = new QLabel(this); |
444 | pixmapLabel->setPixmap(QIcon::fromTheme(QStringLiteral("help-about" )).pixmap(size: QSize(16, 16))); |
445 | pixmapLabel->setToolTip( |
446 | i18n("The files are likely to be identical: they have the same size and their contents are the same at the beginning, middle and end." )); |
447 | pixmapLabel->setCursor(Qt::WhatsThisCursor); |
448 | |
449 | auto *hbox = new QHBoxLayout(); |
450 | hbox->addWidget(filesIdenticalLabel); |
451 | hbox->addWidget(pixmapLabel); |
452 | hbox_verdicts->addLayout(layout: hbox); |
453 | } else { |
454 | hbox_verdicts->addWidget(filesIdenticalLabel); |
455 | } |
456 | } |
457 | hbox_verdicts->addStretch(stretch: 1); |
458 | |
459 | } else { |
460 | // This is the case where we don't want to allow overwriting, the existing |
461 | // file must be preserved (e.g. when renaming). |
462 | QString sentence1; |
463 | |
464 | if (mtimeDest < mtimeSrc) { |
465 | sentence1 = i18n("An older item named '%1' already exists." , d->dest.toDisplayString(QUrl::PreferLocalFile)); |
466 | } else if (mtimeDest == mtimeSrc) { |
467 | sentence1 = i18n("A similar file named '%1' already exists." , d->dest.toDisplayString(QUrl::PreferLocalFile)); |
468 | } else { |
469 | sentence1 = i18n("A more recent item named '%1' already exists." , d->dest.toDisplayString(QUrl::PreferLocalFile)); |
470 | } |
471 | |
472 | QLabel *lb = new KSqueezedTextLabel(sentence1, this); |
473 | lb->setTextFormat(Qt::PlainText); |
474 | pLayout->addWidget(lb); |
475 | } |
476 | |
477 | if (!(_options & RenameDialog_OverwriteItself) && !(_options & RenameDialog_NoRename)) { |
478 | if (_options & RenameDialog_Overwrite) { |
479 | pLayout->addSpacing(size: 15); // spacer |
480 | } |
481 | |
482 | auto *lb2 = new QLabel(i18n("Rename:" ), this); |
483 | pLayout->addWidget(lb2); |
484 | } |
485 | |
486 | auto *layout2 = new QHBoxLayout(); |
487 | pLayout->addLayout(layout: layout2); |
488 | |
489 | d->m_pLineEdit = new QLineEdit(this); |
490 | layout2->addWidget(d->m_pLineEdit); |
491 | |
492 | if (d->bRename) { |
493 | const QString fileName = d->dest.fileName(); |
494 | d->setRenameBoxText(KIO::decodeFileName(str: fileName)); |
495 | |
496 | connect(sender: d->m_pLineEdit, signal: &QLineEdit::textChanged, context: this, slot: &RenameDialog::enableRenameButton); |
497 | |
498 | d->m_pLineEdit->setFocus(); |
499 | } else { |
500 | d->m_pLineEdit->hide(); |
501 | } |
502 | |
503 | if (d->bSuggestNewName) { |
504 | layout2->addWidget(d->bSuggestNewName); |
505 | setTabOrder(d->m_pLineEdit, d->bSuggestNewName); |
506 | } |
507 | |
508 | auto *layout = new QHBoxLayout(); |
509 | pLayout->addLayout(layout); |
510 | |
511 | layout->setContentsMargins(left: 0, top: 10, right: 0, bottom: 0); // add some space above the bottom row with buttons |
512 | layout->addStretch(stretch: 1); |
513 | |
514 | if (d->bApplyAll) { |
515 | layout->addWidget(d->bApplyAll); |
516 | setTabOrder(d->bApplyAll, d->bCancel); |
517 | } |
518 | |
519 | if (d->bSkip) { |
520 | layout->addWidget(d->bSkip); |
521 | setTabOrder(d->bSkip, d->bCancel); |
522 | } |
523 | |
524 | if (d->bRename) { |
525 | layout->addWidget(d->bRename); |
526 | setTabOrder(d->bRename, d->bCancel); |
527 | } |
528 | |
529 | if (d->bOverwrite) { |
530 | layout->addWidget(d->bOverwrite); |
531 | setTabOrder(d->bOverwrite, d->bCancel); |
532 | } |
533 | |
534 | if (d->bResume) { |
535 | layout->addWidget(d->bResume); |
536 | setTabOrder(d->bResume, d->bCancel); |
537 | } |
538 | |
539 | d->bCancel->setDefault(true); |
540 | layout->addWidget(d->bCancel); |
541 | |
542 | resize(sizeHint()); |
543 | |
544 | #if 1 // without kfilemetadata |
545 | // don't wait for kfilemetadata, but wait until the layouting is done |
546 | if (_options & RenameDialog_Overwrite) { |
547 | QMetaObject::invokeMethod(object: this, function: &KIO::RenameDialog::resizePanels, type: Qt::QueuedConnection); |
548 | } |
549 | #endif |
550 | } |
551 | |
552 | RenameDialog::~RenameDialog() = default; |
553 | |
554 | void RenameDialog::enableRenameButton(const QString &newDest) |
555 | { |
556 | if (newDest != KIO::decodeFileName(str: d->dest.fileName()) && !newDest.isEmpty()) { |
557 | d->bRename->setEnabled(true); |
558 | d->bRename->setDefault(true); |
559 | |
560 | if (d->bOverwrite) { |
561 | d->bOverwrite->setEnabled(false); // prevent confusion (#83114) |
562 | } |
563 | } else { |
564 | d->bRename->setEnabled(false); |
565 | |
566 | if (d->bOverwrite) { |
567 | d->bOverwrite->setEnabled(true); |
568 | } |
569 | } |
570 | } |
571 | |
572 | QUrl RenameDialog::newDestUrl() |
573 | { |
574 | const QString fileName = d->m_pLineEdit->text(); |
575 | QUrl newDest = d->dest.adjusted(options: QUrl::RemoveFilename); // keeps trailing slash |
576 | newDest.setPath(path: newDest.path() + KIO::encodeFileName(str: fileName)); |
577 | return newDest; |
578 | } |
579 | |
580 | QUrl RenameDialog::autoDestUrl() const |
581 | { |
582 | const QUrl destDirectory = d->dest.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash); |
583 | const QString newName = KFileUtils::suggestName(baseURL: destDirectory, oldName: d->dest.fileName()); |
584 | QUrl newDest(destDirectory); |
585 | newDest.setPath(path: Utils::concatPaths(path1: newDest.path(), path2: newName)); |
586 | return newDest; |
587 | } |
588 | |
589 | void RenameDialog::cancelPressed() |
590 | { |
591 | done(Result_Cancel); |
592 | } |
593 | |
594 | // Rename |
595 | void RenameDialog::renamePressed() |
596 | { |
597 | if (d->m_pLineEdit->text().isEmpty()) { |
598 | return; |
599 | } |
600 | |
601 | if (d->bApplyAll && d->bApplyAll->isChecked()) { |
602 | done(Result_AutoRename); |
603 | } else { |
604 | const QUrl u = newDestUrl(); |
605 | if (!u.isValid()) { |
606 | KMessageBox::error(parent: this, i18n("Malformed URL\n%1" , u.errorString())); |
607 | qCWarning(KIO_WIDGETS) << u.errorString(); |
608 | return; |
609 | } |
610 | |
611 | done(Result_Rename); |
612 | } |
613 | } |
614 | |
615 | // Propose button clicked |
616 | void RenameDialog::suggestNewNamePressed() |
617 | { |
618 | /* no name to play with */ |
619 | if (d->m_pLineEdit->text().isEmpty()) { |
620 | return; |
621 | } |
622 | |
623 | QUrl destDirectory = d->dest.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash); |
624 | d->setRenameBoxText(KFileUtils::suggestName(baseURL: destDirectory, oldName: d->m_pLineEdit->text())); |
625 | } |
626 | |
627 | void RenameDialog::skipPressed() |
628 | { |
629 | if (d->bApplyAll && d->bApplyAll->isChecked()) { |
630 | done(Result_AutoSkip); |
631 | } else { |
632 | done(Result_Skip); |
633 | } |
634 | } |
635 | |
636 | void RenameDialog::overwritePressed() |
637 | { |
638 | if (d->bApplyAll && d->bApplyAll->isChecked()) { |
639 | done(Result_OverwriteAll); |
640 | } else { |
641 | done(Result_Overwrite); |
642 | } |
643 | } |
644 | |
645 | void RenameDialog::overwriteWhenOlderPressed() |
646 | { |
647 | if (d->bApplyAll && d->bApplyAll->isChecked()) { |
648 | done(Result_OverwriteWhenOlder); |
649 | } |
650 | } |
651 | |
652 | void RenameDialog::overwriteAllPressed() |
653 | { |
654 | done(Result_OverwriteAll); |
655 | } |
656 | |
657 | void RenameDialog::resumePressed() |
658 | { |
659 | if (d->bApplyAll && d->bApplyAll->isChecked()) { |
660 | done(Result_ResumeAll); |
661 | } else { |
662 | done(Result_Resume); |
663 | } |
664 | } |
665 | |
666 | void RenameDialog::resumeAllPressed() |
667 | { |
668 | done(Result_ResumeAll); |
669 | } |
670 | |
671 | void RenameDialog::applyAllPressed() |
672 | { |
673 | const bool applyAll = d->bApplyAll && d->bApplyAll->isChecked(); |
674 | |
675 | if (applyAll) { |
676 | d->m_pLineEdit->setText(KIO::decodeFileName(str: d->dest.fileName())); |
677 | d->m_pLineEdit->setEnabled(false); |
678 | } else { |
679 | d->m_pLineEdit->setEnabled(true); |
680 | } |
681 | |
682 | if (d->bRename) { |
683 | d->bRename->setEnabled(applyAll); |
684 | } |
685 | |
686 | if (d->bSuggestNewName) { |
687 | d->bSuggestNewName->setEnabled(!applyAll); |
688 | } |
689 | |
690 | if (d->bOverwriteWhenOlder) { |
691 | d->bOverwriteWhenOlder->setEnabled(applyAll); |
692 | } |
693 | } |
694 | |
695 | void RenameDialog::showSrcIcon(const KFileItem &fileitem) |
696 | { |
697 | // The preview job failed, show a standard file icon. |
698 | d->m_srcPendingPreview = false; |
699 | |
700 | const int size = d->m_srcPreview->height(); |
701 | const QPixmap pix = QIcon::fromTheme(name: fileitem.iconName(), fallback: QIcon::fromTheme(QStringLiteral("application-octet-stream" ))).pixmap(extent: size); |
702 | d->m_srcPreview->setPixmap(pix); |
703 | } |
704 | |
705 | void RenameDialog::showDestIcon(const KFileItem &fileitem) |
706 | { |
707 | // The preview job failed, show a standard file icon. |
708 | d->m_destPendingPreview = false; |
709 | |
710 | const int size = d->m_destPreview->height(); |
711 | const QPixmap pix = QIcon::fromTheme(name: fileitem.iconName(), fallback: QIcon::fromTheme(QStringLiteral("application-octet-stream" ))).pixmap(extent: size); |
712 | d->m_destPreview->setPixmap(pix); |
713 | } |
714 | |
715 | void RenameDialog::showSrcPreview(const KFileItem &fileitem, const QPixmap &pixmap) |
716 | { |
717 | Q_UNUSED(fileitem); |
718 | |
719 | if (d->m_srcPendingPreview) { |
720 | d->m_srcPreview->setPixmap(pixmap); |
721 | d->m_srcPendingPreview = false; |
722 | } |
723 | } |
724 | |
725 | void RenameDialog::showDestPreview(const KFileItem &fileitem, const QPixmap &pixmap) |
726 | { |
727 | Q_UNUSED(fileitem); |
728 | |
729 | if (d->m_destPendingPreview) { |
730 | d->m_destPreview->setPixmap(pixmap); |
731 | d->m_destPendingPreview = false; |
732 | } |
733 | } |
734 | |
735 | void RenameDialog::resizePanels() |
736 | { |
737 | Q_ASSERT(d->m_srcPreview != nullptr); |
738 | Q_ASSERT(d->m_destPreview != nullptr); |
739 | |
740 | // Force keep the same (max) width of date width for src and dest |
741 | int destDateWidth = d->m_destDateLabel->width(); |
742 | int srcDateWidth = d->m_srcDateLabel->width(); |
743 | int minDateWidth = std::max(a: destDateWidth, b: srcDateWidth); |
744 | d->m_srcDateLabel->setMinimumWidth(minDateWidth); |
745 | d->m_destDateLabel->setMinimumWidth(minDateWidth); |
746 | |
747 | KIO::PreviewJob *srcJob = KIO::filePreview(items: KFileItemList{d->srcItem}, size: QSize(d->m_srcPreview->width() * qreal(0.9), d->m_srcPreview->height())); |
748 | srcJob->setScaleType(KIO::PreviewJob::Unscaled); |
749 | |
750 | KIO::PreviewJob *destJob = KIO::filePreview(items: KFileItemList{d->destItem}, size: QSize(d->m_destPreview->width() * qreal(0.9), d->m_destPreview->height())); |
751 | destJob->setScaleType(KIO::PreviewJob::Unscaled); |
752 | |
753 | connect(sender: srcJob, signal: &PreviewJob::gotPreview, context: this, slot: &RenameDialog::showSrcPreview); |
754 | connect(sender: destJob, signal: &PreviewJob::gotPreview, context: this, slot: &RenameDialog::showDestPreview); |
755 | connect(sender: srcJob, signal: &PreviewJob::failed, context: this, slot: &RenameDialog::showSrcIcon); |
756 | connect(sender: destJob, signal: &PreviewJob::failed, context: this, slot: &RenameDialog::showDestIcon); |
757 | } |
758 | |
759 | QWidget *RenameDialog::createContainerWidget(QLabel *preview, QLabel *SizeLabel, QLabel *DateLabel) |
760 | { |
761 | auto *widgetContainer = new QWidget(); |
762 | auto *containerLayout = new QHBoxLayout(widgetContainer); |
763 | |
764 | containerLayout->addStretch(stretch: 1); |
765 | containerLayout->addWidget(preview); |
766 | |
767 | auto *detailsLayout = new QVBoxLayout(widgetContainer); |
768 | detailsLayout->addStretch(stretch: 1); |
769 | detailsLayout->addWidget(SizeLabel); |
770 | detailsLayout->addWidget(DateLabel); |
771 | detailsLayout->addStretch(stretch: 1); |
772 | |
773 | containerLayout->addLayout(layout: detailsLayout); |
774 | containerLayout->addStretch(stretch: 1); |
775 | |
776 | return widgetContainer; |
777 | } |
778 | |
779 | #include "moc_renamedialog.cpp" |
780 | |