File indexing completed on 2024-06-16 04:47:14

0001 /***************************************************************************
0002  * SPDX-FileCopyrightText: 2022 S. MANKOWSKI stephane@mankowski.fr
0003  * SPDX-FileCopyrightText: 2022 G. DE BURE support@mankowski.fr
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  ***************************************************************************/
0006 /** @file
0007  * This file is plugin for advice.
0008  *
0009  * @author Stephane MANKOWSKI / Guillaume DE BURE
0010  */
0011 #include "skgadviceboardwidget.h"
0012 
0013 #include <qdom.h>
0014 #include <qlayoutitem.h>
0015 #include <qmenu.h>
0016 #include <qpushbutton.h>
0017 #include <qtoolbutton.h>
0018 
0019 #include <klocalizedstring.h>
0020 
0021 #include "skgdocument.h"
0022 #include "skginterfaceplugin.h"
0023 #include "skgmainpanel.h"
0024 #include "skgservices.h"
0025 #include "skgtraces.h"
0026 #include "skgtransactionmng.h"
0027 
0028 SKGAdviceBoardWidget::SKGAdviceBoardWidget(QWidget* iParent, SKGDocument* iDocument)
0029     : SKGBoardWidget(iParent, iDocument, i18nc("Dashboard widget title", "Advices")), m_maxAdvice(7), m_refreshNeeded(true), m_refresh(nullptr), m_inapplyall(false)
0030 {
0031     SKGTRACEINFUNC(10)
0032 
0033     // Create menu
0034     setContextMenuPolicy(Qt::ActionsContextMenu);
0035 
0036     auto g = new QWidget(this);
0037     m_layout = new QFormLayout(g);
0038     m_layout->setContentsMargins(0, 0, 0, 0);
0039     m_layout->setObjectName(QStringLiteral("Slayout"));
0040     m_layout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
0041     m_layout->setHorizontalSpacing(1);
0042     m_layout->setVerticalSpacing(1);
0043     setMainWidget(g);
0044 
0045     // menu
0046     auto menuResetAdvice = new QAction(SKGServices::fromTheme(QStringLiteral("edit-undo")), i18nc("Noun, a user action", "Activate all advice"), this);
0047     connect(menuResetAdvice, &QAction::triggered, this, &SKGAdviceBoardWidget::activateAllAdvice);
0048     addAction(menuResetAdvice);
0049 
0050     auto sep = new QAction(this);
0051     sep->setSeparator(true);
0052     addAction(sep);
0053 
0054     m_menuAuto = new QAction(i18nc("Noun, a type of refresh for advice", "Automatic refresh"), this);
0055     m_menuAuto->setCheckable(true);
0056     m_menuAuto->setChecked(true);
0057     connect(m_menuAuto, &QAction::triggered, this, &SKGAdviceBoardWidget::dataModifiedNotForce);
0058     addAction(m_menuAuto);
0059 
0060     // Refresh
0061     connect(getDocument(), &SKGDocument::transactionSuccessfullyEnded, this, &SKGAdviceBoardWidget::dataModifiedNotForce, Qt::QueuedConnection);
0062     connect(SKGMainPanel::getMainPanel(), &SKGMainPanel::currentPageChanged, this, &SKGAdviceBoardWidget::pageChanged, Qt::QueuedConnection);
0063     connect(this, &SKGAdviceBoardWidget::refreshNeeded, this, [ = ]() {
0064         this->dataModifiedNotForce();
0065     }, Qt::QueuedConnection);
0066 }
0067 
0068 SKGAdviceBoardWidget::~SKGAdviceBoardWidget()
0069 {
0070     SKGTRACEINFUNC(10)
0071     m_menuAuto = nullptr;
0072     m_refresh = nullptr;
0073 }
0074 
0075 QString SKGAdviceBoardWidget::getState()
0076 {
0077     QDomDocument doc(QStringLiteral("SKGML"));
0078     doc.setContent(SKGBoardWidget::getState());
0079     QDomElement root = doc.documentElement();
0080 
0081     root.setAttribute(QStringLiteral("maxAdvice"), SKGServices::intToString(m_maxAdvice));
0082     root.setAttribute(QStringLiteral("automatic"), (m_menuAuto->isChecked() ? QStringLiteral("Y") : QStringLiteral("N")));
0083     return doc.toString();
0084 }
0085 
0086 void SKGAdviceBoardWidget::setState(const QString& iState)
0087 {
0088     SKGBoardWidget::setState(iState);
0089 
0090     QDomDocument doc(QStringLiteral("SKGML"));
0091     doc.setContent(iState);
0092     QDomElement root = doc.documentElement();
0093 
0094     QString maxAdviceS = root.attribute(QStringLiteral("maxAdvice"));
0095     if (maxAdviceS.isEmpty()) {
0096         maxAdviceS = '7';
0097     }
0098     m_maxAdvice = SKGServices::stringToInt(maxAdviceS);
0099 
0100     QString automatic = root.attribute(QStringLiteral("automatic"));
0101     if (automatic.isEmpty()) {
0102         automatic = 'Y';
0103     }
0104 
0105     if (m_menuAuto != nullptr) {
0106         bool previous = m_menuAuto->blockSignals(true);
0107         m_menuAuto->setChecked(automatic == QStringLiteral("Y"));
0108         m_menuAuto->blockSignals(previous);
0109     }
0110 
0111     dataModifiedForce();
0112 }
0113 
0114 void SKGAdviceBoardWidget::pageChanged()
0115 {
0116     if (m_refreshNeeded) {
0117         dataModifiedNotForce();
0118     }
0119 }
0120 
0121 void SKGAdviceBoardWidget::dataModifiedNotForce()
0122 {
0123     dataModified(false);
0124 }
0125 
0126 void SKGAdviceBoardWidget::dataModifiedForce()
0127 {
0128     dataModified(true);
0129 }
0130 
0131 void SKGAdviceBoardWidget::dataModified(bool iForce)
0132 {
0133     SKGTRACEINFUNC(10)
0134     SKGTabPage* page = SKGTabPage::parentTabPage(this);
0135     if (m_inapplyall || (!iForce && ((page != nullptr && page != SKGMainPanel::getMainPanel()->currentPage()) || !m_menuAuto->isChecked()))) {
0136         m_refreshNeeded = true;
0137         if (m_refresh != nullptr) {
0138             m_refresh->show();
0139         }
0140         return;
0141     }
0142     QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
0143     m_refreshNeeded = false;
0144 
0145     // Remove all item of the layout
0146     while (m_layout->count() != 0) {
0147         QLayoutItem* child = m_layout->takeAt(0);
0148         if (child != nullptr) {
0149             QWidget* w = child->widget();
0150             delete w;
0151             delete child;
0152         }
0153     }
0154 
0155     // Get list of advice
0156     double elapse = SKGServices::getMicroTime();
0157     SKGAdviceList globalAdviceList = SKGMainPanel::getMainPanel()->getAdvice();
0158     elapse = SKGServices::getMicroTime() - elapse;
0159 
0160     // Get list of ignored advice
0161     QString currentMonth = QDate::currentDate().toString(QStringLiteral("yyyy-MM"));
0162     QStringList ignoredAdvice = getDocument()->getParameters(QStringLiteral("advice"), "t_value='I' OR t_value='I_" % currentMonth % '\'');
0163     if (elapse > 3000 && m_menuAuto->isChecked() && !ignoredAdvice.contains(QStringLiteral("skgadviceboardwidget_verylong"))) {
0164         SKGAdvice ad;
0165         ad.setUUID(QStringLiteral("skgadviceboardwidget_verylong"));
0166         ad.setPriority(2);
0167         ad.setShortMessage(i18nc("Advice on making the best (short)", "Advice are very long to compute"));
0168         ad.setLongMessage(i18nc("Advice on making the best (long)", "To improve performances, you should switch the widget in 'Manual refresh' (see contextual menu)."));
0169         globalAdviceList.push_back(ad);
0170     }
0171 
0172     // Fill layout
0173     int nbDisplayedAdvice = 0;
0174     int nb = globalAdviceList.count();
0175     m_recommendedActions.clear();
0176     for (int i = 0; i < m_maxAdvice && i < nb; ++i) {
0177         // Get advice
0178         const SKGAdvice& ad = globalAdviceList.at(i);
0179 
0180         // Create icon
0181         QString iconName = (ad.getPriority() == -1 ? QStringLiteral("dialog-information") : ad.getPriority() >= 8 ? QStringLiteral("security-low") : ad.getPriority() <= 4 ? QStringLiteral("security-high") : QStringLiteral("security-medium"));
0182         QString toolTipString = i18n("<p><b>Priority %1:</b>%2</p>", SKGServices::intToString(ad.getPriority()), ad.getLongMessage());
0183 
0184         // Add ignored action
0185         SKGAdvice::SKGAdviceActionList autoCorrections = ad.getAutoCorrections();
0186         {
0187             SKGAdvice::SKGAdviceAction dismiss;
0188             dismiss.Title = i18nc("Dismiss an advice provided", "Dismiss");
0189             dismiss.IconName = QStringLiteral("edit-delete");
0190             dismiss.IsRecommended = false;
0191             autoCorrections.push_back(dismiss);
0192         }
0193         {
0194             SKGAdvice::SKGAdviceAction dismiss;
0195             dismiss.Title = i18nc("Dismiss an advice provided", "Dismiss during current month");
0196             dismiss.IconName = QStringLiteral("edit-delete");
0197             dismiss.IsRecommended = false;
0198             autoCorrections.push_back(dismiss);
0199         }
0200         {
0201             SKGAdvice::SKGAdviceAction dismiss;
0202             dismiss.Title = i18nc("Dismiss an advice provided", "Dismiss this kind");
0203             dismiss.IconName = QStringLiteral("edit-delete");
0204             dismiss.IsRecommended = false;
0205             autoCorrections.push_back(dismiss);
0206         }
0207         {
0208             SKGAdvice::SKGAdviceAction dismiss;
0209             dismiss.Title = i18nc("Dismiss an advice provided", "Dismiss this kind during current month");
0210             dismiss.IconName = QStringLiteral("edit-delete");
0211             dismiss.IsRecommended = false;
0212             autoCorrections.push_back(dismiss);
0213         }
0214 
0215         int nbSolution = autoCorrections.count();
0216 
0217         // Build button
0218         QStringList overlays;
0219         overlays.push_back(nbSolution > 2 ? QStringLiteral("system-run") : QStringLiteral("edit-delete"));
0220         auto icon = new QToolButton(this);
0221         icon->setObjectName(ad.getUUID());
0222         icon->setIcon(SKGServices::fromTheme(iconName, overlays));
0223         icon->setIconSize(QSize(24, 24));
0224         icon->setMaximumSize(QSize(24, 24));
0225         icon->setCursor(Qt::PointingHandCursor);
0226         icon->setAutoRaise(true);
0227 
0228         auto menu = new QMenu(this);
0229         menu->setIcon(icon->icon());
0230         for (int k = 0; k < nbSolution; ++k) {
0231             SKGAdvice::SKGAdviceAction adviceAction = autoCorrections.at(k);
0232             QString actionText = adviceAction.Title;
0233             QAction* action = SKGMainPanel::getMainPanel()->getGlobalAction(QString(actionText).remove(QStringLiteral("skg://")), false);
0234             QAction* act;
0235             if (action != nullptr) {
0236                 // This is an action
0237                 act = menu->addAction(action->icon(), action->text(), SKGMainPanel::getMainPanel(), static_cast<bool (SKGMainPanel::*)()>(&SKGMainPanel::openPage));
0238                 act->setData(actionText);
0239             } else {
0240                 // This is a text
0241                 act = menu->addAction(SKGServices::fromTheme(adviceAction.IconName.isEmpty() ?  QStringLiteral("system-run") : adviceAction.IconName), autoCorrections.at(k).Title, this, &SKGAdviceBoardWidget::adviceClicked);
0242                 if (act != nullptr) {
0243                     act->setProperty("id", ad.getUUID());
0244                     act->setProperty("solution", k < nbSolution - 4 ? k : k - nbSolution);
0245                 }
0246             }
0247 
0248             if ((act != nullptr) && adviceAction.IsRecommended) {
0249                 act->setToolTip(act->text());
0250                 act->setText(act->text() % i18nc("To recommend this action", " (recommended)"));
0251                 m_recommendedActions.append(act);
0252             }
0253         }
0254 
0255         icon->setMenu(menu);
0256         icon->setPopupMode(QToolButton::InstantPopup);
0257 
0258         icon->setToolTip(toolTipString);
0259 
0260         // Create text
0261         auto label = new QLabel(this);
0262         label->setText(ad.getShortMessage());
0263         label->setToolTip(toolTipString);
0264 
0265         // Add them
0266         m_layout->addRow(icon, label);
0267 
0268         ++nbDisplayedAdvice;
0269     }
0270 
0271     // Add apply all recommended actions
0272     QPushButton*  apply = nullptr;
0273     int nb2 = m_recommendedActions.count();
0274     if (nb2 != 0) {
0275         apply = new QPushButton(this);
0276         apply->setIcon(SKGServices::fromTheme(QStringLiteral("games-solve")));
0277         apply->setIconSize(QSize(22, 22));
0278         apply->setMaximumSize(QSize(22, 22));
0279         apply->setCursor(Qt::PointingHandCursor);
0280         QString ToolTip;
0281         for (int i = 0; i < nb2; ++i) {
0282             if (i > 0) {
0283                 ToolTip += '\n';
0284             }
0285             ToolTip += m_recommendedActions.at(i)->toolTip();
0286         }
0287         apply->setToolTip(ToolTip);
0288         connect(apply, &QPushButton::clicked, this, &SKGAdviceBoardWidget::applyRecommended, Qt::QueuedConnection);
0289     }
0290 
0291     // Add more
0292     if (nb > m_maxAdvice) {
0293         auto more = new QPushButton(this);
0294         more->setIcon(SKGServices::fromTheme(QStringLiteral("arrow-down-double")));
0295         more->setIconSize(QSize(22, 22));
0296         more->setMaximumSize(QSize(22, 22));
0297         more->setCursor(Qt::PointingHandCursor);
0298         more->setToolTip(i18nc("Information message", "Display all advices"));
0299         connect(more, &QPushButton::clicked, this, &SKGAdviceBoardWidget::moreAdvice, Qt::QueuedConnection);
0300 
0301         if (apply != nullptr) {
0302             m_layout->addRow(more, apply);
0303             apply = nullptr;
0304         } else {
0305             m_layout->addRow(more, new QLabel(this));
0306         }
0307     } else if (nbDisplayedAdvice > 7) {
0308         // Add less
0309         auto less = new QPushButton(this);
0310         less->setIcon(SKGServices::fromTheme(QStringLiteral("arrow-up-double")));
0311         less->setIconSize(QSize(22, 22));
0312         less->setMaximumSize(QSize(22, 22));
0313         less->setCursor(Qt::PointingHandCursor);
0314         less->setToolTip(i18nc("Information message", "Display less advices"));
0315         connect(less, &QPushButton::clicked, this, &SKGAdviceBoardWidget::lessAdvice, Qt::QueuedConnection);
0316         if (apply != nullptr) {
0317             m_layout->addRow(less, apply);
0318             apply = nullptr;
0319         } else {
0320             m_layout->addRow(less, new QLabel(this));
0321         }
0322     }
0323 
0324     if (apply != nullptr) {
0325         m_layout->addRow(apply, new QLabel(this));
0326     }
0327 
0328     // Add manual refresh button
0329     m_refresh = new QPushButton(this);
0330     m_refresh->setIcon(SKGServices::fromTheme(QStringLiteral("view-refresh")));
0331     m_refresh->setIconSize(QSize(22, 22));
0332     m_refresh->setMaximumSize(QSize(22, 22));
0333     m_refresh->setCursor(Qt::PointingHandCursor);
0334     m_refresh->setToolTip(i18nc("Information message", "Refresh advices"));
0335     m_refresh->hide();
0336     connect(m_refresh, &QPushButton::clicked, this, &SKGAdviceBoardWidget::dataModifiedForce, Qt::QueuedConnection);
0337 
0338     m_layout->addRow(m_refresh, new QLabel(this));
0339 
0340     QApplication::restoreOverrideCursor();
0341 }
0342 
0343 void SKGAdviceBoardWidget::moreAdvice()
0344 {
0345     m_maxAdvice = 9999999;
0346     dataModifiedForce();
0347 }
0348 
0349 void SKGAdviceBoardWidget::lessAdvice()
0350 {
0351     m_maxAdvice = 7;
0352     dataModifiedForce();
0353 }
0354 
0355 void SKGAdviceBoardWidget::applyRecommended()
0356 {
0357     SKGError err;
0358     SKGBEGINTRANSACTION(*getDocument(), i18nc("Noun, name of the user action", "Apply all recommended corrections"), err)
0359     m_inapplyall = true;
0360     int nb = m_recommendedActions.count();
0361     for (int i = 0; i < nb; ++i) {
0362         m_recommendedActions.at(i)->trigger();
0363     }
0364     m_inapplyall = false;
0365 }
0366 
0367 void SKGAdviceBoardWidget::activateAllAdvice()
0368 {
0369     SKGError err;
0370     {
0371         SKGBEGINTRANSACTION(*getDocument(), i18nc("Noun, name of the user action", "Activate all advice"), err)
0372         err = getDocument()->executeSqliteOrder(QStringLiteral("DELETE FROM parameters WHERE t_uuid_parent='advice'"));
0373     }
0374 
0375     // status bar
0376     IFOKDO(err, SKGError(0, i18nc("Successful message after an user action", "Advice activated.")))
0377     else {
0378         err.addError(ERR_FAIL, i18nc("Error message",  "Advice activation failed"));
0379     }
0380 
0381     SKGMainPanel::displayErrorMessage(err);
0382 }
0383 
0384 void SKGAdviceBoardWidget::adviceClicked()
0385 {
0386     // Get advice identifier
0387     auto* act = qobject_cast< QAction* >(sender());
0388     if (act != nullptr) {
0389         QString uuid = act->property("id").toString();
0390         if (!uuid.isEmpty()) {
0391             // Get solution clicker
0392             int solution = sender()->property("solution").toInt();
0393 
0394             if (solution < 0) {
0395                 // We have to ignore this advice
0396                 SKGError err;
0397                 {
0398                     SKGBEGINLIGHTTRANSACTION(*getDocument(), i18nc("Noun, name of the user action", "Dismiss advice"), err)
0399                     QString currentMonth = QDate::currentDate().toString(QStringLiteral("yyyy-MM"));
0400 
0401                     // Create dismiss
0402                     if (solution == -1 || solution == -2) {
0403                         uuid = SKGServices::splitCSVLine(uuid, '|').at(0);
0404                     }
0405                     IFOKDO(err, getDocument()->setParameter(uuid, solution == -2 || solution == -4 ? QStringLiteral("I") :
0406                                                             QString("I_" % currentMonth), QVariant(), QStringLiteral("advice")))
0407 
0408                     // Delete useless dismiss
0409                     IFOKDO(err, getDocument()->executeSqliteOrder("DELETE FROM parameters WHERE t_uuid_parent='advice' AND t_value like 'I_ % ' AND t_value!='I_" % currentMonth % '\''))
0410                 }
0411 
0412                 // status bar
0413                 IFOKDO(err, SKGError(0, i18nc("Successful message after an user action", "Advice dismissed.")))
0414                 else {
0415                     err.addError(ERR_FAIL, i18nc("Error message",  "Advice dismiss failed"));
0416                 }
0417             } else {
0418                 // Get last transaction id
0419                 int previous = getDocument()->getTransactionToProcess(SKGDocument::UNDO);
0420 
0421                 // Execute the advice correction on all plugin
0422                 QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
0423                 int index = 0;
0424                 while (index >= 0) {
0425                     SKGInterfacePlugin* plugin = SKGMainPanel::getMainPanel()->getPluginByIndex(index);
0426                     if (plugin != nullptr) {
0427                         SKGError err = plugin->executeAdviceCorrection(uuid, solution);
0428                         if (!err || err.getReturnCode() != ERR_NOTIMPL) {
0429                             // The correction has been done or failed. This is the end.
0430                             index = -2;
0431                         }
0432                     } else {
0433                         index = -2;
0434                     }
0435                     ++index;
0436                 }
0437 
0438                 // Get last transaction id
0439                 int next = getDocument()->getTransactionToProcess(SKGDocument::UNDO);
0440 
0441                 // If this is the same transaction, it means that an action has been done outside the document ==> a refresh is needed
0442                 if (next == previous) {
0443                     emit refreshNeeded();
0444                 }
0445 
0446                 QApplication::restoreOverrideCursor();
0447             }
0448         }
0449     }
0450 }
0451 
0452