1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "cleandialog.h"
27 #include "ui_cleandialog.h"
28 #include "vcsoutputwindow.h"
29
30 #include <coreplugin/editormanager/editormanager.h>
31 #include <coreplugin/progressmanager/progressmanager.h>
32 #include <utils/runextensions.h>
33
34 #include <QStandardItemModel>
35 #include <QMessageBox>
36 #include <QApplication>
37 #include <QStyle>
38 #include <QIcon>
39
40 #include <QDir>
41 #include <QFile>
42 #include <QFileInfo>
43 #include <QDebug>
44 #include <QDateTime>
45 #include <QTimer>
46
47 namespace VcsBase {
48 namespace Internal {
49
50 enum { nameColumn, columnCount };
51 enum { fileNameRole = Qt::UserRole, isDirectoryRole = Qt::UserRole + 1 };
52
53 // Helper for recursively removing files.
removeFileRecursion(QFutureInterface<void> & futureInterface,const QFileInfo & f,QString * errorMessage)54 static void removeFileRecursion(QFutureInterface<void> &futureInterface,
55 const QFileInfo &f, QString *errorMessage)
56 {
57 if (futureInterface.isCanceled())
58 return;
59 // The version control system might list files/directory in arbitrary
60 // order, causing files to be removed from parent directories.
61 if (!f.exists())
62 return;
63 if (f.isDir()) {
64 const QDir dir(f.absoluteFilePath());
65 foreach (const QFileInfo &fi, dir.entryInfoList(QDir::AllEntries|QDir::NoDotAndDotDot|QDir::Hidden))
66 removeFileRecursion(futureInterface, fi, errorMessage);
67 QDir parent = f.absoluteDir();
68 if (!parent.rmdir(f.fileName()))
69 errorMessage->append(VcsBase::CleanDialog::tr("The directory %1 could not be deleted.").
70 arg(QDir::toNativeSeparators(f.absoluteFilePath())));
71 return;
72 }
73 if (!QFile::remove(f.absoluteFilePath())) {
74 if (!errorMessage->isEmpty())
75 errorMessage->append(QLatin1Char('\n'));
76 errorMessage->append(VcsBase::CleanDialog::tr("The file %1 could not be deleted.").
77 arg(QDir::toNativeSeparators(f.absoluteFilePath())));
78 }
79 }
80
81 // Cleaning files in the background
runCleanFiles(QFutureInterface<void> & futureInterface,const QString & repository,const QStringList & files,const std::function<void (const QString &)> & errorHandler)82 static void runCleanFiles(QFutureInterface<void> &futureInterface,
83 const QString &repository, const QStringList &files,
84 const std::function<void(const QString&)> &errorHandler)
85 {
86 QString errorMessage;
87 futureInterface.setProgressRange(0, files.size());
88 futureInterface.setProgressValue(0);
89 foreach (const QString &name, files) {
90 removeFileRecursion(futureInterface, QFileInfo(name), &errorMessage);
91 if (futureInterface.isCanceled())
92 break;
93 futureInterface.setProgressValue(futureInterface.progressValue() + 1);
94 }
95 if (!errorMessage.isEmpty()) {
96 // Format and emit error.
97 const QString msg = CleanDialog::tr("There were errors when cleaning the repository %1:").
98 arg(QDir::toNativeSeparators(repository));
99 errorMessage.insert(0, QLatin1Char('\n'));
100 errorMessage.insert(0, msg);
101 errorHandler(errorMessage);
102 }
103 }
104
handleError(const QString & errorMessage)105 static void handleError(const QString &errorMessage)
106 {
107 QTimer::singleShot(0, VcsOutputWindow::instance(), [errorMessage]() {
108 VcsOutputWindow::instance()->appendSilently(errorMessage);
109 });
110 }
111
112 // ---------------- CleanDialogPrivate ----------------
113
114 class CleanDialogPrivate
115 {
116 public:
117 CleanDialogPrivate();
118
119 Internal::Ui::CleanDialog ui;
120 QStandardItemModel *m_filesModel;
121 QString m_workingDirectory;
122 };
123
CleanDialogPrivate()124 CleanDialogPrivate::CleanDialogPrivate() :
125 m_filesModel(new QStandardItemModel(0, columnCount))
126 {
127 }
128
129 } // namespace Internal
130
131 /*!
132 \class VcsBase::CleanDialog
133
134 \brief The CleanDialog class provides a file selector dialog for files not
135 under version control.
136
137 Completely clean a directory under version control
138 from all files that are not under version control based on a list
139 generated from the version control system. Presents the user with
140 a checkable list of files and/or directories. Double click opens a file.
141 */
142
CleanDialog(QWidget * parent)143 CleanDialog::CleanDialog(QWidget *parent) :
144 QDialog(parent),
145 d(new Internal::CleanDialogPrivate)
146 {
147 setModal(true);
148
149 d->ui.setupUi(this);
150 d->ui.buttonBox->addButton(tr("Delete..."), QDialogButtonBox::AcceptRole);
151
152 d->m_filesModel->setHorizontalHeaderLabels(QStringList(tr("Name")));
153 d->ui.filesTreeView->setModel(d->m_filesModel);
154 d->ui.filesTreeView->setUniformRowHeights(true);
155 d->ui.filesTreeView->setSelectionMode(QAbstractItemView::NoSelection);
156 d->ui.filesTreeView->setAllColumnsShowFocus(true);
157 d->ui.filesTreeView->setRootIsDecorated(false);
158 connect(d->ui.filesTreeView, &QAbstractItemView::doubleClicked,
159 this, &CleanDialog::slotDoubleClicked);
160 connect(d->ui.selectAllCheckBox, &QAbstractButton::clicked,
161 this, &CleanDialog::selectAllItems);
162 connect(d->ui.filesTreeView, &QAbstractItemView::clicked,
163 this, &CleanDialog::updateSelectAllCheckBox);
164 }
165
~CleanDialog()166 CleanDialog::~CleanDialog()
167 {
168 delete d;
169 }
170
setFileList(const QString & workingDirectory,const QStringList & files,const QStringList & ignoredFiles)171 void CleanDialog::setFileList(const QString &workingDirectory, const QStringList &files,
172 const QStringList &ignoredFiles)
173 {
174 d->m_workingDirectory = workingDirectory;
175 d->ui.groupBox->setTitle(tr("Repository: %1").
176 arg(QDir::toNativeSeparators(workingDirectory)));
177 if (const int oldRowCount = d->m_filesModel->rowCount())
178 d->m_filesModel->removeRows(0, oldRowCount);
179
180 foreach (const QString &fileName, files)
181 addFile(workingDirectory, fileName, true);
182 foreach (const QString &fileName, ignoredFiles)
183 addFile(workingDirectory, fileName, false);
184
185 for (int c = 0; c < d->m_filesModel->columnCount(); c++)
186 d->ui.filesTreeView->resizeColumnToContents(c);
187
188 if (ignoredFiles.isEmpty())
189 d->ui.selectAllCheckBox->setChecked(true);
190 }
191
addFile(const QString & workingDirectory,QString fileName,bool checked)192 void CleanDialog::addFile(const QString &workingDirectory, QString fileName, bool checked)
193 {
194 QStyle *style = QApplication::style();
195 const QIcon folderIcon = style->standardIcon(QStyle::SP_DirIcon);
196 const QIcon fileIcon = style->standardIcon(QStyle::SP_FileIcon);
197 const QChar slash = QLatin1Char('/');
198 // Clean the trailing slash of directories
199 if (fileName.endsWith(slash))
200 fileName.chop(1);
201 QFileInfo fi(workingDirectory + slash + fileName);
202 bool isDir = fi.isDir();
203 if (isDir)
204 checked = false;
205 auto nameItem = new QStandardItem(QDir::toNativeSeparators(fileName));
206 nameItem->setFlags(Qt::ItemIsUserCheckable|Qt::ItemIsEnabled);
207 nameItem->setIcon(isDir ? folderIcon : fileIcon);
208 nameItem->setCheckable(true);
209 nameItem->setCheckState(checked ? Qt::Checked : Qt::Unchecked);
210 nameItem->setData(QVariant(fi.absoluteFilePath()), Internal::fileNameRole);
211 nameItem->setData(QVariant(isDir), Internal::isDirectoryRole);
212 // Tooltip with size information
213 if (fi.isFile()) {
214 const QString lastModified =
215 QLocale::system().toString(fi.lastModified(), QLocale::ShortFormat);
216 nameItem->setToolTip(tr("%n bytes, last modified %1.", nullptr,
217 fi.size()).arg(lastModified));
218 }
219 d->m_filesModel->appendRow(nameItem);
220 }
221
checkedFiles() const222 QStringList CleanDialog::checkedFiles() const
223 {
224 QStringList rc;
225 if (const int rowCount = d->m_filesModel->rowCount()) {
226 for (int r = 0; r < rowCount; r++) {
227 const QStandardItem *item = d->m_filesModel->item(r, 0);
228 if (item->checkState() == Qt::Checked)
229 rc.push_back(item->data(Internal::fileNameRole).toString());
230 }
231 }
232 return rc;
233 }
234
accept()235 void CleanDialog::accept()
236 {
237 if (promptToDelete())
238 QDialog::accept();
239 }
240
promptToDelete()241 bool CleanDialog::promptToDelete()
242 {
243 // Prompt the user and delete files
244 const QStringList selectedFiles = checkedFiles();
245 if (selectedFiles.isEmpty())
246 return true;
247
248 if (QMessageBox::question(this, tr("Delete"),
249 tr("Do you want to delete %n files?", nullptr, selectedFiles.size()),
250 QMessageBox::Yes|QMessageBox::No, QMessageBox::Yes) != QMessageBox::Yes)
251 return false;
252
253 // Remove in background
254 QFuture<void> task = Utils::runAsync(Internal::runCleanFiles, d->m_workingDirectory,
255 selectedFiles, Internal::handleError);
256
257 const QString taskName = tr("Cleaning \"%1\"").
258 arg(QDir::toNativeSeparators(d->m_workingDirectory));
259 Core::ProgressManager::addTask(task, taskName, "VcsBase.cleanRepository");
260 return true;
261 }
262
slotDoubleClicked(const QModelIndex & index)263 void CleanDialog::slotDoubleClicked(const QModelIndex &index)
264 {
265 // Open file on doubleclick
266 if (const QStandardItem *item = d->m_filesModel->itemFromIndex(index))
267 if (!item->data(Internal::isDirectoryRole).toBool()) {
268 const QString fname = item->data(Internal::fileNameRole).toString();
269 Core::EditorManager::openEditor(fname);
270 }
271 }
272
selectAllItems(bool checked)273 void CleanDialog::selectAllItems(bool checked)
274 {
275 if (const int rowCount = d->m_filesModel->rowCount()) {
276 for (int r = 0; r < rowCount; ++r) {
277 QStandardItem *item = d->m_filesModel->item(r, 0);
278 item->setCheckState(checked ? Qt::Checked : Qt::Unchecked);
279 }
280 }
281 }
282
updateSelectAllCheckBox()283 void CleanDialog::updateSelectAllCheckBox()
284 {
285 bool checked = true;
286 if (const int rowCount = d->m_filesModel->rowCount()) {
287 for (int r = 0; r < rowCount; ++r) {
288 const QStandardItem *item = d->m_filesModel->item(r, 0);
289 if (item->checkState() == Qt::Unchecked) {
290 checked = false;
291 break;
292 }
293 }
294 d->ui.selectAllCheckBox->setChecked(checked);
295 }
296 }
297
298 } // namespace VcsBase
299