File indexing completed on 2024-12-15 03:45:03

0001 /*
0002     SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: MIT
0005 */
0006 
0007 #include <kuserfeedback_version.h>
0008 #include "mainwindow.h"
0009 #include "ui_mainwindow.h"
0010 #include "helpcontroller.h"
0011 #include "connectdialog.h"
0012 
0013 #include <jobs/handshakejob.h>
0014 #include <jobs/productexportjob.h>
0015 #include <jobs/productimportjob.h>
0016 #include <model/productmodel.h>
0017 
0018 #include <rest/restapi.h>
0019 #include <rest/restclient.h>
0020 #include <rest/serverinfo.h>
0021 
0022 #include <provider/widgets/feedbackconfigdialog.h>
0023 #include <provider/widgets/notificationpopup.h>
0024 #include <provider/core/applicationversionsource.h>
0025 #include <provider/core/platforminfosource.h>
0026 #include <provider/core/propertyratiosource.h>
0027 #include <provider/core/provider.h>
0028 #include <provider/core/qtversionsource.h>
0029 #include <provider/core/startcountsource.h>
0030 #include <provider/core/usagetimesource.h>
0031 
0032 #include <QActionGroup>
0033 #include <QApplication>
0034 #include <QDebug>
0035 #include <QFileDialog>
0036 #include <QIcon>
0037 #include <QInputDialog>
0038 #include <QKeySequence>
0039 #include <QMessageBox>
0040 #include <QNetworkAccessManager>
0041 #include <QNetworkReply>
0042 #include <QNetworkRequest>
0043 #include <QScopedValueRollback>
0044 #include <QSettings>
0045 #include <QTimer>
0046 #include <QUrl>
0047 
0048 using namespace KUserFeedback::Console;
0049 
0050 MainWindow::MainWindow() :
0051     ui(new Ui::MainWindow),
0052     m_restClient(new RESTClient(this)),
0053     m_productModel(new ProductModel(this)),
0054     m_feedbackProvider(new KUserFeedback::Provider(this))
0055 {
0056     ui->setupUi(this);
0057     setWindowIcon(QIcon::fromTheme(QStringLiteral("search")));
0058 
0059     addView(ui->surveyEditor, ui->menuSurvey);
0060     addView(ui->schemaEdit, ui->menuSchema);
0061     addView(ui->analyticsView, ui->menuAnalytics);
0062 
0063     ui->productListView->setModel(m_productModel);
0064     ui->productListView->addActions({ ui->actionAddProduct, ui->actionDeleteProduct });
0065 
0066     connect(m_restClient, &RESTClient::errorMessage, this, &MainWindow::logError);
0067     m_productModel->setRESTClient(m_restClient);
0068 
0069     ui->actionViewAnalytics->setData(QVariant::fromValue(ui->analyticsView));
0070     ui->actionViewSurveys->setData(QVariant::fromValue(ui->surveyEditor));
0071     ui->actionViewSchema->setData(QVariant::fromValue(ui->schemaEdit));
0072     auto viewGroup = new QActionGroup(this);
0073     viewGroup->setExclusive(true);
0074     viewGroup->addAction(ui->actionViewAnalytics);
0075     viewGroup->addAction(ui->actionViewSurveys);
0076     viewGroup->addAction(ui->actionViewSchema);
0077     connect(viewGroup, &QActionGroup::triggered, this, [this](QAction *action) {
0078         auto view = action->data().value<QWidget*>();
0079         if (ui->viewStack->currentWidget() == ui->schemaEdit && ui->schemaEdit->isDirty() && view != ui->schemaEdit) {
0080             const auto r = QMessageBox::critical(this, tr("Unsaved Schema Changes"),
0081                 tr("You have unsaved changes in the schema editor. Do you really want to close it and discard your changes?"),
0082                 QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel);
0083             if (r != QMessageBox::Discard) {
0084                 ui->actionViewSchema->setChecked(true);
0085                 return;
0086             }
0087         }
0088         ui->viewStack->setCurrentWidget(view);
0089     });
0090     ui->actionViewAnalytics->setChecked(true);
0091 
0092     connect(ui->actionConnectToServer, &QAction::triggered, this, [this]() {
0093         QSettings settings;
0094         auto info = ServerInfo::load(settings.value(QStringLiteral("LastServerInfo")).toString());
0095         ConnectDialog dlg(this);
0096         dlg.addRecentServerInfos(ServerInfo::allServerInfoNames());
0097         dlg.setServerInfo(info);
0098         if (dlg.exec()) {
0099             info = dlg.serverInfo();
0100             info.save();
0101             settings.setValue(QStringLiteral("LastServerInfo"), info.name());
0102             connectToServer(info);
0103         }
0104     });
0105 
0106     connect(ui->actionAddProduct, &QAction::triggered, this, &MainWindow::createProduct);
0107     connect(ui->actionDeleteProduct, &QAction::triggered, this, &MainWindow::deleteProduct);
0108     connect(ui->actionImportProduct, &QAction::triggered, this, &MainWindow::importProduct);
0109     connect(ui->actionExportProduct, &QAction::triggered, this, &MainWindow::exportProduct);
0110 
0111     connect(ui->schemaEdit, &SchemaEditor::productChanged, m_productModel, &ProductModel::reload);
0112 
0113     ui->actionQuit->setShortcut(QKeySequence::Quit);
0114     connect(ui->actionQuit, &QAction::triggered, this, &QMainWindow::close);
0115     ui->menuWindow->addAction(ui->productsDock->toggleViewAction());
0116     ui->menuWindow->addAction(ui->logDock->toggleViewAction());
0117 
0118     ui->actionUserManual->setEnabled(HelpController::isAvailable());
0119     ui->actionUserManual->setShortcut(QKeySequence::HelpContents);
0120     connect(ui->actionUserManual, &QAction::triggered, this, []() {
0121         HelpController::openContents();
0122     });
0123     ui->actionContribute->setVisible(m_feedbackProvider->isEnabled());
0124     connect(ui->actionContribute, &QAction::triggered, this, [this]() {
0125         FeedbackConfigDialog dlg(this);
0126         dlg.setFeedbackProvider(m_feedbackProvider);
0127         dlg.exec();
0128     });
0129     connect(ui->actionAbout, &QAction::triggered, this, [this]() {
0130         QMessageBox::about(this, tr("About User Feedback Console"), tr(
0131             "Version: %1\n"
0132             "License: MIT\n"
0133             "Copyright ⓒ 2017 Volker Krause <vkrause@kde.org>"
0134         ).arg(QStringLiteral(KUSERFEEDBACK_VERSION_STRING)));
0135     });
0136 
0137     connect(ui->productListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this](const QItemSelection&, const QItemSelection &deselected) {
0138         static bool recursionGuard = false;
0139         if (recursionGuard)
0140             return;
0141         if (ui->viewStack->currentWidget() == ui->schemaEdit && ui->schemaEdit->isDirty()) {
0142             const auto r = QMessageBox::critical(this, tr("Unsaved Schema Changes"),
0143                 tr("You have unsaved changes in the schema editor, do you really want to open another product and discard your changes?"),
0144                 QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel);
0145             if (r != QMessageBox::Discard) {
0146                 QScopedValueRollback<bool> guard(recursionGuard, true);
0147                 ui->productListView->selectionModel()->select(deselected, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current);
0148                 return;
0149             }
0150         }
0151         productSelected();
0152     });
0153     connect(m_productModel, &QAbstractItemModel::dataChanged, this, &MainWindow::productSelected);
0154 
0155     connect(ui->viewStack, &QStackedWidget::currentChanged, this, &MainWindow::updateActions);
0156     connect(ui->viewStack, &QStackedWidget::currentChanged, this, [viewGroup](int index) {
0157         viewGroup->actions().at(index)->setChecked(true);
0158     });
0159     updateActions();
0160 
0161     QSettings settings;
0162     settings.beginGroup(QStringLiteral("MainWindow"));
0163     restoreGeometry(settings.value(QStringLiteral("Geometry")).toByteArray());
0164     restoreState(settings.value(QStringLiteral("State")).toByteArray());
0165     ui->viewStack->setCurrentIndex(settings.value(QStringLiteral("CurrentView"), 0).toInt());
0166 
0167     QTimer::singleShot(0, ui->actionConnectToServer, &QAction::trigger);
0168 
0169     m_feedbackProvider->setFeedbackServer(QUrl(QStringLiteral("https://telemetry.kde.org")));
0170     m_feedbackProvider->setSubmissionInterval(1);
0171     auto viewModeSource = new KUserFeedback::PropertyRatioSource(ui->viewStack, "currentIndex", QStringLiteral("viewRatio"));
0172     viewModeSource->setDescription(tr("Usage ratio of the analytics view, survey editor and schema editor."));
0173     viewModeSource->addValueMapping(0, QStringLiteral("analytics"));
0174     viewModeSource->addValueMapping(1, QStringLiteral("surveyEditor"));
0175     viewModeSource->addValueMapping(2, QStringLiteral("schemaEditor"));
0176     viewModeSource->setTelemetryMode(Provider::DetailedUsageStatistics);
0177     m_feedbackProvider->addDataSource(viewModeSource);
0178     m_feedbackProvider->addDataSource(new ApplicationVersionSource);
0179     m_feedbackProvider->addDataSource(new PlatformInfoSource);
0180     m_feedbackProvider->addDataSource(new QtVersionSource);
0181     m_feedbackProvider->addDataSource(new StartCountSource);
0182     m_feedbackProvider->addDataSource(new UsageTimeSource);
0183 
0184     m_feedbackProvider->setEncouragementDelay(60);
0185     m_feedbackProvider->setEncouragementInterval(5);
0186     m_feedbackProvider->setApplicationStartsUntilEncouragement(5);
0187     m_feedbackProvider->setApplicationUsageTimeUntilEncouragement(600); // 10 mins
0188 
0189     auto notifyPopup = new KUserFeedback::NotificationPopup(this);
0190     notifyPopup->setFeedbackProvider(m_feedbackProvider);
0191 }
0192 
0193 MainWindow::~MainWindow()
0194 {
0195     QSettings settings;
0196     settings.beginGroup(QStringLiteral("MainWindow"));
0197     settings.setValue(QStringLiteral("State"), saveState());
0198     settings.setValue(QStringLiteral("Geometry"), saveGeometry());
0199     settings.setValue(QStringLiteral("CurrentView"), ui->viewStack->currentIndex());
0200 }
0201 
0202 template <typename T>
0203 void MainWindow::addView(T *view, QMenu *menu)
0204 {
0205     for (auto action : view->actions())
0206         menu->addAction(action);
0207 
0208     view->setRESTClient(m_restClient);
0209     connect(view, &T::logMessage, this, &MainWindow::logMessage);
0210 }
0211 
0212 void MainWindow::connectToServer(const ServerInfo& info)
0213 {
0214     m_restClient->setServerInfo(info);
0215     auto job = new HandshakeJob(m_restClient, this);
0216     connect(job, &Job::destroyed, this, &MainWindow::updateActions);
0217     connect(job, &Job::error, this, [this](const QString &msg) {
0218         logError(msg);
0219         QMessageBox::critical(this, tr("Connection Failure"), tr("Failed to connect to server: %1").arg(msg));
0220     });
0221     connect(job, &Job::info, this, &MainWindow::logMessage);
0222 }
0223 
0224 void MainWindow::createProduct()
0225 {
0226     const auto name = QInputDialog::getText(this, tr("Add New Product"), tr("Product Identifier:"));
0227     if (name.isEmpty())
0228         return;
0229     Product product;
0230     product.setName(name);
0231 
0232     auto reply = RESTApi::createProduct(m_restClient, product);
0233     connect(reply, &QNetworkReply::finished, this, [this, reply, name]() {
0234         reply->deleteLater();
0235         if (reply->error() == QNetworkReply::NoError) {
0236             logMessage(QString::fromUtf8(reply->readAll()));
0237             m_productModel->reload();
0238         }
0239     });
0240 }
0241 
0242 void MainWindow::deleteProduct()
0243 {
0244     auto sel = ui->productListView->selectionModel()->selectedRows();
0245     if (sel.isEmpty())
0246         return;
0247     const auto product = sel.first().data(ProductModel::ProductRole).value<Product>();
0248     const auto mb = QMessageBox::critical(this, tr("Delete Product"),
0249                         tr("Do you really want to delete product %1 with all its data?").arg(product.name()),
0250                         QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel);
0251     if (mb != QMessageBox::Discard)
0252         return;
0253 
0254     auto reply = RESTApi::deleteProduct(m_restClient, product);
0255     connect(reply, &QNetworkReply::finished, this, [this, reply]() {
0256         reply->deleteLater();
0257         if (reply->error() == QNetworkReply::NoError) {
0258             logMessage(QString::fromUtf8(reply->readAll()));
0259         }
0260         m_productModel->reload();
0261     });
0262 }
0263 
0264 void MainWindow::importProduct()
0265 {
0266     const auto fileName = QFileDialog::getExistingDirectory(this, tr("Import Product"));
0267     if (fileName.isEmpty())
0268         return;
0269 
0270     QFileInfo fi(fileName);
0271     if (!fi.exists() || !fi.isDir() || !fi.isReadable()) {
0272         QMessageBox::critical(this, tr("Import Failed"), tr("Could not open file."));
0273         return;
0274     }
0275 
0276     auto job = new ProductImportJob(fileName, m_restClient, this);
0277     connect(job, &Job::error, this, [this](const auto &msg) {
0278         QMessageBox::critical(this, tr("Import Failed"), tr("Import error: %1").arg(msg));
0279     });
0280     connect(job, &Job::finished, this, [this]() {
0281         logMessage(tr("Product imported successfully."));
0282         m_productModel->reload();
0283     });
0284 }
0285 
0286 void MainWindow::exportProduct()
0287 {
0288     if (!selectedProduct().isValid())
0289         return;
0290 
0291     const auto fileName = QFileDialog::getExistingDirectory(this, tr("Export Product"));
0292     if (fileName.isEmpty())
0293         return;
0294 
0295     QFileInfo fi(fileName);
0296     if (!fi.exists() || !fi.isDir() || !fi.isWritable()) {
0297         QMessageBox::critical(this, tr("Import Failed"), tr("Could not open file."));
0298         return;
0299     }
0300 
0301     auto job = new ProductExportJob(selectedProduct(), fileName, m_restClient, this);
0302     connect(job, &Job::error, this, [this](const auto &msg) {
0303         QMessageBox::critical(this, tr("Export Failed"), tr("Export error: %1").arg(msg));
0304     });
0305     connect(job, &Job::finished, this, [this]() {
0306         logMessage(tr("Product exported successfully."));
0307     });
0308 }
0309 
0310 void MainWindow::productSelected()
0311 {
0312     const auto product = selectedProduct();
0313     ui->surveyEditor->setProduct(product);
0314     ui->schemaEdit->setProduct(product);
0315     ui->analyticsView->setProduct(product);
0316     updateActions();
0317 }
0318 
0319 void MainWindow::logMessage(const QString& msg)
0320 {
0321     ui->logWidget->appendPlainText(msg);
0322 }
0323 
0324 void MainWindow::logError(const QString& msg)
0325 {
0326     ui->logWidget->appendHtml( QStringLiteral("<font color=\"red\">") + msg + QStringLiteral("</font>"));
0327 }
0328 
0329 Product MainWindow::selectedProduct() const
0330 {
0331     const auto selection = ui->productListView->selectionModel()->selectedRows();
0332     if (selection.isEmpty())
0333         return {};
0334     const auto idx = selection.first();
0335     return idx.data(ProductModel::ProductRole).value<Product>();
0336 }
0337 
0338 void MainWindow::updateActions()
0339 {
0340     // product action state
0341     ui->actionAddProduct->setEnabled(m_restClient->isConnected());
0342     ui->actionDeleteProduct->setEnabled(selectedProduct().isValid());
0343     ui->actionImportProduct->setEnabled(m_restClient->isConnected());
0344     ui->actionExportProduct->setEnabled(selectedProduct().isValid());
0345 
0346     // deactivate menus of the inactive views
0347     ui->menuAnalytics->setEnabled(ui->viewStack->currentWidget() == ui->analyticsView);
0348     ui->menuSurvey->setEnabled(ui->viewStack->currentWidget() == ui->surveyEditor);
0349     ui->menuSchema->setEnabled(ui->viewStack->currentWidget() == ui->schemaEdit);
0350 }
0351 
0352 void MainWindow::closeEvent(QCloseEvent* event)
0353 {
0354     if (ui->schemaEdit->isDirty()) {
0355         const auto r = QMessageBox::critical(this, tr("Unsaved Changes"),
0356             tr("There are unsaved changes in the schema editor. Do you want to discard them and close the application?"),
0357             QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel);
0358         if (r != QMessageBox::Discard) {
0359             event->ignore();
0360             return;
0361         }
0362     }
0363     QMainWindow::closeEvent(event);
0364 }
0365 
0366 #include "moc_mainwindow.cpp"