1 /***************************************************************************
2  * SPDX-FileCopyrightText: 2021 S. MANKOWSKI stephane@mankowski.fr
3  * SPDX-FileCopyrightText: 2021 G. DE BURE support@mankowski.fr
4  * SPDX-License-Identifier: GPL-3.0-or-later
5  ***************************************************************************/
6 /** @file
7  * This file is plugin for advice.
8  *
9  * @author Stephane MANKOWSKI / Guillaume DE BURE
10  */
11 #include "skgadviceboardwidget.h"
12 
13 #include <qdom.h>
14 #include <qlayoutitem.h>
15 #include <qmenu.h>
16 #include <qpushbutton.h>
17 #include <qtoolbutton.h>
18 
19 #include <klocalizedstring.h>
20 
21 #include "skgdocument.h"
22 #include "skginterfaceplugin.h"
23 #include "skgmainpanel.h"
24 #include "skgservices.h"
25 #include "skgtraces.h"
26 #include "skgtransactionmng.h"
27 
SKGAdviceBoardWidget(QWidget * iParent,SKGDocument * iDocument)28 SKGAdviceBoardWidget::SKGAdviceBoardWidget(QWidget* iParent, SKGDocument* iDocument)
29     : SKGBoardWidget(iParent, iDocument, i18nc("Dashboard widget title", "Advices")), m_maxAdvice(7), m_refreshNeeded(true), m_refresh(nullptr), m_inapplyall(false)
30 {
31     SKGTRACEINFUNC(10)
32 
33     // Create menu
34     setContextMenuPolicy(Qt::ActionsContextMenu);
35 
36     auto g = new QWidget(this);
37     m_layout = new QFormLayout(g);
38     m_layout->setContentsMargins(0, 0, 0, 0);
39     m_layout->setObjectName(QStringLiteral("Slayout"));
40     m_layout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
41     m_layout->setHorizontalSpacing(1);
42     m_layout->setVerticalSpacing(1);
43     setMainWidget(g);
44 
45     // menu
46     auto menuResetAdvice = new QAction(SKGServices::fromTheme(QStringLiteral("edit-undo")), i18nc("Noun, a user action", "Activate all advice"), this);
47     connect(menuResetAdvice, &QAction::triggered, this, &SKGAdviceBoardWidget::activateAllAdvice);
48     addAction(menuResetAdvice);
49 
50     auto sep = new QAction(this);
51     sep->setSeparator(true);
52     addAction(sep);
53 
54     m_menuAuto = new QAction(i18nc("Noun, a type of refresh for advice", "Automatic refresh"), this);
55     m_menuAuto->setCheckable(true);
56     m_menuAuto->setChecked(true);
57     connect(m_menuAuto, &QAction::triggered, this, &SKGAdviceBoardWidget::dataModifiedNotForce);
58     addAction(m_menuAuto);
59 
60     // Refresh
61     connect(getDocument(), &SKGDocument::transactionSuccessfullyEnded, this, &SKGAdviceBoardWidget::dataModifiedNotForce, Qt::QueuedConnection);
62     connect(SKGMainPanel::getMainPanel(), &SKGMainPanel::currentPageChanged, this, &SKGAdviceBoardWidget::pageChanged, Qt::QueuedConnection);
63     connect(this, &SKGAdviceBoardWidget::refreshNeeded, this, [ = ]() {
64         this->dataModifiedNotForce();
65     }, Qt::QueuedConnection);
66 }
67 
~SKGAdviceBoardWidget()68 SKGAdviceBoardWidget::~SKGAdviceBoardWidget()
69 {
70     SKGTRACEINFUNC(10)
71     m_menuAuto = nullptr;
72     m_refresh = nullptr;
73 }
74 
getState()75 QString SKGAdviceBoardWidget::getState()
76 {
77     QDomDocument doc(QStringLiteral("SKGML"));
78     doc.setContent(SKGBoardWidget::getState());
79     QDomElement root = doc.documentElement();
80 
81     root.setAttribute(QStringLiteral("maxAdvice"), SKGServices::intToString(m_maxAdvice));
82     root.setAttribute(QStringLiteral("automatic"), (m_menuAuto->isChecked() ? QStringLiteral("Y") : QStringLiteral("N")));
83     return doc.toString();
84 }
85 
setState(const QString & iState)86 void SKGAdviceBoardWidget::setState(const QString& iState)
87 {
88     SKGBoardWidget::setState(iState);
89 
90     QDomDocument doc(QStringLiteral("SKGML"));
91     doc.setContent(iState);
92     QDomElement root = doc.documentElement();
93 
94     QString maxAdviceS = root.attribute(QStringLiteral("maxAdvice"));
95     if (maxAdviceS.isEmpty()) {
96         maxAdviceS = '7';
97     }
98     m_maxAdvice = SKGServices::stringToInt(maxAdviceS);
99 
100     QString automatic = root.attribute(QStringLiteral("automatic"));
101     if (automatic.isEmpty()) {
102         automatic = 'Y';
103     }
104 
105     if (m_menuAuto != nullptr) {
106         bool previous = m_menuAuto->blockSignals(true);
107         m_menuAuto->setChecked(automatic == QStringLiteral("Y"));
108         m_menuAuto->blockSignals(previous);
109     }
110 
111     dataModifiedForce();
112 }
113 
pageChanged()114 void SKGAdviceBoardWidget::pageChanged()
115 {
116     if (m_refreshNeeded) {
117         dataModifiedNotForce();
118     }
119 }
120 
dataModifiedNotForce()121 void SKGAdviceBoardWidget::dataModifiedNotForce()
122 {
123     dataModified(false);
124 }
125 
dataModifiedForce()126 void SKGAdviceBoardWidget::dataModifiedForce()
127 {
128     dataModified(true);
129 }
130 
dataModified(bool iForce)131 void SKGAdviceBoardWidget::dataModified(bool iForce)
132 {
133     SKGTRACEINFUNC(10)
134     SKGTabPage* page = SKGTabPage::parentTabPage(this);
135     if (m_inapplyall || (!iForce && ((page != nullptr && page != SKGMainPanel::getMainPanel()->currentPage()) || !m_menuAuto->isChecked()))) {
136         m_refreshNeeded = true;
137         if (m_refresh != nullptr) {
138             m_refresh->show();
139         }
140         return;
141     }
142     QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
143     m_refreshNeeded = false;
144 
145     // Remove all item of the layout
146     while (m_layout->count() != 0) {
147         QLayoutItem* child = m_layout->takeAt(0);
148         if (child != nullptr) {
149             QWidget* w = child->widget();
150             delete w;
151             delete child;
152         }
153     }
154 
155     // Get list of advice
156     double elapse = SKGServices::getMicroTime();
157     SKGAdviceList globalAdviceList = SKGMainPanel::getMainPanel()->getAdvice();
158     elapse = SKGServices::getMicroTime() - elapse;
159 
160     // Get list of ignored advice
161     QString currentMonth = QDate::currentDate().toString(QStringLiteral("yyyy-MM"));
162     QStringList ignoredAdvice = getDocument()->getParameters(QStringLiteral("advice"), "t_value='I' OR t_value='I_" % currentMonth % '\'');
163     if (elapse > 3000 && m_menuAuto->isChecked() && !ignoredAdvice.contains(QStringLiteral("skgadviceboardwidget_verylong"))) {
164         SKGAdvice ad;
165         ad.setUUID(QStringLiteral("skgadviceboardwidget_verylong"));
166         ad.setPriority(2);
167         ad.setShortMessage(i18nc("Advice on making the best (short)", "Advice are very long to compute"));
168         ad.setLongMessage(i18nc("Advice on making the best (long)", "To improve performances, you should switch the widget in 'Manual refresh' (see contextual menu)."));
169         globalAdviceList.push_back(ad);
170     }
171 
172     // Fill layout
173     int nbDisplayedAdvice = 0;
174     int nb = globalAdviceList.count();
175     m_recommendedActions.clear();
176     for (int i = 0; i < m_maxAdvice && i < nb; ++i) {
177         // Get advice
178         const SKGAdvice& ad = globalAdviceList.at(i);
179 
180         // Create icon
181         QString iconName = (ad.getPriority() == -1 ? QStringLiteral("dialog-information") : ad.getPriority() >= 8 ? QStringLiteral("security-low") : ad.getPriority() <= 4 ? QStringLiteral("security-high") : QStringLiteral("security-medium"));
182         QString toolTipString = i18n("<p><b>Priority %1:</b>%2</p>", SKGServices::intToString(ad.getPriority()), ad.getLongMessage());
183 
184         // Add ignored action
185         SKGAdvice::SKGAdviceActionList autoCorrections = ad.getAutoCorrections();
186         {
187             SKGAdvice::SKGAdviceAction dismiss;
188             dismiss.Title = i18nc("Dismiss an advice provided", "Dismiss");
189             dismiss.IconName = QStringLiteral("edit-delete");
190             dismiss.IsRecommended = false;
191             autoCorrections.push_back(dismiss);
192         }
193         {
194             SKGAdvice::SKGAdviceAction dismiss;
195             dismiss.Title = i18nc("Dismiss an advice provided", "Dismiss during current month");
196             dismiss.IconName = QStringLiteral("edit-delete");
197             dismiss.IsRecommended = false;
198             autoCorrections.push_back(dismiss);
199         }
200         {
201             SKGAdvice::SKGAdviceAction dismiss;
202             dismiss.Title = i18nc("Dismiss an advice provided", "Dismiss this kind");
203             dismiss.IconName = QStringLiteral("edit-delete");
204             dismiss.IsRecommended = false;
205             autoCorrections.push_back(dismiss);
206         }
207         {
208             SKGAdvice::SKGAdviceAction dismiss;
209             dismiss.Title = i18nc("Dismiss an advice provided", "Dismiss this kind during current month");
210             dismiss.IconName = QStringLiteral("edit-delete");
211             dismiss.IsRecommended = false;
212             autoCorrections.push_back(dismiss);
213         }
214 
215         int nbSolution = autoCorrections.count();
216 
217         // Build button
218         QStringList overlays;
219         overlays.push_back(nbSolution > 2 ? QStringLiteral("system-run") : QStringLiteral("edit-delete"));
220         auto icon = new QToolButton(this);
221         icon->setObjectName(ad.getUUID());
222         icon->setIcon(SKGServices::fromTheme(iconName, overlays));
223         icon->setIconSize(QSize(24, 24));
224         icon->setMaximumSize(QSize(24, 24));
225         icon->setCursor(Qt::PointingHandCursor);
226         icon->setAutoRaise(true);
227 
228         auto menu = new QMenu(this);
229         menu->setIcon(icon->icon());
230         for (int k = 0; k < nbSolution; ++k) {
231             SKGAdvice::SKGAdviceAction adviceAction = autoCorrections.at(k);
232             QString actionText = adviceAction.Title;
233             QAction* action = SKGMainPanel::getMainPanel()->getGlobalAction(QString(actionText).remove(QStringLiteral("skg://")), false);
234             QAction* act;
235             if (action != nullptr) {
236                 // This is an action
237                 act = menu->addAction(action->icon(), action->text(), SKGMainPanel::getMainPanel(), static_cast<bool (SKGMainPanel::*)()>(&SKGMainPanel::openPage));
238                 act->setData(actionText);
239             } else {
240                 // This is a text
241                 act = menu->addAction(SKGServices::fromTheme(adviceAction.IconName.isEmpty() ?  QStringLiteral("system-run") : adviceAction.IconName), autoCorrections.at(k).Title, this, &SKGAdviceBoardWidget::adviceClicked);
242                 if (act != nullptr) {
243                     act->setProperty("id", ad.getUUID());
244                     act->setProperty("solution", k < nbSolution - 4 ? k : k - nbSolution);
245                 }
246             }
247 
248             if ((act != nullptr) && adviceAction.IsRecommended) {
249                 act->setToolTip(act->text());
250                 act->setText(act->text() % i18nc("To recommend this action", " (recommended)"));
251                 m_recommendedActions.append(act);
252             }
253         }
254 
255         icon->setMenu(menu);
256         icon->setPopupMode(QToolButton::InstantPopup);
257 
258         icon->setToolTip(toolTipString);
259 
260         // Create text
261         auto label = new QLabel(this);
262         label->setText(ad.getShortMessage());
263         label->setToolTip(toolTipString);
264 
265         // Add them
266         m_layout->addRow(icon, label);
267 
268         ++nbDisplayedAdvice;
269     }
270 
271     // Add apply all recommended actions
272     QPushButton*  apply = nullptr;
273     int nb2 = m_recommendedActions.count();
274     if (nb2 != 0) {
275         apply = new QPushButton(this);
276         apply->setIcon(SKGServices::fromTheme(QStringLiteral("games-solve")));
277         apply->setIconSize(QSize(22, 22));
278         apply->setMaximumSize(QSize(22, 22));
279         apply->setCursor(Qt::PointingHandCursor);
280         QString ToolTip;
281         for (int i = 0; i < nb2; ++i) {
282             if (i > 0) {
283                 ToolTip += '\n';
284             }
285             ToolTip += m_recommendedActions.at(i)->toolTip();
286         }
287         apply->setToolTip(ToolTip);
288         connect(apply, &QPushButton::clicked, this, &SKGAdviceBoardWidget::applyRecommended, Qt::QueuedConnection);
289     }
290 
291     // Add more
292     if (nb > m_maxAdvice) {
293         auto more = new QPushButton(this);
294         more->setIcon(SKGServices::fromTheme(QStringLiteral("arrow-down-double")));
295         more->setIconSize(QSize(22, 22));
296         more->setMaximumSize(QSize(22, 22));
297         more->setCursor(Qt::PointingHandCursor);
298         more->setToolTip(i18nc("Information message", "Display all advices"));
299         connect(more, &QPushButton::clicked, this, &SKGAdviceBoardWidget::moreAdvice, Qt::QueuedConnection);
300 
301         if (apply != nullptr) {
302             m_layout->addRow(more, apply);
303             apply = nullptr;
304         } else {
305             m_layout->addRow(more, new QLabel(this));
306         }
307     } else if (nbDisplayedAdvice > 7) {
308         // Add less
309         auto less = new QPushButton(this);
310         less->setIcon(SKGServices::fromTheme(QStringLiteral("arrow-up-double")));
311         less->setIconSize(QSize(22, 22));
312         less->setMaximumSize(QSize(22, 22));
313         less->setCursor(Qt::PointingHandCursor);
314         less->setToolTip(i18nc("Information message", "Display less advices"));
315         connect(less, &QPushButton::clicked, this, &SKGAdviceBoardWidget::lessAdvice, Qt::QueuedConnection);
316         if (apply != nullptr) {
317             m_layout->addRow(less, apply);
318             apply = nullptr;
319         } else {
320             m_layout->addRow(less, new QLabel(this));
321         }
322     }
323 
324     if (apply != nullptr) {
325         m_layout->addRow(apply, new QLabel(this));
326     }
327 
328     // Add manual refresh button
329     m_refresh = new QPushButton(this);
330     m_refresh->setIcon(SKGServices::fromTheme(QStringLiteral("view-refresh")));
331     m_refresh->setIconSize(QSize(22, 22));
332     m_refresh->setMaximumSize(QSize(22, 22));
333     m_refresh->setCursor(Qt::PointingHandCursor);
334     m_refresh->setToolTip(i18nc("Information message", "Refresh advices"));
335     m_refresh->hide();
336     connect(m_refresh, &QPushButton::clicked, this, &SKGAdviceBoardWidget::dataModifiedForce, Qt::QueuedConnection);
337 
338     m_layout->addRow(m_refresh, new QLabel(this));
339 
340     QApplication::restoreOverrideCursor();
341 }
342 
moreAdvice()343 void SKGAdviceBoardWidget::moreAdvice()
344 {
345     m_maxAdvice = 9999999;
346     dataModifiedForce();
347 }
348 
lessAdvice()349 void SKGAdviceBoardWidget::lessAdvice()
350 {
351     m_maxAdvice = 7;
352     dataModifiedForce();
353 }
354 
applyRecommended()355 void SKGAdviceBoardWidget::applyRecommended()
356 {
357     SKGError err;
358     SKGBEGINTRANSACTION(*getDocument(), i18nc("Noun, name of the user action", "Apply all recommended corrections"), err)
359     m_inapplyall = true;
360     int nb = m_recommendedActions.count();
361     for (int i = 0; i < nb; ++i) {
362         m_recommendedActions.at(i)->trigger();
363     }
364     m_inapplyall = false;
365 }
366 
activateAllAdvice()367 void SKGAdviceBoardWidget::activateAllAdvice()
368 {
369     SKGError err;
370     {
371         SKGBEGINTRANSACTION(*getDocument(), i18nc("Noun, name of the user action", "Activate all advice"), err)
372         err = getDocument()->executeSqliteOrder(QStringLiteral("DELETE FROM parameters WHERE t_uuid_parent='advice'"));
373     }
374 
375     // status bar
376     IFOKDO(err, SKGError(0, i18nc("Successful message after an user action", "Advice activated.")))
377     else {
378         err.addError(ERR_FAIL, i18nc("Error message",  "Advice activation failed"));
379     }
380 
381     SKGMainPanel::displayErrorMessage(err);
382 }
383 
adviceClicked()384 void SKGAdviceBoardWidget::adviceClicked()
385 {
386     // Get advice identifier
387     auto* act = qobject_cast< QAction* >(sender());
388     if (act != nullptr) {
389         QString uuid = act->property("id").toString();
390         if (!uuid.isEmpty()) {
391             // Get solution clicker
392             int solution = sender()->property("solution").toInt();
393 
394             if (solution < 0) {
395                 // We have to ignore this advice
396                 SKGError err;
397                 {
398                     SKGBEGINLIGHTTRANSACTION(*getDocument(), i18nc("Noun, name of the user action", "Dismiss advice"), err)
399                     QString currentMonth = QDate::currentDate().toString(QStringLiteral("yyyy-MM"));
400 
401                     // Create dismiss
402                     if (solution == -1 || solution == -2) {
403                         uuid = SKGServices::splitCSVLine(uuid, '|').at(0);
404                     }
405                     IFOKDO(err, getDocument()->setParameter(uuid, solution == -2 || solution == -4 ? QStringLiteral("I") :
406                                                             QString("I_" % currentMonth), QVariant(), QStringLiteral("advice")))
407 
408                     // Delete useless dismiss
409                     IFOKDO(err, getDocument()->executeSqliteOrder("DELETE FROM parameters WHERE t_uuid_parent='advice' AND t_value like 'I_ % ' AND t_value!='I_" % currentMonth % '\''))
410                 }
411 
412                 // status bar
413                 IFOKDO(err, SKGError(0, i18nc("Successful message after an user action", "Advice dismissed.")))
414                 else {
415                     err.addError(ERR_FAIL, i18nc("Error message",  "Advice dismiss failed"));
416                 }
417             } else {
418                 // Get last transaction id
419                 int previous = getDocument()->getTransactionToProcess(SKGDocument::UNDO);
420 
421                 // Execute the advice correction on all plugin
422                 QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
423                 int index = 0;
424                 while (index >= 0) {
425                     SKGInterfacePlugin* plugin = SKGMainPanel::getMainPanel()->getPluginByIndex(index);
426                     if (plugin != nullptr) {
427                         SKGError err = plugin->executeAdviceCorrection(uuid, solution);
428                         if (!err || err.getReturnCode() != ERR_NOTIMPL) {
429                             // The correction has been done or failed. This is the end.
430                             index = -2;
431                         }
432                     } else {
433                         index = -2;
434                     }
435                     ++index;
436                 }
437 
438                 // Get last transaction id
439                 int next = getDocument()->getTransactionToProcess(SKGDocument::UNDO);
440 
441                 // If this is the same transaction, it means that an action has been done outside the document ==> a refresh is needed
442                 if (next == previous) {
443                     emit refreshNeeded();
444                 }
445 
446                 QApplication::restoreOverrideCursor();
447             }
448         }
449     }
450 }
451 
452 
453