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