1 /* ============================================================
2 * QuiteRSS is a open-source cross-platform RSS/Atom news feeds reader
3 * Copyright (C) 2011-2020 QuiteRSS Team <quiterssteam@gmail.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 * ============================================================ */
18 #include "filterrulesdialog.h"
19 
20 #include "mainapplication.h"
21 #include "settings.h"
22 
FilterRulesDialog(QWidget * parent,int filterId,int feedId)23 FilterRulesDialog::FilterRulesDialog(QWidget *parent, int filterId, int feedId)
24   : Dialog(parent, Qt::WindowMinMaxButtonsHint)
25   , filterId_(filterId)
26 {
27   setWindowTitle(tr("Filter Rules"));
28   setMinimumHeight(300);
29 
30   feedsTree_ = new QTreeWidget(this);
31   feedsTree_->setObjectName("feedsTreeFR");
32   feedsTree_->setColumnCount(2);
33   feedsTree_->setColumnHidden(1, true);
34 #ifdef HAVE_QT5
35   feedsTree_->header()->setSectionsMovable(false);
36 #else
37   feedsTree_->header()->setMovable(false);
38 #endif
39 
40   itemNotChecked_ = false;
41 
42   QStringList treeItem;
43   treeItem << tr("Feeds") << "Id";
44   feedsTree_->setHeaderLabels(treeItem);
45 
46   treeItem.clear();
47   treeItem << tr("All Feeds") << "0";
48   QTreeWidgetItem *treeWidgetItem = new QTreeWidgetItem(treeItem);
49   treeWidgetItem->setCheckState(0, Qt::Unchecked);
50   feedsTree_->addTopLevelItem(treeWidgetItem);
51 
52   QSqlQuery q;
53   QQueue<int> parentIds;
54   parentIds.enqueue(0);
55   while (!parentIds.empty()) {
56     int parentId = parentIds.dequeue();
57     QString qStr = QString("SELECT text, id, image, xmlUrl FROM feeds WHERE parentId='%1' ORDER BY rowToParent").
58         arg(parentId);
59     q.exec(qStr);
60     while (q.next()) {
61       QString feedText = q.value(0).toString();
62       QString feedIdT = q.value(1).toString();
63       QByteArray byteArray = q.value(2).toByteArray();
64       QString xmlUrl = q.value(3).toString();
65 
66       treeItem.clear();
67       treeItem << feedText << feedIdT;
68       treeWidgetItem = new QTreeWidgetItem(treeItem);
69 
70       if ((feedId == feedIdT.toInt()) || (feedId == parentId))
71         treeWidgetItem->setCheckState(0, Qt::Checked);
72       else
73         treeWidgetItem->setCheckState(0, Qt::Unchecked);
74 
75       QPixmap iconItem;
76       if (xmlUrl.isEmpty()) {
77         iconItem.load(":/images/folder");
78       } else {
79         if (byteArray.isNull() || mainApp->mainWindow()->defaultIconFeeds_) {
80           iconItem.load(":/images/feed");
81         } else {
82           iconItem.loadFromData(QByteArray::fromBase64(byteArray));
83         }
84       }
85       treeWidgetItem->setIcon(0, iconItem);
86 
87       QList<QTreeWidgetItem *> treeItems =
88           feedsTree_->findItems(QString::number(parentId),
89                                 Qt::MatchFixedString | Qt::MatchRecursive,
90                                 1);
91       treeItems.at(0)->addChild(treeWidgetItem);
92       if (xmlUrl.isEmpty())
93         parentIds.enqueue(feedIdT.toInt());
94     }
95   }
96   feedsTree_->expandAll();
97 
98   if (feedId != -1) {
99     int rowCount = 0;
100     QTreeWidgetItem *childItem = feedsTree_->topLevelItem(0);
101     while (childItem) {
102       if (childItem->text(1).toInt() == feedId) break;
103       rowCount++;
104       childItem = feedsTree_->itemBelow(childItem);
105     }
106     feedsTree_->verticalScrollBar()->setValue(rowCount);
107     feedsTree_->topLevelItem(0)->setCheckState(0, Qt::Unchecked);
108   }
109   connect(feedsTree_, SIGNAL(itemChanged(QTreeWidgetItem*,int)),
110           this, SLOT(feedItemChanged(QTreeWidgetItem*,int)));
111 
112   filterName_ = new LineEdit(this);
113   QHBoxLayout *filterNamelayout = new QHBoxLayout();
114   filterNamelayout->addWidget(new QLabel(tr("Filter name:")));
115   filterNamelayout->addWidget(filterName_);
116 
117   matchComboBox_ = new QComboBox(this);
118 
119   QStringList itemList;
120   itemList << tr("Match all news") << tr("Match all conditions")
121            << tr("Match any condition");
122   matchComboBox_->addItems(itemList);
123   matchComboBox_->setCurrentIndex(1);
124   connect(matchComboBox_, SIGNAL(currentIndexChanged(int)),
125           this, SLOT(selectMatch(int)));
126 
127   QHBoxLayout *matchLayout = new QHBoxLayout();
128   matchLayout->addWidget(matchComboBox_);
129   matchLayout->addStretch();
130 
131   conditionScrollArea_ = new QScrollArea(this);
132   conditionScrollArea_->setFocusPolicy(Qt::NoFocus);
133   conditionScrollArea_->setWidgetResizable(true);
134 
135   conditionLayout_ = new QVBoxLayout();
136   conditionLayout_->setMargin(1);
137   conditionLayout_->setSpacing(1);
138   if (filterId_ == -1)
139     addCondition();
140 
141   conditionWidget_ = new QWidget(this);
142   conditionWidget_->setObjectName("infoWidgetFR");
143   conditionWidget_->setLayout(conditionLayout_);
144   conditionScrollArea_->setWidget(conditionWidget_);
145 
146   QVBoxLayout *splitterLayoutV1 = new QVBoxLayout();
147   splitterLayoutV1->setMargin(0);
148   splitterLayoutV1->addLayout(matchLayout);
149   splitterLayoutV1->addWidget(conditionScrollArea_, 1);
150 
151   QWidget *splitterWidget1 = new QWidget(this);
152   splitterWidget1->setMinimumWidth(400);
153   splitterWidget1->setLayout(splitterLayoutV1);
154 
155   actionsScrollArea_ = new QScrollArea(this);
156   actionsScrollArea_->setWidgetResizable(true);
157   actionsScrollArea_->setFocusPolicy(Qt::NoFocus);
158 
159   actionsLayout_ = new QVBoxLayout();
160   actionsLayout_->setMargin(1);
161   actionsLayout_->setSpacing(1);
162   if (filterId_ == -1)
163     addAction();
164 
165   actionsWidget_ = new QWidget(this);
166   actionsWidget_->setObjectName("actionsWidgetFR");
167   actionsWidget_->setLayout(actionsLayout_);
168   actionsScrollArea_->setWidget(actionsWidget_);
169 
170   QVBoxLayout *splitterLayoutV2 = new QVBoxLayout();
171   splitterLayoutV2->setMargin(0);
172   splitterLayoutV2->addWidget(new QLabel(tr("Perform these actions:")));
173   splitterLayoutV2->addWidget(actionsScrollArea_, 1);
174 
175   QWidget *splitterWidget2 = new QWidget(this);
176   splitterWidget2->setLayout(splitterLayoutV2);
177 
178   QSplitter *spliter = new QSplitter(Qt::Vertical, this);
179   spliter->setChildrenCollapsible(false);
180   spliter->addWidget(splitterWidget1);
181   spliter->addWidget(splitterWidget2);
182 
183   QVBoxLayout *rulesLayout = new QVBoxLayout();
184   rulesLayout->setMargin(0);
185   rulesLayout->addLayout(filterNamelayout);
186   rulesLayout->addWidget(spliter);
187 
188   QWidget *rulesWidget = new QWidget(this);
189   rulesWidget->setLayout(rulesLayout);
190 
191   QSplitter *mainSpliter = new QSplitter(this);
192   mainSpliter->setChildrenCollapsible(false);
193   mainSpliter->addWidget(rulesWidget);
194   mainSpliter->addWidget(feedsTree_);
195 
196   QLabel *iconWarning = new QLabel(this);
197   iconWarning->setPixmap(QPixmap(":/images/warning"));
198   textWarning_ = new QLabel(this);
199   QFont font = textWarning_->font();
200   font.setBold(true);
201   textWarning_->setFont(font);
202 
203   QHBoxLayout *warningLayout = new QHBoxLayout();
204   warningLayout->setMargin(0);
205   warningLayout->addWidget(iconWarning);
206   warningLayout->addWidget(textWarning_, 1);
207 
208   warningWidget_ = new QWidget(this);
209   warningWidget_->setLayout(warningLayout);
210   warningWidget_->setVisible(false);
211 
212   buttonsLayout->insertWidget(0, warningWidget_, 1);
213   buttonBox->addButton(QDialogButtonBox::Ok);
214   buttonBox->addButton(QDialogButtonBox::Cancel);
215   connect(buttonBox, SIGNAL(accepted()), this, SLOT(acceptDialog()));
216 
217   pageLayout->addWidget(mainSpliter);
218 
219   setData();
220 
221   filterName_->setFocus();
222   filterName_->selectAll();
223 
224   Settings settings;
225   restoreGeometry(settings.value("filterRulesDlg/geometry").toByteArray());
226 
227   connect(filterName_, SIGNAL(textChanged(QString)),
228           this, SLOT(filterNameChanged(QString)));
229   connect(this, SIGNAL(finished(int)), this, SLOT(closeDialog()));
230 }
231 
setData()232 void FilterRulesDialog::setData()
233 {
234   if (filterId_ == -1) return;
235 
236   QSqlQuery q;
237   QString qStr = QString("SELECT name, type, feeds FROM filters WHERE id=='%1'").
238       arg(filterId_);
239   q.exec(qStr);
240   if (q.next()) {
241     filterName_->setText(q.value(0).toString());
242 
243     matchComboBox_->setCurrentIndex(q.value(1).toInt());
244 
245     itemNotChecked_ = true;
246     QStringList strIdFeeds = q.value(2).toString().split(",", QString::SkipEmptyParts);
247     foreach (QString strIdFeed, strIdFeeds) {
248       QList<QTreeWidgetItem *> treeItems =
249           feedsTree_->findItems(strIdFeed,
250                                 Qt::MatchFixedString | Qt::MatchRecursive,
251                                 1);
252       if (treeItems.count())
253         treeItems.at(0)->setCheckState(0, Qt::Checked);
254     }
255     QTreeWidgetItem *treeItem = feedsTree_->itemBelow(feedsTree_->topLevelItem(0));
256     while (treeItem) {
257       if (treeItem->checkState(0) == Qt::Unchecked) {
258         feedsTree_->topLevelItem(0)->setCheckState(0, Qt::Unchecked);
259         break;
260       }
261       treeItem = feedsTree_->itemBelow(treeItem);
262     }
263     itemNotChecked_ = false;
264 
265     qStr = QString("SELECT field, condition, content "
266                    "FROM filterConditions WHERE idFilter=='%1' ORDER BY content").
267         arg(filterId_);
268     q.exec(qStr);
269     while (q.next()) {
270       ItemCondition *itemCondition = addCondition();
271       itemCondition->comboBox1_->setCurrentIndex(q.value(0).toInt());
272       itemCondition->comboBox2_->setCurrentIndex(q.value(1).toInt());
273       if (q.value(0).toInt() == 4)
274         itemCondition->comboBox3_->setCurrentIndex(q.value(2).toInt());
275       else
276         itemCondition->lineEdit_->setText(q.value(2).toString());
277     }
278     if (conditionLayout_->count() == 0)
279       addCondition();
280 
281     qStr = QString("SELECT action, params "
282                    "FROM filterActions WHERE idFilter=='%1'").
283         arg(filterId_);
284     q.exec(qStr);
285     while (q.next()) {
286       ItemAction *itemAction = addAction();
287       int action = q.value(0).toInt();
288       itemAction->comboBox1_->setCurrentIndex(action);
289       if (action == 3) {
290         int index = itemAction->comboBox2_->findData(q.value(1).toInt());
291         itemAction->comboBox2_->setCurrentIndex(index);
292       } else if (action == 4) {
293         itemAction->soundPathEdit_->setText(q.value(1).toString());
294       } else if (action == 5) {
295         itemAction->colorButton_->setToolTip(q.value(1).toString());
296         QPixmap pixmap(14, 14);
297         pixmap.fill(QColor(q.value(1).toString()));
298         itemAction->colorButton_->setIcon(pixmap);
299       }
300     }
301     if (actionsLayout_->count() == 0)
302       addAction();
303   }
304 }
305 
closeDialog()306 void FilterRulesDialog::closeDialog()
307 {
308   Settings settings;
309   settings.setValue("filterRulesDlg/geometry", saveGeometry());
310 }
311 
acceptDialog()312 void FilterRulesDialog::acceptDialog()
313 {
314   if (filterName_->text().isEmpty()) {
315     filterName_->setFocus();
316     textWarning_->setText(tr("Please enter name for the filter."));
317     warningWidget_->setVisible(true);
318     return;
319   }
320 
321   if (matchComboBox_->currentIndex() != 0) {
322     for (int i = 0; i < conditionLayout_->count()-2; i++) {
323       ItemCondition *itemCondition =
324           qobject_cast<ItemCondition*>(conditionLayout_->itemAt(i)->widget());
325       if ((itemCondition->comboBox1_->currentIndex() != 4) &&
326           itemCondition->lineEdit_->text().isEmpty()) {
327         itemCondition->lineEdit_->setFocus();
328         textWarning_->setText(tr("Please enter search condition for the news filter."));
329         warningWidget_->setVisible(true);
330         return;
331       }
332     }
333   }
334 
335   feedsTree_->expandAll();
336   QString strIdFeeds;
337   QTreeWidgetItem *treeItem = feedsTree_->itemBelow(feedsTree_->topLevelItem(0));
338   while (treeItem) {
339     if (treeItem->checkState(0) == Qt::Checked) {
340       strIdFeeds.append(",");
341       strIdFeeds.append(treeItem->text(1));
342     }
343     treeItem = feedsTree_->itemBelow(treeItem);
344   }
345   strIdFeeds.append(",");
346 
347   QSqlQuery q;
348   if (filterId_ == -1) {
349     QString qStr = QString("INSERT INTO filters (name, type, feeds) "
350                            "VALUES (?, ?, ?)");
351     q.prepare(qStr);
352     q.addBindValue(filterName_->text());
353     q.addBindValue(matchComboBox_->currentIndex());
354     q.addBindValue(strIdFeeds);
355     q.exec();
356 
357     filterId_ = q.lastInsertId().toInt();
358     qStr = QString("UPDATE filters SET num='%1' WHERE id=='%1'").
359         arg(filterId_);
360     q.exec(qStr);
361 
362     for (int i = 0; i < conditionLayout_->count()-2; i++) {
363       ItemCondition *itemCondition =
364           qobject_cast<ItemCondition*>(conditionLayout_->itemAt(i)->widget());
365       qStr = QString("INSERT INTO filterConditions "
366                      "(idFilter, field, condition, content) "
367                      "VALUES (?, ?, ?, ?)");
368       q.prepare(qStr);
369       q.addBindValue(filterId_);
370       q.addBindValue(itemCondition->comboBox1_->currentIndex());
371       q.addBindValue(itemCondition->comboBox2_->currentIndex());
372       if (itemCondition->comboBox1_->currentIndex() == 4)
373         q.addBindValue(itemCondition->comboBox3_->currentIndex());
374       else
375         q.addBindValue(itemCondition->lineEdit_->text());
376       q.exec();
377     }
378 
379     for (int i = 0; i < actionsLayout_->count()-2; i++) {
380       ItemAction *itemAction =
381           qobject_cast<ItemAction*>(actionsLayout_->itemAt(i)->widget());
382       qStr = QString("INSERT INTO filterActions "
383                      "(idFilter, action, params) "
384                      "VALUES (?, ?, ?)");
385       q.prepare(qStr);
386       q.addBindValue(filterId_);
387       q.addBindValue(itemAction->comboBox1_->currentIndex());
388       if (itemAction->comboBox1_->currentIndex() == 3)
389         q.addBindValue(itemAction->comboBox2_->itemData(itemAction->comboBox2_->currentIndex()));
390       else if (itemAction->comboBox1_->currentIndex() == 4)
391         q.addBindValue(itemAction->soundPathEdit_->text());
392       else if (itemAction->comboBox1_->currentIndex() == 5)
393         q.addBindValue(itemAction->colorButton_->toolTip());
394       else
395         q.addBindValue(0);
396       q.exec();
397     }
398   } else {
399     q.prepare("UPDATE filters SET name=?, type=?, feeds=? WHERE id=?");
400     q.addBindValue(filterName_->text());
401     q.addBindValue(matchComboBox_->currentIndex());
402     q.addBindValue(strIdFeeds);
403     q.addBindValue(filterId_);
404     q.exec();
405 
406     q.exec(QString("DELETE FROM filterConditions WHERE idFilter='%1'").arg(filterId_));
407     q.exec(QString("DELETE FROM filterActions WHERE idFilter='%1'").arg(filterId_));
408 
409     for (int i = 0; i < conditionLayout_->count()-2; i++) {
410       ItemCondition *itemCondition =
411           qobject_cast<ItemCondition*>(conditionLayout_->itemAt(i)->widget());
412       QString qStr = QString("INSERT INTO filterConditions "
413                              "(idFilter, field, condition, content) "
414                              "VALUES (?, ?, ?, ?)");
415       q.prepare(qStr);
416       q.addBindValue(filterId_);
417       q.addBindValue(itemCondition->comboBox1_->currentIndex());
418       q.addBindValue(itemCondition->comboBox2_->currentIndex());
419       if (itemCondition->comboBox1_->currentIndex() == 4)
420         q.addBindValue(itemCondition->comboBox3_->currentIndex());
421       else
422         q.addBindValue(itemCondition->lineEdit_->text());
423       q.exec();
424     }
425 
426     for (int i = 0; i < actionsLayout_->count()-2; i++) {
427       ItemAction *itemAction =
428           qobject_cast<ItemAction*>(actionsLayout_->itemAt(i)->widget());
429       QString qStr = QString("INSERT INTO filterActions "
430                              "(idFilter, action, params) "
431                              "VALUES (?, ?, ?)");
432       q.prepare(qStr);
433       q.addBindValue(filterId_);
434       q.addBindValue(itemAction->comboBox1_->currentIndex());
435       if (itemAction->comboBox1_->currentIndex() == 3)
436         q.addBindValue(itemAction->comboBox2_->itemData(itemAction->comboBox2_->currentIndex()));
437       else if (itemAction->comboBox1_->currentIndex() == 4)
438         q.addBindValue(itemAction->soundPathEdit_->text());
439       else if (itemAction->comboBox1_->currentIndex() == 5)
440         q.addBindValue(itemAction->colorButton_->toolTip());
441       else
442         q.addBindValue(0);
443       q.exec();
444     }
445   }
446   accept();
447 }
448 
filterNameChanged(const QString &)449 void FilterRulesDialog::filterNameChanged(const QString &)
450 {
451   warningWidget_->setVisible(false);
452 }
453 
selectMatch(int index)454 void FilterRulesDialog::selectMatch(int index)
455 {
456   if (index == 0) {
457     conditionWidget_->setEnabled(false);
458   } else {
459     conditionWidget_->setEnabled(true);
460   }
461 }
462 
feedItemChanged(QTreeWidgetItem * item,int column)463 void FilterRulesDialog::feedItemChanged(QTreeWidgetItem *item, int column)
464 {
465   if ((column != 0) || itemNotChecked_) return;
466 
467   itemNotChecked_ = true;
468   if (item->checkState(0) == Qt::Unchecked) {
469     setCheckStateItem(item, Qt::Unchecked);
470 
471     QTreeWidgetItem *parentItem = item->parent();
472     while (parentItem) {
473       parentItem->setCheckState(0, Qt::Unchecked);
474       parentItem = parentItem->parent();
475     }
476   } else {
477     setCheckStateItem(item, Qt::Checked);
478   }
479   itemNotChecked_ = false;
480 }
481 
setCheckStateItem(QTreeWidgetItem * item,Qt::CheckState state)482 void FilterRulesDialog::setCheckStateItem(QTreeWidgetItem *item, Qt::CheckState state)
483 {
484   for (int i = 0; i < item->childCount(); ++i) {
485     QTreeWidgetItem *childItem = item->child(i);
486     childItem->setCheckState(0, state);
487     setCheckStateItem(childItem, state);
488   }
489 }
490 
addCondition()491 ItemCondition *FilterRulesDialog::addCondition()
492 {
493   conditionLayout_->removeItem(conditionLayout_->itemAt(conditionLayout_->count()-1));
494   conditionLayout_->removeItem(conditionLayout_->itemAt(conditionLayout_->count()-1));
495   ItemCondition *itemCondition = new ItemCondition(this);
496   conditionLayout_->addWidget(itemCondition);
497   conditionLayout_->addStretch();
498   conditionLayout_->addSpacing(25);
499   connect(itemCondition->addButton_, SIGNAL(clicked()), this, SLOT(addCondition()));
500   connect(itemCondition, SIGNAL(signalDeleteCondition(ItemCondition*)),
501           this, SLOT(deleteCondition(ItemCondition*)));
502 
503   QScrollBar *scrollBar = conditionScrollArea_->verticalScrollBar();
504   scrollBar->setValue(scrollBar->maximum() -
505                       scrollBar->minimum() +
506                       scrollBar->pageStep());
507   itemCondition->lineEdit_->setFocus();
508   connect(itemCondition->lineEdit_, SIGNAL(textChanged(QString)),
509           this, SLOT(filterNameChanged(QString)));
510   connect(itemCondition->comboBox1_, SIGNAL(currentIndexChanged(QString)),
511           this, SLOT(filterNameChanged(QString)));
512   return itemCondition;
513 }
514 
deleteCondition(ItemCondition * item)515 void FilterRulesDialog::deleteCondition(ItemCondition *item)
516 {
517   delete item;
518   if (conditionLayout_->count() == 2) {
519     addCondition();
520   }
521 }
522 
addAction()523 ItemAction *FilterRulesDialog::addAction()
524 {
525   actionsLayout_->removeItem(actionsLayout_->itemAt(actionsLayout_->count()-1));
526   actionsLayout_->removeItem(actionsLayout_->itemAt(actionsLayout_->count()-1));
527   ItemAction *itemAction = new ItemAction(this);
528 
529   QSqlQuery q;
530   q.exec("SELECT id, name, image FROM labels ORDER BY num");
531   while (q.next()) {
532     int idLabel = q.value(0).toInt();
533     QString nameLabel = q.value(1).toString();
534     if ((idLabel <= 6) && (MainWindow::nameLabels().at(idLabel-1) == nameLabel)) {
535       nameLabel = MainWindow::trNameLabels().at(idLabel-1);
536     }
537     QByteArray byteArray = q.value(2).toByteArray();
538     QPixmap imageLabel;
539     if (!byteArray.isNull())
540       imageLabel.loadFromData(byteArray);
541 
542     itemAction->comboBox2_->addItem(QIcon(imageLabel), nameLabel, idLabel);
543   }
544 
545   actionsLayout_->addWidget(itemAction);
546   actionsLayout_->addStretch();
547   actionsLayout_->addSpacing(25);
548   connect(itemAction, SIGNAL(signalPlaySound(QString)),
549           parent(), SLOT(slotPlaySound(QString)));
550   connect(itemAction->addButton_, SIGNAL(clicked()), this, SLOT(addAction()));
551   connect(itemAction, SIGNAL(signalDeleteAction(ItemAction*)),
552           this, SLOT(deleteAction(ItemAction*)));
553   return itemAction;
554 }
555 
deleteAction(ItemAction * item)556 void FilterRulesDialog::deleteAction(ItemAction *item)
557 {
558   delete item;
559   if (actionsLayout_->count() == 2) {
560     addAction();
561   }
562 }
563