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"