1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2006-2010 Peter Penz <peter.penz@gmx.at>
4 SPDX-FileCopyrightText: 2020 Méven Car <meven.car@kdemail.net>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "renamefiledialog.h"
10
11#include <KGuiItem>
12#include <KIO/BatchRenameJob>
13#include <KIO/CopyJob>
14#include <KIO/FileUndoManager>
15#include <KJobUiDelegate>
16#include <KJobWidgets>
17#include <KLocalizedString>
18#include <KMessageWidget>
19
20#include <QComboBox>
21#include <QDialogButtonBox>
22#include <QHBoxLayout>
23#include <QLabel>
24#include <QLineEdit>
25#include <QMimeDatabase>
26#include <QPushButton>
27#include <QShowEvent>
28#include <QSpinBox>
29#include <QTimer>
30
31#include <set>
32
33namespace
34{
35
36enum Result {
37 Ok,
38 Invalid
39};
40
41// TODO c++23 port to std::expected
42struct ValidationResult {
43 Result result;
44 QString text;
45 KMessageWidget::MessageType type;
46};
47inline ValidationResult ok()
48{
49 return ValidationResult{.result: Result::Ok, .text: QString(), .type: KMessageWidget::MessageType::Information};
50};
51inline ValidationResult invalid(const QString &text)
52{
53 return ValidationResult{.result: Result::Invalid, .text: text, .type: KMessageWidget::MessageType::Error};
54};
55
56/// design pattern strategy
57class RenameOperationAbstractStrategy
58{
59public:
60 RenameOperationAbstractStrategy() { };
61 virtual ~RenameOperationAbstractStrategy() { };
62
63 virtual QWidget *init(const KFileItemList &items, QWidget *parent, std::function<void()> &updateCallback) = 0;
64 virtual const std::function<QString(const QStringView fileName)> renameFunction() = 0;
65 virtual ValidationResult validate(const KFileItemList &items, const QStringView fileName) = 0;
66};
67
68enum RenameStrategy {
69 // SingleFileRename
70 Enumerate,
71 Replace,
72 AddText,
73 // Regex
74};
75
76class SingleFileRenameStrategy : public RenameOperationAbstractStrategy
77{
78public:
79 ~SingleFileRenameStrategy() override
80 {
81 }
82
83 QWidget *init(const KFileItemList &items, QWidget *parent, std::function<void()> &updateCallback) override
84 {
85 Q_UNUSED(updateCallback)
86
87 QWidget *widget = new QWidget(parent);
88 auto layout = new QVBoxLayout(widget);
89
90 QString newName = items.first().name();
91 auto fileNameLabel = new QLabel(xi18nc("@label:textbox", "Rename the item <filename>%1</filename> to:", newName), widget);
92 fileNameLabel->setTextFormat(Qt::PlainText);
93
94 int selectionLength = newName.length();
95 // If the current item is a directory, select the whole file name.
96 if (!items.first().isDir()) {
97 QMimeDatabase db;
98 const QString extension = db.suffixForFileName(fileName: items.first().name());
99 if (extension.length() > 0) {
100 // Don't select the extension
101 selectionLength -= extension.length() + 1;
102 }
103 }
104
105 fileNameEdit = new QLineEdit(newName, widget);
106 fileNameEdit->setSelection(0, selectionLength);
107 fileNameLabel->setBuddy(fileNameEdit);
108 widget->setFocusProxy(fileNameEdit);
109
110 QObject::connect(sender: fileNameEdit, signal: &QLineEdit::textChanged, slot&: updateCallback);
111
112 layout->addWidget(fileNameLabel);
113 layout->addWidget(fileNameEdit);
114
115 fileNameEdit->setFocus();
116
117 return widget;
118 }
119
120 const std::function<QString(const QStringView fileName)> renameFunction() override
121 {
122 return [this](const QStringView /*fileName */) {
123 return fileNameEdit->text();
124 };
125 }
126
127 ValidationResult validate(const KFileItemList &items, const QStringView fileName) override
128 {
129 const auto oldUrl = items.at(i: 0).url();
130 const auto placeholder = fileNameEdit->text();
131 if (placeholder.isEmpty()) {
132 return invalid(text: QString());
133 }
134 QUrl newUrl = oldUrl.adjusted(options: QUrl::RemoveFilename);
135 newUrl.setPath(path: newUrl.path() + KIO::encodeFileName(str: fileName.toString()));
136 bool fileExists = false;
137 if (oldUrl.isLocalFile() && newUrl != oldUrl) {
138 fileExists = QFile::exists(fileName: newUrl.toLocalFile());
139 }
140 if (fileExists) {
141 return invalid(xi18nc("@info error a file already exists", "A file named <filename>%1</filename> already exists.", newUrl.fileName()));
142 }
143 if (placeholder == QLatin1String("..") || (placeholder == QLatin1String("."))) {
144 return invalid(xi18nc("@info %1 is an invalid filename", "<filename>%1</filename> is not a valid file name.", placeholder));
145 }
146 return ok();
147 }
148
149 QLineEdit *fileNameEdit;
150};
151
152class EnumerateStrategy : public RenameOperationAbstractStrategy
153{
154public:
155 ~EnumerateStrategy() override
156 {
157 }
158
159 QWidget *init(const KFileItemList &items, QWidget *parent, std::function<void()> &updateCallback) override
160 {
161 QWidget *widget = new QWidget(parent);
162 auto layout = new QVBoxLayout(widget);
163
164 auto renameLabel = new QLabel(i18ncp("@label:textbox", "Rename the %1 selected item to:", "Rename the %1 selected items to:", items.count()), widget);
165 layout->addWidget(renameLabel);
166
167 auto indexLabel = new QLabel(i18nc("@info", "# will be replaced by ascending numbers starting with:"), widget);
168 indexSpinBox = new QSpinBox(widget);
169 indexSpinBox->setMinimum(0);
170 indexSpinBox->setMaximum(1'000'000'000);
171 indexSpinBox->setSingleStep(1);
172 indexSpinBox->setValue(1);
173 indexSpinBox->setDisplayIntegerBase(10);
174 indexLabel->setBuddy(indexSpinBox);
175
176 auto newName = i18nc("This a template for new filenames, # is replaced by a number later, must be the end character", "New name #");
177 placeHolderEdit = new QLineEdit(newName, widget);
178
179 layout->addWidget(placeHolderEdit);
180
181 // Layout
182 auto indexLayout = new QHBoxLayout;
183 indexLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
184 indexLayout->addWidget(indexLabel);
185 indexLayout->addWidget(indexSpinBox);
186 layout->addLayout(layout: indexLayout);
187
188 QObject::connect(sender: indexSpinBox, signal: &QSpinBox::valueChanged, slot&: updateCallback);
189 QObject::connect(sender: placeHolderEdit, signal: &QLineEdit::textChanged, slot&: updateCallback);
190
191 placeHolderEdit->setSelection(0, newName.length() - 1);
192 placeHolderEdit->setFocus();
193
194 widget->setTabOrder(placeHolderEdit, indexSpinBox);
195 widget->setFocusProxy(placeHolderEdit);
196
197 // Check for extensions.
198 std::set<QString> extensions;
199 QMimeDatabase db;
200 for (const auto &fileItem : std::as_const(t: items)) {
201 const QString extension = fileItem.suffix();
202 const auto [it, isInserted] = extensions.insert(x: extension);
203 if (!isInserted) {
204 allExtensionsDifferent = false;
205 break;
206 }
207 }
208
209 return widget;
210 }
211
212 const std::function<QString(const QStringView fileName)> renameFunction() override
213 {
214 auto newName = placeHolderEdit->text();
215 const auto placeHolder = QLatin1Char('#');
216
217 // look for consecutive # groups
218 static const QRegularExpression regex(QStringLiteral("%1+").arg(a: placeHolder));
219
220 auto matchDashes = regex.globalMatch(subject: newName);
221 QRegularExpressionMatch lastMatchDashes;
222 int matchCount = 0;
223 while (matchDashes.hasNext()) {
224 lastMatchDashes = matchDashes.next();
225 matchCount++;
226 }
227
228 validPlaceholder = matchCount == 1;
229
230 int placeHolderStart = lastMatchDashes.capturedStart(nth: 0);
231 int placeHolderLength = lastMatchDashes.capturedLength(nth: 0);
232
233 QString pattern(newName);
234
235 if (!validPlaceholder) {
236 if (allExtensionsDifferent) {
237 // pattern: my-file
238 // in: file-a.txt file-b.md
239 } else {
240 // pattern: my-file
241 // in: file-a.txt file-b.txt
242 // effective pattern: my-file#
243 placeHolderLength = 1;
244 placeHolderStart = pattern.length();
245 pattern.append(c: placeHolder);
246 }
247 }
248 bool allExtensionsDiff = allExtensionsDifferent;
249 bool valid = validPlaceholder;
250
251 index = indexSpinBox->value();
252 std::function<QString(const QStringView fileName)> function =
253 [pattern, allExtensionsDiff, valid, placeHolderStart, placeHolderLength, this](const QStringView fileName) {
254 Q_UNUSED(fileName);
255
256 QString indexString = QString::number(index);
257
258 if (!valid) {
259 if (allExtensionsDiff) {
260 // pattern: my-file
261 // in: file-a.txt file-b.md
262 return pattern;
263 }
264 }
265
266 // Insert leading zeros if necessary
267 indexString = indexString.prepend(s: QString(placeHolderLength - indexString.length(), QLatin1Char('0')));
268 ++index;
269
270 return QString(pattern).replace(i: placeHolderStart, len: placeHolderLength, after: indexString);
271 };
272 return function;
273 }
274
275 ValidationResult validate(const KFileItemList & /*items*/, const QStringView /* fileName */) override
276 {
277 const auto placeholder = placeHolderEdit->text();
278 if (placeholder.isEmpty()) {
279 return invalid(text: QString());
280 }
281 if (!validPlaceholder && !allExtensionsDifferent) {
282 return invalid(
283 i18nc("@info", "Invalid filename: The new name should contain one sequence of #, unless all the files have different file extensions."));
284 }
285 return ok();
286 }
287
288 bool validPlaceholder = false;
289 bool allExtensionsDifferent = true;
290 QLineEdit *placeHolderEdit;
291 QSpinBox *indexSpinBox;
292 int index;
293};
294
295class ReplaceStrategy : public RenameOperationAbstractStrategy
296{
297public:
298 ~ReplaceStrategy() override
299 {
300 }
301
302 QWidget *init(const KFileItemList &items, QWidget *parent, std::function<void()> &updateCallback) override
303 {
304 Q_UNUSED(items)
305
306 QWidget *widget = new QWidget(parent);
307 auto layout = new QVBoxLayout(widget);
308
309 auto renameLabel = new QLabel(
310 i18ncp("@label:textbox by: [Replacing: xx] [With: yy]", "Rename the %1 selected item by:", "Rename the %1 selected items by:", items.count()),
311 widget);
312 layout->addWidget(renameLabel);
313
314 auto patternLabel = new QLabel(i18nc("@info replace as in replacing [value] with [value]", "Replacing:"), widget);
315 patternLineEdit = new QLineEdit(widget);
316 patternLineEdit->setPlaceholderText(i18nc("@info placeholder text", "Pattern"));
317 patternLabel->setBuddy(patternLineEdit);
318 widget->setFocusProxy(patternLineEdit);
319
320 auto replacementLabel = new QLabel(i18nc("@info with as in replacing [value] with [value]", "With:"), widget);
321 replacementEdit = new QLineEdit(widget);
322 replacementEdit->setPlaceholderText(i18nc("@info placeholder text", "Replacement"));
323 replacementLabel->setBuddy(replacementEdit);
324
325 QObject::connect(sender: patternLineEdit, signal: &QLineEdit::textChanged, slot&: updateCallback);
326 QObject::connect(sender: replacementEdit, signal: &QLineEdit::textChanged, slot&: updateCallback);
327
328 auto replaceLayout = new QHBoxLayout();
329 replaceLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
330
331 replaceLayout->addWidget(patternLabel);
332 replaceLayout->addWidget(patternLineEdit);
333 replaceLayout->addWidget(replacementLabel);
334 replaceLayout->addWidget(replacementEdit);
335
336 layout->addLayout(layout: replaceLayout);
337
338 return widget;
339 }
340
341 const std::function<QString(const QStringView fileName)> renameFunction() override
342 {
343 const auto pattern = patternLineEdit->text();
344 const auto replacement = replacementEdit->text();
345 std::function<QString(const QStringView fileName)> renameFunction = [pattern, replacement](const QStringView fileName) {
346 auto output = fileName.toString();
347 if (pattern.isEmpty()) {
348 return output;
349 }
350 output.replace(before: pattern, after: replacement);
351 while (output.startsWith(c: QLatin1Char(' '))) {
352 output = output.mid(position: 1);
353 }
354 return output;
355 };
356 return renameFunction;
357 }
358
359 ValidationResult validate(const KFileItemList &items, const QStringView /* fileName */) override
360 {
361 const auto pattern = patternLineEdit->text();
362 if (pattern.isEmpty()) {
363 return invalid(text: QString());
364 }
365 auto any_match = std::any_of(first: items.cbegin(), last: items.cend(), pred: [pattern](const KFileItem &item) {
366 return item.url().fileName().contains(s: pattern);
367 });
368 if (!any_match) {
369 return invalid(i18nc("@info pattern as in text replacement pattern", "No file name contains the pattern."));
370 }
371 const auto replacement = replacementEdit->text();
372 if (replacement.isEmpty()) {
373 auto it = std::find_if(first: items.cbegin(), last: items.cend(), pred: [pattern](const KFileItem &item) {
374 return item.url().fileName() == pattern;
375 });
376 if (it != items.cend()) {
377 return invalid(xi18nc("@info pattern as in text replacement pattern",
378 "Replacing “%1” with an empty replacement would cause <filename>%2</filename> to have an empty file name.",
379 pattern,
380 it->url().fileName()));
381 }
382 }
383 return ok();
384 }
385
386 QLineEdit *patternLineEdit;
387 QLineEdit *replacementEdit;
388};
389
390class AddTextStrategy : public RenameOperationAbstractStrategy
391{
392public:
393 ~AddTextStrategy() override
394 {
395 }
396
397 QWidget *init(const KFileItemList &items, QWidget *parent, std::function<void()> &updateCallback) override
398 {
399 Q_UNUSED(items)
400
401 QWidget *widget = new QWidget(parent);
402 auto layout = new QVBoxLayout(widget);
403
404 auto renameLabel = new QLabel(i18ncp("@label:textbox", "Rename the %1 selected item:", "Rename the %1 selected items:", items.count()), widget);
405 layout->addWidget(renameLabel);
406
407 auto textLabel = new QLabel(i18nc("@label:textbox add text to a filename", "Add Text:"), widget);
408 textLineEdit = new QLineEdit(widget);
409 textLineEdit->setPlaceholderText(i18nc("@info:placeholder", "Text to add"));
410 textLabel->setBuddy(textLineEdit);
411 widget->setFocusProxy(textLineEdit);
412
413 beforeAfterCombo = new QComboBox(widget);
414 beforeAfterCombo->addItems(texts: {i18nc("@item:inlistbox as in insert text before filename", "Before filename"),
415 i18nc("@item:inlistbox as in insert text after filename", "After filename")});
416
417 QObject::connect(sender: textLineEdit, signal: &QLineEdit::textChanged, slot&: updateCallback);
418 QObject::connect(sender: beforeAfterCombo, signal: &QComboBox::currentIndexChanged, slot&: updateCallback);
419
420 auto addTextLayout = new QHBoxLayout();
421 addTextLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
422
423 addTextLayout->addWidget(textLabel);
424 addTextLayout->addWidget(textLineEdit);
425 addTextLayout->addWidget(beforeAfterCombo);
426
427 layout->addLayout(layout: addTextLayout);
428
429 return widget;
430 }
431
432 const std::function<QString(const QStringView fileName)> renameFunction() override
433 {
434 const auto textToAdd = textLineEdit->text();
435 const auto append = beforeAfterCombo->currentIndex() == 1;
436 std::function<QString(const QStringView fileName)> renameFunction = [textToAdd, append](const QStringView fileName) {
437 QString output = fileName.toString();
438 if (textToAdd.isEmpty()) {
439 return output;
440 }
441
442 QMimeDatabase db;
443 const QString extension = db.suffixForFileName(fileName: output);
444
445 if (!extension.isEmpty()) {
446 output = output.chopped(n: extension.length() + 1);
447 }
448 if (append) {
449 output = output + textToAdd;
450 } else {
451 // prepend
452 output = textToAdd + output;
453 }
454 if (!extension.isEmpty()) {
455 output += QLatin1Char('.') + extension;
456 }
457 return output;
458 };
459 return renameFunction;
460 }
461
462 ValidationResult validate(const KFileItemList &items, const QStringView /* fileName */) override
463 {
464 const auto prefix = textLineEdit->text();
465 if (prefix.isEmpty()) {
466 return invalid(text: QString());
467 }
468 const auto rename = renameFunction();
469 QUrl newUrl;
470
471 auto it = std::find_if(first: items.cbegin(), last: items.cend(), pred: [&rename, &newUrl](const KFileItem &item) {
472 bool fileExists = false;
473 auto oldUrl = item.url();
474 newUrl = oldUrl.adjusted(options: QUrl::RemoveFilename);
475 newUrl.setPath(path: newUrl.path() + KIO::encodeFileName(str: rename(item.url().fileName())));
476 if (oldUrl.isLocalFile() && newUrl != oldUrl) {
477 fileExists = QFile::exists(fileName: newUrl.toLocalFile());
478 }
479
480 return fileExists;
481 });
482 if (it != items.cend()) {
483 return invalid(xi18nc("@info error a file already exists", "A file named <filename>%1</filename> already exists.", newUrl.fileName()));
484 }
485 return ok();
486 }
487
488 QLineEdit *textLineEdit;
489 QLineEdit *appendEdit;
490 QComboBox *beforeAfterCombo;
491};
492}
493
494namespace KIO
495{
496
497class Q_DECL_HIDDEN RenameFileDialog::RenameFileDialogPrivate
498{
499public:
500 RenameFileDialogPrivate(const KFileItemList &items)
501 : items(items)
502 , renameOneItem(false)
503 , allExtensionsDifferent(true)
504 {
505 }
506
507 QList<QUrl> renamedItems;
508 KFileItemList items;
509 QPushButton *okButton;
510
511 KMessageWidget *messageWidget;
512 QLabel *previewLabel;
513 QLineEdit *preview;
514
515 bool renameOneItem;
516 bool allExtensionsDifferent;
517
518 QComboBox *comboRenameType;
519 QVBoxLayout *m_topLayout;
520 QWidget *m_contentWidget;
521
522 std::unique_ptr<RenameOperationAbstractStrategy> renameStrategy;
523};
524
525RenameFileDialog::RenameFileDialog(const KFileItemList &items, QWidget *parent)
526 : QDialog(parent)
527 , d(new RenameFileDialogPrivate(items))
528{
529 Q_ASSERT(items.count() >= 1);
530 d->renameOneItem = items.count() == 1;
531
532 setWindowTitle(d->renameOneItem ? i18nc("@title:window", "Rename Item") : i18nc("@title:window", "Rename Items"));
533 QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
534 QVBoxLayout *mainLayout = new QVBoxLayout(this);
535 d->okButton = buttonBox->button(which: QDialogButtonBox::Ok);
536 d->okButton->setDefault(true);
537 d->okButton->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Return));
538 connect(sender: buttonBox, signal: &QDialogButtonBox::accepted, context: this, slot: &RenameFileDialog::slotAccepted);
539 connect(sender: buttonBox, signal: &QDialogButtonBox::rejected, context: this, slot: &RenameFileDialog::reject);
540 connect(sender: buttonBox, signal: &QDialogButtonBox::rejected, context: this, slot: &QObject::deleteLater);
541 d->okButton->setDefault(true);
542
543 KGuiItem::assign(button: d->okButton, item: KGuiItem(i18nc("@action:button", "&Rename"), QStringLiteral("dialog-ok-apply")));
544
545 QWidget *page = new QWidget(this);
546 mainLayout->addWidget(page);
547 mainLayout->addWidget(buttonBox);
548
549 d->m_topLayout = new QVBoxLayout(page);
550
551 if (!d->renameOneItem) {
552 QLabel *renameTypeChoiceLabel = new QLabel(i18nc("@info", "How to rename:"), page);
553 d->comboRenameType = new QComboBox(page);
554 d->comboRenameType->addItems(
555 texts: {i18nc("@info renaming operation", "Enumerate"), i18nc("@info renaming operation", "Replace text"), i18nc("@info renaming operation", "Add text")});
556 renameTypeChoiceLabel->setBuddy(d->comboRenameType);
557
558 QHBoxLayout *renameTypeChoice = new QHBoxLayout;
559 renameTypeChoice->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
560
561 renameTypeChoice->addWidget(renameTypeChoiceLabel);
562 renameTypeChoice->addWidget(d->comboRenameType);
563 d->m_topLayout->addLayout(layout: renameTypeChoice);
564
565 connect(sender: d->comboRenameType, signal: &QComboBox::currentIndexChanged, context: this, slot: &RenameFileDialog::slotOperationChanged);
566
567 d->previewLabel = new QLabel(i18nc("@info As in filename renaming preview", "Preview:"), page);
568 d->preview = new QLineEdit(page);
569 d->preview->setReadOnly(true);
570 d->previewLabel->setBuddy(d->preview);
571 }
572
573 d->m_contentWidget = new QWidget();
574 d->m_topLayout->addWidget(d->m_contentWidget);
575
576 d->messageWidget = new KMessageWidget(page);
577 d->messageWidget->setCloseButtonVisible(false);
578 d->messageWidget->setWordWrap(true);
579 d->m_topLayout->addWidget(d->messageWidget);
580
581 if (!d->renameOneItem) {
582 d->m_topLayout->addWidget(d->previewLabel, stretch: Qt::AlignBottom);
583 d->m_topLayout->addWidget(d->preview, stretch: Qt::AlignBottom);
584 }
585
586 // initialize UI
587 slotOperationChanged(index: RenameStrategy::Enumerate);
588
589 setFixedWidth(sizeHint().width());
590}
591
592RenameFileDialog::~RenameFileDialog()
593{
594}
595
596void RenameFileDialog::slotAccepted()
597{
598 QWidget *widget = parentWidget();
599 if (!widget) {
600 widget = this;
601 }
602
603 const QList<QUrl> srcList = d->items.urlList();
604 d->renamedItems.reserve(asize: d->items.count());
605
606 KIO::FileUndoManager::CommandType cmdType;
607 KIO::Job *job = nullptr;
608
609 if (d->renameOneItem) {
610 Q_ASSERT(d->items.count() == 1);
611 cmdType = KIO::FileUndoManager::Rename;
612 const QUrl oldUrl = d->items.constFirst().url();
613 QUrl newUrl = oldUrl.adjusted(options: QUrl::RemoveFilename);
614 newUrl.setPath(path: newUrl.path() + KIO::encodeFileName(str: d->renameStrategy->renameFunction()(oldUrl.fileName())));
615
616 job = KIO::moveAs(src: oldUrl, dest: newUrl, flags: KIO::HideProgressInfo);
617 connect(sender: qobject_cast<KIO::CopyJob *>(object: job),
618 signal: &KIO::CopyJob::copyingDone,
619 context: this,
620 slot: [this](KIO::Job * /* job */, const QUrl &from, const QUrl &to, const QDateTime & /*mtime*/, bool /*directory*/, bool /*renamed*/) {
621 slotFileRenamed(oldUrl: from, newUrl: to);
622 });
623 } else {
624 cmdType = KIO::FileUndoManager::BatchRename;
625
626 job = KIO::batchRenameWithFunction(srcList, renameFunction: d->renameStrategy->renameFunction());
627 connect(sender: qobject_cast<KIO::BatchRenameJob *>(object: job), signal: &KIO::BatchRenameJob::fileRenamed, context: this, slot: &RenameFileDialog::slotFileRenamed);
628 }
629
630 KJobWidgets::setWindow(job, widget);
631 const QUrl parentUrl = srcList.first().adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
632 KIO::FileUndoManager::self()->recordJob(op: cmdType, src: srcList, dst: parentUrl, job);
633
634 connect(sender: job, signal: &KJob::result, context: this, slot: &RenameFileDialog::slotResult);
635
636 accept();
637}
638
639void RenameFileDialog::slotOperationChanged(int index)
640{
641 setUpdatesEnabled(false);
642
643 if (d->renameOneItem) {
644 d->renameStrategy.reset(p: new SingleFileRenameStrategy());
645 } else {
646 if (index == RenameStrategy::Enumerate) {
647 d->renameStrategy.reset(p: new EnumerateStrategy());
648 } else if (index == RenameStrategy::Replace) {
649 d->renameStrategy.reset(p: new ReplaceStrategy());
650 } else if (index == RenameStrategy::AddText) {
651 d->renameStrategy.reset(p: new AddTextStrategy());
652 }
653 }
654
655 std::function<void()> updateCallback = std::bind(f: &RenameFileDialog::slotStateChanged, args: this);
656
657 auto newWidget = d->renameStrategy->init(items: d->items, parent: this, updateCallback);
658 d->m_topLayout->replaceWidget(from: d->m_contentWidget, to: newWidget);
659 newWidget->setFocus();
660 newWidget->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
661
662 delete d->m_contentWidget;
663 d->m_contentWidget = newWidget;
664
665 if (!d->renameOneItem) {
666 setTabOrder(d->comboRenameType, d->m_contentWidget);
667 setTabOrder(d->m_contentWidget, d->preview);
668 }
669
670 setUpdatesEnabled(true);
671
672 slotStateChanged();
673}
674
675void RenameFileDialog::slotStateChanged()
676{
677 const auto firstItem = d->items.first();
678 auto previewText = d->renameStrategy->renameFunction()(firstItem.url().fileName());
679
680 const QString suffix = QLatin1Char('.') + firstItem.suffix();
681 if (!firstItem.suffix().isEmpty() && !previewText.endsWith(s: suffix) && !previewText.isEmpty() && previewText != suffix) {
682 previewText.append(s: suffix);
683 }
684
685 if (!d->renameOneItem) {
686 d->preview->setText(previewText);
687 d->preview->setAccessibleName(previewText);
688 }
689 ValidationResult validationResult;
690 if (previewText.isEmpty()) {
691 validationResult = invalid(xi18nc("@info", "<filename>%1</filename> cannot be renamed to an empty file name.", firstItem.name()));
692 } else {
693 validationResult = d->renameStrategy->validate(items: d->items, fileName: previewText);
694 }
695 d->okButton->setEnabled(validationResult.result == Result::Ok);
696 if (validationResult.result == Result::Ok || validationResult.text.isEmpty()) {
697 d->messageWidget->hide();
698 QTimer::singleShot(interval: 0, receiver: this, slot: [this]() {
699 adjustSize();
700 });
701 } else {
702 d->messageWidget->setMessageType(validationResult.type);
703 d->messageWidget->setText(validationResult.text);
704 d->messageWidget->animatedShow();
705 }
706}
707
708void RenameFileDialog::slotFileRenamed(const QUrl &oldUrl, const QUrl &newUrl)
709{
710 Q_UNUSED(oldUrl)
711 d->renamedItems << newUrl;
712}
713
714void RenameFileDialog::slotResult(KJob *job)
715{
716 if (!job->error()) {
717 Q_EMIT renamingFinished(urls: d->renamedItems);
718 } else {
719 Q_EMIT error(error: job);
720 }
721}
722
723} // namespace KIO
724
725#include "moc_renamefiledialog.cpp"
726

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