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 | |
19 | #include <QDialogButtonBox> |
20 | #include <QHBoxLayout> |
21 | #include <QLabel> |
22 | #include <QLineEdit> |
23 | #include <QMimeDatabase> |
24 | #include <QPushButton> |
25 | #include <QShowEvent> |
26 | #include <QSpinBox> |
27 | |
28 | namespace KIO |
29 | { |
30 | class Q_DECL_HIDDEN RenameFileDialog::RenameFileDialogPrivate |
31 | { |
32 | public: |
33 | RenameFileDialogPrivate(const KFileItemList &items) |
34 | : lineEdit(nullptr) |
35 | , items(items) |
36 | , spinBox(nullptr) |
37 | , renameOneItem(false) |
38 | , allExtensionsDifferent(true) |
39 | { |
40 | } |
41 | |
42 | QList<QUrl> renamedItems; |
43 | QLineEdit *lineEdit; |
44 | KFileItemList items; |
45 | QSpinBox *spinBox; |
46 | QPushButton *okButton; |
47 | bool renameOneItem; |
48 | bool allExtensionsDifferent; |
49 | }; |
50 | |
51 | RenameFileDialog::RenameFileDialog(const KFileItemList &items, QWidget *parent) |
52 | : QDialog(parent) |
53 | , d(new RenameFileDialogPrivate(items)) |
54 | { |
55 | setMinimumWidth(320); |
56 | |
57 | const int itemCount = items.count(); |
58 | Q_ASSERT(itemCount >= 1); |
59 | d->renameOneItem = (itemCount == 1); |
60 | |
61 | setWindowTitle(d->renameOneItem ? i18nc("@title:window" , "Rename Item" ) : i18nc("@title:window" , "Rename Items" )); |
62 | QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); |
63 | QVBoxLayout *mainLayout = new QVBoxLayout(this); |
64 | d->okButton = buttonBox->button(which: QDialogButtonBox::Ok); |
65 | d->okButton->setDefault(true); |
66 | d->okButton->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Return)); |
67 | connect(sender: buttonBox, signal: &QDialogButtonBox::accepted, context: this, slot: &RenameFileDialog::slotAccepted); |
68 | connect(sender: buttonBox, signal: &QDialogButtonBox::rejected, context: this, slot: &RenameFileDialog::reject); |
69 | connect(sender: buttonBox, signal: &QDialogButtonBox::rejected, context: this, slot: &QObject::deleteLater); |
70 | d->okButton->setDefault(true); |
71 | |
72 | KGuiItem::assign(button: d->okButton, item: KGuiItem(i18nc("@action:button" , "&Rename" ), QStringLiteral("dialog-ok-apply" ))); |
73 | |
74 | QWidget *page = new QWidget(this); |
75 | mainLayout->addWidget(page); |
76 | mainLayout->addWidget(buttonBox); |
77 | |
78 | QVBoxLayout *topLayout = new QVBoxLayout(page); |
79 | |
80 | QLabel *editLabel = nullptr; |
81 | QString newName; |
82 | if (d->renameOneItem) { |
83 | newName = items.first().name(); |
84 | editLabel = new QLabel(xi18nc("@label:textbox" , "Rename the item <filename>%1</filename> to:" , newName), page); |
85 | editLabel->setTextFormat(Qt::PlainText); |
86 | } else { |
87 | newName = i18nc("This a template for new filenames, # is replaced by a number later, must be the end character" , "New name #" ); |
88 | editLabel = new QLabel(i18ncp("@label:textbox" , "Rename the %1 selected item to:" , "Rename the %1 selected items to:" , itemCount), page); |
89 | } |
90 | |
91 | d->lineEdit = new QLineEdit(page); |
92 | mainLayout->addWidget(d->lineEdit); |
93 | connect(sender: d->lineEdit, signal: &QLineEdit::textChanged, context: this, slot: &RenameFileDialog::slotTextChanged); |
94 | |
95 | int selectionLength = newName.length(); |
96 | if (d->renameOneItem) { |
97 | // If the current item is a directory, select the whole file name. |
98 | if (!items.first().isDir()) { |
99 | QMimeDatabase db; |
100 | const QString extension = db.suffixForFileName(fileName: items.first().name()); |
101 | if (extension.length() > 0) { |
102 | // Don't select the extension |
103 | selectionLength -= extension.length() + 1; |
104 | } |
105 | } |
106 | } else { |
107 | // Don't select the # character |
108 | --selectionLength; |
109 | } |
110 | |
111 | d->lineEdit->setText(newName); |
112 | d->lineEdit->setSelection(0, selectionLength); |
113 | |
114 | topLayout->addWidget(editLabel); |
115 | topLayout->addWidget(d->lineEdit); |
116 | |
117 | if (!d->renameOneItem) { |
118 | QMimeDatabase db; |
119 | QSet<QString> extensions; |
120 | for (const KFileItem &item : std::as_const(t&: d->items)) { |
121 | const QString extension = db.suffixForFileName(fileName: item.name()); |
122 | |
123 | if (extensions.contains(value: extension)) { |
124 | d->allExtensionsDifferent = false; |
125 | break; |
126 | } |
127 | |
128 | extensions.insert(value: extension); |
129 | } |
130 | |
131 | QLabel *infoLabel = new QLabel(i18nc("@info" , "# will be replaced by ascending numbers starting with:" ), page); |
132 | mainLayout->addWidget(infoLabel); |
133 | d->spinBox = new QSpinBox(page); |
134 | d->spinBox->setMinimum(0); |
135 | d->spinBox->setMaximum(1'000'000'000); |
136 | d->spinBox->setSingleStep(1); |
137 | d->spinBox->setValue(1); |
138 | d->spinBox->setDisplayIntegerBase(10); |
139 | |
140 | QHBoxLayout *horizontalLayout = new QHBoxLayout; |
141 | horizontalLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0); |
142 | horizontalLayout->addWidget(infoLabel); |
143 | horizontalLayout->addWidget(d->spinBox); |
144 | |
145 | topLayout->addLayout(layout: horizontalLayout); |
146 | } |
147 | |
148 | d->lineEdit->setFocus(); |
149 | } |
150 | |
151 | RenameFileDialog::~RenameFileDialog() |
152 | { |
153 | } |
154 | |
155 | void RenameFileDialog::slotAccepted() |
156 | { |
157 | QWidget *widget = parentWidget(); |
158 | if (!widget) { |
159 | widget = this; |
160 | } |
161 | |
162 | const QList<QUrl> srcList = d->items.urlList(); |
163 | const QString newName = d->lineEdit->text(); |
164 | KIO::FileUndoManager::CommandType cmdType; |
165 | KIO::Job *job = nullptr; |
166 | if (d->renameOneItem) { |
167 | Q_ASSERT(d->items.count() == 1); |
168 | cmdType = KIO::FileUndoManager::Rename; |
169 | const QUrl oldUrl = d->items.constFirst().url(); |
170 | QUrl newUrl = oldUrl.adjusted(options: QUrl::RemoveFilename); |
171 | newUrl.setPath(path: newUrl.path() + KIO::encodeFileName(str: newName)); |
172 | d->renamedItems << newUrl; |
173 | job = KIO::moveAs(src: oldUrl, dest: newUrl, flags: KIO::HideProgressInfo); |
174 | } else { |
175 | d->renamedItems.reserve(asize: d->items.count()); |
176 | cmdType = KIO::FileUndoManager::BatchRename; |
177 | job = KIO::batchRename(src: srcList, newName, index: d->spinBox->value(), placeHolder: QLatin1Char('#')); |
178 | connect(sender: qobject_cast<KIO::BatchRenameJob *>(object: job), signal: &KIO::BatchRenameJob::fileRenamed, context: this, slot: &RenameFileDialog::slotFileRenamed); |
179 | } |
180 | |
181 | KJobWidgets::setWindow(job, widget); |
182 | const QUrl parentUrl = srcList.first().adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash); |
183 | KIO::FileUndoManager::self()->recordJob(op: cmdType, src: srcList, dst: parentUrl, job); |
184 | |
185 | connect(sender: job, signal: &KJob::result, context: this, slot: &RenameFileDialog::slotResult); |
186 | connect(sender: job, signal: &KJob::result, context: this, slot: &QObject::deleteLater); |
187 | |
188 | accept(); |
189 | } |
190 | |
191 | void RenameFileDialog::slotTextChanged(const QString &newName) |
192 | { |
193 | bool enable = !newName.isEmpty() && (newName != QLatin1String(".." )) && (newName != QLatin1String("." )); |
194 | if (enable && !d->renameOneItem) { |
195 | const int count = newName.count(c: QLatin1Char('#')); |
196 | if (count == 0) { |
197 | // Renaming multiple files without '#' will only work if all extensions are different. |
198 | enable = d->allExtensionsDifferent; |
199 | } else { |
200 | // Ensure that the new name contains exactly one # (or a connected sequence of #'s) |
201 | const int first = newName.indexOf(c: QLatin1Char('#')); |
202 | const int last = newName.lastIndexOf(c: QLatin1Char('#')); |
203 | enable = (last - first + 1 == count); |
204 | } |
205 | } |
206 | d->okButton->setEnabled(enable); |
207 | } |
208 | |
209 | void RenameFileDialog::slotFileRenamed(const QUrl &oldUrl, const QUrl &newUrl) |
210 | { |
211 | Q_UNUSED(oldUrl) |
212 | d->renamedItems << newUrl; |
213 | } |
214 | |
215 | void RenameFileDialog::slotResult(KJob *job) |
216 | { |
217 | if (!job->error()) { |
218 | Q_EMIT renamingFinished(urls: d->renamedItems); |
219 | } else { |
220 | Q_EMIT error(error: job); |
221 | } |
222 | } |
223 | |
224 | } // namespace KIO |
225 | |
226 | #include "moc_renamefiledialog.cpp" |
227 | |