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