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

source code of kio/src/widgets/renamedialog.cpp