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 | } 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 | |
63 | static 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 | |
72 | static 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 | |
81 | static 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 | |
89 | enum CompareFilesResult { |
90 | Identical, |
91 | PartiallyIdentical, |
92 | Different, |
93 | }; |
94 | static 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 */ |
162 | class Q_DECL_HIDDEN RenameDialog::RenameDialogPrivate |
163 | { |
164 | public: |
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 | |
205 | RenameDialog::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 * = 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 | |
554 | RenameDialog::~RenameDialog() = default; |
555 | |
556 | void 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 | |
574 | QUrl 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 | |
582 | QUrl 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 | |
591 | void RenameDialog::cancelPressed() |
592 | { |
593 | done(Result_Cancel); |
594 | } |
595 | |
596 | // Rename |
597 | void 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 |
618 | void 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 | |
629 | void RenameDialog::skipPressed() |
630 | { |
631 | if (d->bApplyAll && d->bApplyAll->isChecked()) { |
632 | done(Result_AutoSkip); |
633 | } else { |
634 | done(Result_Skip); |
635 | } |
636 | } |
637 | |
638 | void RenameDialog::overwritePressed() |
639 | { |
640 | if (d->bApplyAll && d->bApplyAll->isChecked()) { |
641 | done(Result_OverwriteAll); |
642 | } else { |
643 | done(Result_Overwrite); |
644 | } |
645 | } |
646 | |
647 | void RenameDialog::overwriteWhenOlderPressed() |
648 | { |
649 | if (d->bApplyAll && d->bApplyAll->isChecked()) { |
650 | done(Result_OverwriteWhenOlder); |
651 | } |
652 | } |
653 | |
654 | void RenameDialog::overwriteAllPressed() |
655 | { |
656 | done(Result_OverwriteAll); |
657 | } |
658 | |
659 | void RenameDialog::resumePressed() |
660 | { |
661 | if (d->bApplyAll && d->bApplyAll->isChecked()) { |
662 | done(Result_ResumeAll); |
663 | } else { |
664 | done(Result_Resume); |
665 | } |
666 | } |
667 | |
668 | void RenameDialog::resumeAllPressed() |
669 | { |
670 | done(Result_ResumeAll); |
671 | } |
672 | |
673 | void 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 | |
697 | void 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 | |
707 | void 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 | |
717 | void 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 | |
727 | void 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 | |
737 | void 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 | |
761 | QWidget *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 | |