diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -61,6 +61,7 @@ executablefileopendialog.cpp dndpopupmenuplugin.cpp kurifiltersearchprovideractions.cpp + renamefiledialog.cpp ) if (WIN32) list(APPEND kiowidgets_SRCS @@ -174,6 +175,7 @@ Paste PixmapLoader KUriFilterSearchProviderActions # KF6: fix and move to non-KIO prefixed install folder + RenameFileDialog PREFIX KIO REQUIRED_HEADERS KIO_namespaced_widgets_HEADERS diff --git a/src/widgets/renamefiledialog.h b/src/widgets/renamefiledialog.h new file mode 100644 --- /dev/null +++ b/src/widgets/renamefiledialog.h @@ -0,0 +1,79 @@ +/* This file is part of the KDE libraries + Copyright (C) 2006-2010 by Peter Penz (peter.penz@gmx.at) + Copyright (C) 2020 by Méven Car (meven.car@kdemail.net) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef RENAMEFILEDIALOG_H +#define RENAMEFILEDIALOG_H + +#include + +#include "kiowidgets_export.h" + +#include +#include + +class QLineEdit; +class QSpinBox; +class QPushButton; +class KJob; + +namespace KIO { +class RenameFileDialogPrivate; + +/** + * @class KIO::RenameFileDialog renamefiledialog.h + * + * @brief Dialog for renaming a variable number of files. + * + * The dialog deletes itself when accepted or rejected. + * + * @since 5.67 + */ +// TODO KF6 : rename the class RenameFileDialog to RenameDialog and the class RenameDialog to RenameFileOverwrittenDialog or similar. +class KIOWIDGETS_EXPORT RenameFileDialog : public QDialog +{ + Q_OBJECT + +public: + /** + * Contructs the Dialog to rename file(s) + * + * @param parent the parent QWidget + * @param items a non-empty list of items to rename + */ + explicit RenameFileDialog(const KFileItemList &items, QWidget *parent); + ~RenameFileDialog() override; + +Q_SIGNALS: + void renamingFinished(const QList &urls); + void error(KJob *error); + +private Q_SLOTS: + void slotAccepted(); + void slotTextChanged(const QString &newName); + void slotFileRenamed(const QUrl &oldUrl, const QUrl &newUrl); + void slotResult(KJob *job); + +private: + class RenameFileDialogPrivate; + RenameFileDialogPrivate *const d; +}; +} // namespace KIO + +#endif diff --git a/src/widgets/renamefiledialog.cpp b/src/widgets/renamefiledialog.cpp new file mode 100644 --- /dev/null +++ b/src/widgets/renamefiledialog.cpp @@ -0,0 +1,256 @@ +/* This file is part of the KDE libraries + Copyright (C) 2006-2010 by Peter Penz (peter.penz@gmx.at) + Copyright (C) 2020 by Méven Car (meven.car@kdemail.net) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "renamefiledialog.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KIO { +class Q_DECL_HIDDEN RenameFileDialog::RenameFileDialogPrivate +{ +public: + + RenameFileDialogPrivate(const KFileItemList &items) + : lineEdit(nullptr) + , items(items) + , spinBox(nullptr) + , renameOneItem(false) + , allExtensionsDifferent(true) + { + } + + QList renamedItems; + QLineEdit *lineEdit; + KFileItemList items; + QSpinBox *spinBox; + QPushButton *okButton; + bool renameOneItem; + bool allExtensionsDifferent; +}; + +RenameFileDialog::RenameFileDialog(const KFileItemList &items, QWidget *parent) + : QDialog(parent) + , d(new RenameFileDialogPrivate(items)) +{ + setMinimumWidth(320); + + const int itemCount = items.count(); + Q_ASSERT(itemCount >= 1); + d->renameOneItem = (itemCount == 1); + + setWindowTitle(d->renameOneItem + ? i18nc("@title:window", "Rename Item") + : i18nc("@title:window", "Rename Items")); + QDialogButtonBox *buttonBox = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + QVBoxLayout *mainLayout = new QVBoxLayout; + setLayout(mainLayout); + d->okButton = buttonBox->button(QDialogButtonBox::Ok); + d->okButton->setDefault(true); + d->okButton->setShortcut(Qt::CTRL + Qt::Key_Return); + connect(buttonBox, &QDialogButtonBox::accepted, this, &RenameFileDialog::slotAccepted); + connect(buttonBox, &QDialogButtonBox::rejected, this, &RenameFileDialog::reject); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QObject::deleteLater); + d->okButton->setDefault(true); + + KGuiItem::assign(d->okButton, + KGuiItem(i18nc("@action:button", "&Rename"), + QStringLiteral("dialog-ok-apply"))); + + QWidget *page = new QWidget(this); + mainLayout->addWidget(page); + mainLayout->addWidget(buttonBox); + + QVBoxLayout *topLayout = new QVBoxLayout(page); + + QLabel *editLabel = nullptr; + QString newName; + if (d->renameOneItem) { + newName = items.first().name(); + editLabel + = new QLabel(xi18nc("@label:textbox", "Rename the item %1 to:", + newName), + page); + editLabel->setTextFormat(Qt::PlainText); + } else { + newName = i18nc( + "This a template for new filenames, # is replaced by a number later, must be the end character", + "New name #"); + editLabel = new QLabel(i18ncp("@label:textbox", + "Rename the %1 selected item to:", + "Rename the %1 selected items to:", itemCount), + page); + } + + d->lineEdit = new QLineEdit(page); + mainLayout->addWidget(d->lineEdit); + connect(d->lineEdit, &QLineEdit::textChanged, this, &RenameFileDialog::slotTextChanged); + + int selectionLength = newName.length(); + if (d->renameOneItem) { + // If the current item is a directory, select the whole file name. + if (!items.first().isDir()) { + QMimeDatabase db; + const QString extension = db.suffixForFileName(items.first().name()); + if (extension.length() > 0) { + // Don't select the extension + selectionLength -= extension.length() + 1; + } + } + } else { + // Don't select the # character + --selectionLength; + } + + d->lineEdit->setText(newName); + d->lineEdit->setSelection(0, selectionLength); + + topLayout->addWidget(editLabel); + topLayout->addWidget(d->lineEdit); + + if (!d->renameOneItem) { + QMimeDatabase db; + QSet extensions; + for (const KFileItem &item : qAsConst(d->items)) { + const QString extension = db.suffixForFileName(item.name()); + + if (extensions.contains(extension)) { + d->allExtensionsDifferent = false; + break; + } + + extensions.insert(extension); + } + + QLabel *infoLabel + = new QLabel(i18nc("@info", + "# will be replaced by ascending numbers starting with:"), page); + mainLayout->addWidget(infoLabel); + d->spinBox = new QSpinBox(page); + d->spinBox->setMaximum(10000); + d->spinBox->setMinimum(0); + d->spinBox->setSingleStep(1); + d->spinBox->setValue(1); + d->spinBox->setDisplayIntegerBase(10); + + QHBoxLayout *horizontalLayout = new QHBoxLayout(page); + horizontalLayout->setMargin(0); + horizontalLayout->addWidget(infoLabel); + horizontalLayout->addWidget(d->spinBox); + + topLayout->addLayout(horizontalLayout); + } + + d->lineEdit->setFocus(); +} + +RenameFileDialog::~RenameFileDialog() +{ +} + +void RenameFileDialog::slotAccepted() +{ + QWidget *widget = parentWidget(); + if (!widget) { + widget = this; + } + + const QList srcList = d->items.urlList(); + const QString newName = d->lineEdit->text(); + KIO::FileUndoManager::CommandType cmdType; + KIO::Job *job = nullptr; + if (d->renameOneItem) { + Q_ASSERT(d->items.count() == 1); + cmdType = KIO::FileUndoManager::Rename; + const QUrl oldUrl = d->items.constFirst().url(); + QUrl newUrl = oldUrl.adjusted(QUrl::RemoveFilename); + newUrl.setPath(newUrl.path() + KIO::encodeFileName(newName)); + d->renamedItems << newUrl; + job = KIO::moveAs(oldUrl, newUrl, KIO::HideProgressInfo); + } else { + d->renamedItems.reserve(d->items.count()); + cmdType = KIO::FileUndoManager::BatchRename; + job = KIO::batchRename(srcList, newName, d->spinBox->value(), QLatin1Char('#')); + connect(qobject_cast( + job), &KIO::BatchRenameJob::fileRenamed, this, + &RenameFileDialog::slotFileRenamed); + } + + KJobWidgets::setWindow(job, widget); + const QUrl parentUrl + = srcList.first().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); + KIO::FileUndoManager::self()->recordJob(cmdType, srcList, parentUrl, job); + + connect(job, &KJob::result, this, &RenameFileDialog::slotResult); + connect(job, &KJob::result, this, &QObject::deleteLater); + + accept(); +} + +void RenameFileDialog::slotTextChanged(const QString &newName) +{ + bool enable = !newName.isEmpty() && (newName != QLatin1String("..")) + && (newName != QLatin1String(".")); + if (enable && !d->renameOneItem) { + const int count = newName.count(QLatin1Char('#')); + if (count == 0) { + // Renaming multiple files without '#' will only work if all extensions are different. + enable = d->allExtensionsDifferent; + } else { + // Ensure that the new name contains exactly one # (or a connected sequence of #'s) + const int first = newName.indexOf(QLatin1Char('#')); + const int last = newName.lastIndexOf(QLatin1Char('#')); + enable = (last - first + 1 == count); + } + } + d->okButton->setEnabled(enable); +} + +void RenameFileDialog::slotFileRenamed(const QUrl &oldUrl, const QUrl &newUrl) +{ + Q_UNUSED(oldUrl) + d->renamedItems << newUrl; +} + +void RenameFileDialog::slotResult(KJob *job) +{ + if (!job->error()) { + emit renamingFinished(d->renamedItems); + } else { + emit error(job); + } +} + +} // namespace KIO