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 | |
33 | namespace |
34 | { |
35 | |
36 | enum Result { |
37 | Ok, |
38 | Invalid |
39 | }; |
40 | |
41 | // TODO c++23 port to std::expected |
42 | struct ValidationResult { |
43 | Result result; |
44 | QString text; |
45 | KMessageWidget::MessageType type; |
46 | }; |
47 | inline ValidationResult ok() |
48 | { |
49 | return ValidationResult{.result: Result::Ok, .text: QString(), .type: KMessageWidget::MessageType::Information}; |
50 | }; |
51 | inline ValidationResult invalid(const QString &text) |
52 | { |
53 | return ValidationResult{.result: Result::Invalid, .text: text, .type: KMessageWidget::MessageType::Error}; |
54 | }; |
55 | |
56 | /// design pattern strategy |
57 | class RenameOperationAbstractStrategy |
58 | { |
59 | public: |
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 | |
68 | enum RenameStrategy { |
69 | // SingleFileRename |
70 | Enumerate, |
71 | Replace, |
72 | AddText, |
73 | // Regex |
74 | }; |
75 | |
76 | class SingleFileRenameStrategy : public RenameOperationAbstractStrategy |
77 | { |
78 | public: |
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 | |
152 | class EnumerateStrategy : public RenameOperationAbstractStrategy |
153 | { |
154 | public: |
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 | |
295 | class ReplaceStrategy : public RenameOperationAbstractStrategy |
296 | { |
297 | public: |
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 | |
390 | class AddTextStrategy : public RenameOperationAbstractStrategy |
391 | { |
392 | public: |
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 | |
494 | namespace KIO |
495 | { |
496 | |
497 | class Q_DECL_HIDDEN RenameFileDialog::RenameFileDialogPrivate |
498 | { |
499 | public: |
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 | |
525 | RenameFileDialog::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 | |
592 | RenameFileDialog::~RenameFileDialog() |
593 | { |
594 | } |
595 | |
596 | void 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 | |
639 | void 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 | |
675 | void 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 | |
708 | void RenameFileDialog::slotFileRenamed(const QUrl &oldUrl, const QUrl &newUrl) |
709 | { |
710 | Q_UNUSED(oldUrl) |
711 | d->renamedItems << newUrl; |
712 | } |
713 | |
714 | void 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 | |