File indexing completed on 2024-04-28 05:46:31

0001 /*
0002     SPDX-FileCopyrightText: 2010 Volker Lanz <vl@fidra.de>
0003     SPDX-FileCopyrightText: 2014-2020 Andrius Štikonas <andrius@stikonas.eu>
0004 
0005     SPDX-License-Identifier: GPL-3.0-or-later
0006 */
0007 
0008 #include "gui/applyprogressdialog.h"
0009 
0010 #include "gui/applyprogressdialogwidget.h"
0011 #include "gui/applyprogressdetailswidget.h"
0012 
0013 #include <core/operationrunner.h>
0014 
0015 #include <ops/operation.h>
0016 
0017 #include <jobs/job.h>
0018 
0019 #include <util/report.h>
0020 #include <util/htmlreport.h>
0021 
0022 #include <QApplication>
0023 #include <QCloseEvent>
0024 #include <QDialogButtonBox>
0025 #include <QFont>
0026 #include <QFile>
0027 #include <QFileDialog>
0028 #include <QKeyEvent>
0029 #include <QPushButton>
0030 #include <QTemporaryFile>
0031 #include <QTextStream>
0032 
0033 #include <KAboutData>
0034 #include <KConfigGroup>
0035 #include <KIO/CopyJob>
0036 #include <KIO/OpenUrlJob>
0037 #include <KJobUiDelegate>
0038 #include <KLocalizedString>
0039 #include <KMessageBox>
0040 #include <KSharedConfig>
0041 
0042 const QString ApplyProgressDialog::m_TimeFormat = QStringLiteral("hh:mm:ss");
0043 
0044 static QWidget* mainWindow(QWidget* w)
0045 {
0046     while (w && w->parentWidget())
0047         w = w->parentWidget();
0048     return w;
0049 }
0050 
0051 /** Creates a new ProgressDialog
0052     @param parent pointer to the parent widget
0053     @param orunner the OperationRunner whose progress this dialog is showing
0054 */
0055 ApplyProgressDialog::ApplyProgressDialog(QWidget* parent, OperationRunner& orunner) :
0056     QDialog(parent),
0057     m_ProgressDialogWidget(new ApplyProgressDialogWidget(this)),
0058     m_ProgressDetailsWidget(new ApplyProgressDetailsWidget(this)),
0059     m_OperationRunner(orunner),
0060     m_Report(nullptr),
0061     m_SavedParentTitle(mainWindow(this)->windowTitle()),
0062     m_Timer(this),
0063     m_ElapsedTimer(),
0064     m_CurrentOpItem(nullptr),
0065     m_CurrentJobItem(nullptr),
0066     m_LastReportUpdate(0)
0067 {
0068     QVBoxLayout *mainLayout = new QVBoxLayout(this);
0069     setLayout(mainLayout);
0070     mainLayout->addWidget(&dialogWidget());
0071     QFrame* detailsBox = new QFrame(this);
0072     mainLayout->addWidget(detailsBox);
0073     QVBoxLayout *detailsLayout = new QVBoxLayout(detailsBox);
0074     detailsLayout->addWidget(&detailsWidget());
0075     detailsWidget().hide();
0076 
0077     setAttribute(Qt::WA_ShowModal, true);
0078 
0079     dialogButtonBox = new QDialogButtonBox;
0080     okButton = dialogButtonBox->addButton(QDialogButtonBox::Ok);
0081     cancelButton = dialogButtonBox->addButton(QDialogButtonBox::Cancel);
0082     detailsButton = new QPushButton;
0083     detailsButton->setText(xi18nc("@action:button", "&Details") + QStringLiteral(" >>"));
0084     detailsButton->setIcon(QIcon::fromTheme(QStringLiteral("help-about")));
0085     dialogButtonBox->addButton(detailsButton, QDialogButtonBox::ActionRole);
0086     mainLayout->addWidget(dialogButtonBox);
0087 
0088     dialogWidget().treeTasks().setColumnWidth(0, width() * 8 / 10);
0089     detailsWidget().buttonBrowser().setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
0090     detailsWidget().buttonSave().setIcon(QIcon::fromTheme(QStringLiteral("document-save")));
0091 
0092     setupConnections();
0093 
0094     KConfigGroup kcg(KSharedConfig::openConfig(), QStringLiteral("applyProgressDialog"));
0095     restoreGeometry(kcg.readEntry<QByteArray>("Geometry", QByteArray()));
0096 }
0097 
0098 /** Destroys a ProgressDialog */
0099 ApplyProgressDialog::~ApplyProgressDialog()
0100 {
0101     KConfigGroup kcg(KSharedConfig::openConfig(), QStringLiteral("applyProgressDialog"));
0102     kcg.writeEntry("Geometry", saveGeometry());
0103     delete m_Report;
0104 }
0105 
0106 void ApplyProgressDialog::setupConnections()
0107 {
0108     connect(&operationRunner(), &OperationRunner::progressSub, &dialogWidget().progressSub(), &QProgressBar::setValue);
0109     connect(&operationRunner(), &OperationRunner::finished, this, &ApplyProgressDialog::onAllOpsFinished);
0110     connect(&operationRunner(), &OperationRunner::cancelled, this, &ApplyProgressDialog::onAllOpsCancelled);
0111     connect(&operationRunner(), &OperationRunner::error, this, &ApplyProgressDialog::onAllOpsError);
0112     connect(&operationRunner(), &OperationRunner::opStarted, this, &ApplyProgressDialog::onOpStarted);
0113     connect(&operationRunner(), &OperationRunner::opFinished, this, &ApplyProgressDialog::onOpFinished);
0114     connect(&timer(), &QTimer::timeout, this, &ApplyProgressDialog::onSecondElapsed);
0115     connect(&detailsWidget().buttonSave(), &QPushButton::clicked, this, &ApplyProgressDialog::saveReport);
0116     connect(&detailsWidget().buttonBrowser(), &QPushButton::clicked, this, &ApplyProgressDialog::browserReport);
0117     connect(dialogButtonBox, &QDialogButtonBox::accepted, this, &ApplyProgressDialog::onOkButton);
0118     connect(dialogButtonBox, &QDialogButtonBox::rejected, this, &ApplyProgressDialog::onCancelButton);
0119     connect(detailsButton, &QPushButton::clicked, this, &ApplyProgressDialog::toggleDetails);
0120 }
0121 
0122 /** Shows the dialog */
0123 void ApplyProgressDialog::show()
0124 {
0125     setStatus(xi18nc("@info:progress", "Setting up..."));
0126 
0127     resetReport();
0128 
0129     dialogWidget().progressTotal().setRange(0, operationRunner().numJobs());
0130     dialogWidget().progressTotal().setValue(0);
0131 
0132     dialogWidget().treeTasks().clear();
0133     okButton->setVisible(false);
0134     okButton->setEnabled(false);
0135     cancelButton->setVisible(true);
0136     cancelButton->setEnabled(true);
0137 
0138 
0139     timer().start(1000);
0140     time().start();
0141 
0142     setLastReportUpdate(0);
0143 
0144     onSecondElapsed(); // resets the total time output label
0145 
0146     QDialog::show();
0147 }
0148 
0149 void ApplyProgressDialog::resetReport()
0150 {
0151     delete m_Report;
0152     m_Report = new Report(nullptr);
0153 
0154     detailsWidget().editReport().clear();
0155     detailsWidget().editReport().setCursorWidth(0);
0156     detailsWidget().buttonSave().setEnabled(false);
0157     detailsWidget().buttonBrowser().setEnabled(false);
0158 
0159     connect(&report(), &Report::outputChanged, this, &ApplyProgressDialog::updateReportUnforced);
0160 }
0161 
0162 void ApplyProgressDialog::closeEvent(QCloseEvent* e)
0163 {
0164     e->ignore();
0165     operationRunner().isRunning() ? onCancelButton() : onOkButton();
0166 }
0167 
0168 void ApplyProgressDialog::toggleDetails()
0169 {
0170     const bool isVisible = detailsWidget().isVisible();
0171     detailsWidget().setVisible(!isVisible);
0172     detailsButton->setText(xi18nc("@action:button", "&Details") + (isVisible ? QStringLiteral(" >>") : QStringLiteral(" <<")));
0173 }
0174 
0175 void ApplyProgressDialog::onDetailsButton()
0176 {
0177     updateReport(true);
0178     return;
0179 }
0180 
0181 void ApplyProgressDialog::onCancelButton()
0182 {
0183     if (operationRunner().isRunning()) {
0184         // only cancel once
0185         if (operationRunner().isCancelling())
0186             return;
0187 
0188         // suspend the runner, so it doesn't happily carry on while the user decides
0189         // if he really wants to cancel
0190         operationRunner().suspendMutex().lock();
0191         if (KMessageBox::warningTwoActions(this, xi18nc("@info", "Do you really want to cancel?"), xi18nc("@title:window", "Cancel Running Operations"), KGuiItem(xi18nc("@action:button", "Yes, Cancel Operations"), QStringLiteral("dialog-ok")), KStandardGuiItem::cancel()) == KMessageBox::PrimaryAction)
0192             // in the meantime while we were showing the messagebox, the runner might have finished.
0193             if (operationRunner().isRunning()) {
0194                 QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
0195                 cancelButton->setEnabled(false);
0196                 setStatus(xi18nc("@info:progress", "Waiting for operation to finish..."));
0197                 repaint();
0198                 dialogWidget().repaint();
0199                 QApplication::restoreOverrideCursor();
0200                 operationRunner().cancel();
0201             }
0202 
0203         operationRunner().suspendMutex().unlock();
0204     }
0205     return;
0206 }
0207 
0208 void ApplyProgressDialog::onOkButton()
0209 {
0210     mainWindow(this)->setWindowTitle(savedParentTitle());
0211 
0212     QDialog::accept();
0213 }
0214 
0215 void ApplyProgressDialog::onAllOpsFinished()
0216 {
0217     allOpsDone(xi18nc("@info:progress", "All operations successfully finished."));
0218 }
0219 
0220 void ApplyProgressDialog::onAllOpsCancelled()
0221 {
0222     allOpsDone(xi18nc("@info:progress", "Operations cancelled."));
0223 }
0224 
0225 void ApplyProgressDialog::onAllOpsError()
0226 {
0227     allOpsDone(xi18nc("@info:progress", "There were errors while applying operations. Aborted."));
0228 }
0229 
0230 void ApplyProgressDialog::allOpsDone(const QString& msg)
0231 {
0232     dialogWidget().progressTotal().setValue(operationRunner().numJobs());
0233     cancelButton->setVisible(false);
0234     okButton->setVisible(true);
0235     okButton->setEnabled(true);
0236     detailsWidget().buttonSave().setEnabled(true);
0237     detailsWidget().buttonBrowser().setEnabled(true);
0238     timer().stop();
0239     updateReport(true);
0240 
0241     setStatus(msg);
0242 }
0243 
0244 void ApplyProgressDialog::updateReportUnforced()
0245 {
0246     updateReport(false);
0247 }
0248 
0249 void ApplyProgressDialog::updateReport(bool force)
0250 {
0251     // Rendering the HTML in the QTextEdit is extremely expensive. So make sure not to do that
0252     // unnecessarily and not too often:
0253     // (1) If the widget isn't visible, don't update.
0254     // (2) Also don't update if the last update was n msecs ago, BUT
0255     // (3) DO update if we're being forced to.
0256     if (force || (detailsWidget().isVisible() && time().elapsed() - lastReportUpdate() > 2000)) {
0257         detailsWidget().editReport().setHtml(QStringLiteral("<html><body>") + report().toHtml() + QStringLiteral("</body></html>"));
0258         detailsWidget().editReport().moveCursor(QTextCursor::End);
0259         detailsWidget().editReport().ensureCursorVisible();
0260 
0261         setLastReportUpdate(time().elapsed());
0262     }
0263 }
0264 
0265 void ApplyProgressDialog::onOpStarted(int num, Operation* op)
0266 {
0267     addTaskOutput(num, *op);
0268     setStatus(op->description());
0269 
0270     dialogWidget().progressSub().setValue(0);
0271     dialogWidget().progressSub().setRange(0, op->totalProgress());
0272 
0273     connect(op, &Operation::jobStarted, this, &ApplyProgressDialog::onJobStarted);
0274     connect(op, &Operation::jobFinished, this, &ApplyProgressDialog::onJobFinished);
0275 }
0276 
0277 void ApplyProgressDialog::onJobStarted(Job* job, Operation* op)
0278 {
0279     for (qint32 i = 0; i < dialogWidget().treeTasks().topLevelItemCount(); i++) {
0280         QTreeWidgetItem* item = dialogWidget().treeTasks().topLevelItem(i);
0281 
0282         if (item == nullptr || reinterpret_cast<const Operation*>(item->data(0, Qt::UserRole).toULongLong()) != op)
0283             continue;
0284 
0285         QTreeWidgetItem* child = new QTreeWidgetItem();
0286         child->setText(0, job->description());
0287         child->setIcon(0, QIcon::fromTheme(job->statusIcon()));
0288         child->setText(1, QTime(0, 0).toString(timeFormat()));
0289         item->addChild(child);
0290         dialogWidget().treeTasks().scrollToBottom();
0291         setCurrentJobItem(child);
0292         break;
0293     }
0294 }
0295 
0296 void ApplyProgressDialog::onJobFinished(Job* job, Operation* op)
0297 {
0298     if (currentJobItem())
0299         currentJobItem()->setIcon(0, QIcon::fromTheme(job->statusIcon()));
0300 
0301     setCurrentJobItem(nullptr);
0302 
0303     const int current = dialogWidget().progressTotal().value();
0304     dialogWidget().progressTotal().setValue(current + 1);
0305 
0306     setParentTitle(op->description());
0307     updateReport(true);
0308 }
0309 
0310 void ApplyProgressDialog::onOpFinished(int num, Operation* op)
0311 {
0312     if (currentOpItem()) {
0313         currentOpItem()->setText(0, opDesc(num, *op));
0314         currentOpItem()->setIcon(0, QIcon::fromTheme(op->statusIcon()));
0315     }
0316 
0317     setCurrentOpItem(nullptr);
0318 
0319     setStatus(op->description());
0320 
0321     dialogWidget().progressSub().setValue(op->totalProgress());
0322     updateReport(true);
0323 }
0324 
0325 void ApplyProgressDialog::setParentTitle(const QString& s)
0326 {
0327     const int percent = dialogWidget().progressTotal().value() * 100 / dialogWidget().progressTotal().maximum();
0328     mainWindow(this)->setWindowTitle(QString::number(percent) + QStringLiteral("% - ") + s + QStringLiteral(" - ") + savedParentTitle());
0329 }
0330 
0331 void ApplyProgressDialog::setStatus(const QString& s)
0332 {
0333     setWindowTitle(s);
0334     dialogWidget().status().setText(s);
0335 
0336     setParentTitle(s);
0337 }
0338 
0339 QString ApplyProgressDialog::opDesc(int num, const Operation& op) const
0340 {
0341     return xi18nc("@info:progress", "[%1/%2] - %3: %4", num, operationRunner().numOperations(), op.statusText(), op.description());
0342 }
0343 
0344 void ApplyProgressDialog::addTaskOutput(int num, const Operation& op)
0345 {
0346     QTreeWidgetItem* item = new QTreeWidgetItem();
0347     item->setIcon(0, QIcon::fromTheme(op.statusIcon()));
0348     item->setText(0, opDesc(num, op));
0349     item->setText(1, QTime(0, 0).toString(timeFormat()));
0350 
0351     QFont f;
0352     f.setWeight(QFont::Bold);
0353     item->setFont(0, f);
0354     item->setFont(1, f);
0355 
0356     item->setData(0, Qt::UserRole, reinterpret_cast<qulonglong>(&op));
0357     dialogWidget().treeTasks().addTopLevelItem(item);
0358     dialogWidget().treeTasks().scrollToBottom();
0359     setCurrentOpItem(item);
0360 }
0361 
0362 void ApplyProgressDialog::onSecondElapsed()
0363 {
0364     if (currentJobItem()) {
0365         QTime t = QTime::fromString(currentJobItem()->text(1), timeFormat()).addSecs(1);
0366         currentJobItem()->setText(1, t.toString(timeFormat()));
0367     }
0368 
0369     if (currentOpItem()) {
0370         QTime t = QTime::fromString(currentOpItem()->text(1), timeFormat()).addSecs(1);
0371         currentOpItem()->setText(1, t.toString(timeFormat()));;
0372     }
0373 
0374     const QTime outputTime = QTime(0, 0).addMSecs(time().elapsed());
0375     dialogWidget().totalTime().setText(xi18nc("@info:progress", "Total Time: %1", outputTime.toString(timeFormat())));
0376 }
0377 
0378 void ApplyProgressDialog::keyPressEvent(QKeyEvent* e)
0379 {
0380     e->accept();
0381 
0382     switch (e->key()) {
0383     case Qt::Key_Return:
0384     case Qt::Key_Enter:
0385         if (okButton->isEnabled())
0386             onOkButton();
0387         break;
0388 
0389     case Qt::Key_Escape:
0390         cancelButton->isEnabled() ? onCancelButton() : onOkButton();
0391         break;
0392 
0393     default:
0394         break;
0395     }
0396 }
0397 
0398 void ApplyProgressDialog::saveReport()
0399 {
0400     const QUrl url = QFileDialog::getSaveFileUrl();
0401 
0402     if (url.isEmpty())
0403         return;
0404 
0405     QTemporaryFile tempFile;
0406 
0407     if (tempFile.open()) {
0408         QTextStream s(&tempFile);
0409 
0410         HtmlReport html;
0411 
0412         s << html.header()
0413           << report().toHtml()
0414           << html.footer();
0415 
0416         tempFile.close();
0417 
0418         KIO::CopyJob* job = KIO::move(QUrl::fromLocalFile(tempFile.fileName()), url, KIO::HideProgressInfo);
0419         job->exec();
0420         if (job->error())
0421             job->uiDelegate()->showErrorMessage();
0422     } else
0423         KMessageBox::error(this, xi18nc("@info", "Could not create temporary file when trying to save to <filename>%1</filename>.", url.fileName()), xi18nc("@title:window", "Could Not Save Report."));
0424 }
0425 
0426 void ApplyProgressDialog::browserReport()
0427 {
0428     QTemporaryFile file;
0429 
0430     file.setFileTemplate(QStringLiteral("/tmp/") + QCoreApplication::applicationName() + QStringLiteral("-XXXXXX.html"));
0431     file.setAutoRemove(false);
0432 
0433     if (file.open()) {
0434         QTextStream s(&file);
0435 
0436         HtmlReport html;
0437 
0438         s << html.header()
0439           << report().toHtml()
0440           << html.footer();
0441 
0442         file.setPermissions(QFile::ReadOwner | QFile::WriteOwner);
0443 
0444         auto *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(file.fileName()), QStringLiteral("text/html"), this);
0445         job->start();
0446     } else
0447         KMessageBox::error(this, xi18nc("@info", "Could not create temporary file <filename>%1</filename> for writing.", file.fileName()), i18nc("@title:window", "Could Not Launch Browser."));
0448 }