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 }