1 /*
2  * SPDX-FileCopyrightText: 2011-2021 Laurent Montel <montel@kde.org>
3  *
4  * SPDX-License-Identifier: GPL-2.0-or-later
5  */
6 #include "renamefiledialog.h"
7 #include <KIO/StatJob>
8 #include <KJobWidgets>
9 #include <KLocalizedString>
10 #include <KMessageBox>
11 #include <KStandardGuiItem>
12 #include <QLineEdit>
13 #include <QPushButton>
14 #include <kseparator.h>
15 
16 #include <QCheckBox>
17 #include <QFileInfo>
18 #include <QHBoxLayout>
19 #include <QLabel>
20 #include <QVBoxLayout>
21 
22 using namespace PimCommon;
23 
24 class Q_DECL_HIDDEN PimCommon::RenameFileDialog::RenameFileDialogPrivate
25 {
26 public:
RenameFileDialogPrivate(const QUrl & _url,RenameFileDialog * qq)27     RenameFileDialogPrivate(const QUrl &_url, RenameFileDialog *qq)
28         : url(_url)
29         , q(qq)
30     {
31     }
32 
33     Q_REQUIRED_RESULT QString suggestName(const QUrl &baseURL, const QString &oldName);
34 
35     const QUrl url;
36     QCheckBox *applyAll = nullptr;
37     QPushButton *renameBtn = nullptr;
38     QPushButton *suggestNewNameBtn = nullptr;
39     QLineEdit *nameEdit = nullptr;
40     RenameFileDialog *const q;
41 };
42 
suggestName(const QUrl & baseURL,const QString & oldName)43 QString PimCommon::RenameFileDialog::RenameFileDialogPrivate::suggestName(const QUrl &baseURL, const QString &oldName)
44 {
45     QString dotSuffix;
46     QString suggestedName;
47     QString basename = oldName;
48     const QChar spacer(QLatin1Char(' '));
49 
50     // ignore dots at the beginning, that way "..aFile.tar.gz" will become "..aFile 1.tar.gz" instead of " 1..aFile.tar.gz"
51     int index = basename.indexOf(QLatin1Char('.'));
52     int continuous = 0;
53     while (continuous == index) {
54         index = basename.indexOf(QLatin1Char('.'), index + 1);
55         ++continuous;
56     }
57 
58     if (index != -1) {
59         dotSuffix = basename.mid(index);
60         basename.truncate(index);
61     }
62 
63     const int pos = basename.lastIndexOf(spacer);
64 
65     if (pos != -1) {
66         const QString tmp = basename.mid(pos + 1);
67         bool ok;
68         const int number = tmp.toInt(&ok);
69 
70         if (!ok) { // ok there is no number
71             suggestedName = basename + spacer + QLatin1Char('1') + dotSuffix;
72         } else {
73             // yes there's already a number behind the spacer so increment it by one
74             basename.replace(pos + 1, tmp.length(), QString::number(number + 1));
75             suggestedName = basename + dotSuffix;
76         }
77     } else { // no spacer yet
78         suggestedName = basename + spacer + QLatin1Char('1') + dotSuffix;
79     }
80 
81     // Check if suggested name already exists
82     bool exists = false;
83     // TODO: network transparency. However, using NetAccess from a modal dialog
84     // could be a problem, no? (given that it uses a modal widget itself....)
85     if (baseURL.isLocalFile()) {
86         exists = QFileInfo::exists(baseURL.toLocalFile() + QLatin1Char('/') + suggestedName);
87     }
88 
89     if (!exists) {
90         return suggestedName;
91     } else { // already exists -> recurse
92         return suggestName(baseURL, suggestedName);
93     }
94 }
95 
RenameFileDialog(const QUrl & url,bool multiFiles,QWidget * parent)96 RenameFileDialog::RenameFileDialog(const QUrl &url, bool multiFiles, QWidget *parent)
97     : QDialog(parent)
98     , d(new RenameFileDialogPrivate(url, this))
99 {
100     setWindowTitle(i18nc("@title:window", "File Already Exists"));
101     auto pLayout = new QVBoxLayout(this);
102 
103     auto label = new QLabel(xi18n("A file named <filename>%1</filename> already exists. Do you want to overwrite it?", url.fileName()), this);
104     pLayout->addWidget(label);
105 
106     auto renameLayout = new QHBoxLayout();
107     pLayout->addLayout(renameLayout);
108 
109     d->nameEdit = new QLineEdit(this);
110     renameLayout->addWidget(d->nameEdit);
111     d->nameEdit->setClearButtonEnabled(true);
112     d->nameEdit->setText(url.fileName());
113     d->suggestNewNameBtn = new QPushButton(i18n("Suggest New &Name"), this);
114     renameLayout->addWidget(d->suggestNewNameBtn);
115     connect(d->suggestNewNameBtn, &QPushButton::clicked, this, &RenameFileDialog::slotSuggestNewNamePressed);
116 
117     auto overWrite = new QPushButton(this);
118     KStandardGuiItem::assign(overWrite, KStandardGuiItem::Overwrite);
119     connect(overWrite, &QPushButton::clicked, this, &RenameFileDialog::slotOverwritePressed);
120 
121     auto ignore = new QPushButton(i18n("&Ignore"), this);
122     connect(ignore, &QPushButton::clicked, this, &RenameFileDialog::slotIgnorePressed);
123 
124     d->renameBtn = new QPushButton(i18n("&Rename"), this);
125     connect(d->renameBtn, &QPushButton::clicked, this, &RenameFileDialog::slotRenamePressed);
126 
127     auto separator = new KSeparator(this);
128     pLayout->addWidget(separator);
129 
130     auto layout = new QHBoxLayout();
131     pLayout->addLayout(layout);
132 
133     if (multiFiles) {
134         d->applyAll = new QCheckBox(i18n("Appl&y to All"), this);
135         connect(d->applyAll, &QCheckBox::clicked, this, &RenameFileDialog::slotApplyAllPressed);
136         layout->addWidget(d->applyAll);
137         slotApplyAllPressed();
138     }
139     layout->addWidget(d->renameBtn);
140     layout->addWidget(overWrite);
141     layout->addWidget(ignore);
142 }
143 
144 RenameFileDialog::~RenameFileDialog() = default;
145 
slotOverwritePressed()146 void RenameFileDialog::slotOverwritePressed()
147 {
148     if (d->applyAll && d->applyAll->isChecked()) {
149         done(RENAMEFILE_OVERWRITEALL);
150     } else {
151         done(RENAMEFILE_OVERWRITE);
152     }
153 }
154 
slotIgnorePressed()155 void RenameFileDialog::slotIgnorePressed()
156 {
157     if (d->applyAll && d->applyAll->isChecked()) {
158         done(RENAMEFILE_IGNOREALL);
159     } else {
160         done(RENAMEFILE_IGNORE);
161     }
162 }
163 
slotRenamePressed()164 void RenameFileDialog::slotRenamePressed()
165 {
166     if (d->nameEdit->text().isEmpty()) {
167         return;
168     }
169 
170     bool fileExists = false;
171     if (newName().isLocalFile()) {
172         fileExists = QFile::exists(newName().path());
173     } else {
174         auto job = KIO::statDetails(newName(), KIO::StatJob::DestinationSide, KIO::StatBasic);
175         KJobWidgets::setWindow(job, this);
176         fileExists = job->exec();
177     }
178 
179     if (fileExists) {
180         KMessageBox::error(this, i18n("This filename \"%1\" already exists.", newName().toDisplayString(QUrl::PreferLocalFile)), i18n("File already exists"));
181         return;
182     }
183     done(RENAMEFILE_RENAME);
184 }
185 
slotApplyAllPressed()186 void RenameFileDialog::slotApplyAllPressed()
187 {
188     const bool enabled(!d->applyAll->isChecked());
189     d->nameEdit->setEnabled(enabled);
190     d->suggestNewNameBtn->setEnabled(enabled);
191     d->renameBtn->setEnabled(enabled);
192 }
193 
slotSuggestNewNamePressed()194 void RenameFileDialog::slotSuggestNewNamePressed()
195 {
196     if (d->nameEdit->text().isEmpty()) {
197         return;
198     }
199 
200     auto destDirectory = d->url.adjusted(QUrl::RemoveFilename);
201     d->nameEdit->setText(d->suggestName(destDirectory, d->nameEdit->text()));
202 }
203 
newName() const204 QUrl RenameFileDialog::newName() const
205 {
206     const QString fileName = d->nameEdit->text();
207 
208     auto newDest = d->url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
209     newDest.setPath(newDest.path() + QLatin1Char('/') + KIO::encodeFileName(fileName));
210 
211     return newDest;
212 }
213