444afd1c2c6f29afe016454a4acd3d249fd7a4da
[quassel.git] / src / qtui / settingspages / corehighlightsettingspage.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2018 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
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 2 of the License, or     *
8  *   (at your option) version 3.                                           *
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, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #include <QHeaderView>
22 #include <QTableWidget>
23
24 #include "client.h"
25 #include "corehighlightsettingspage.h"
26 #include "qtui.h"
27
28 CoreHighlightSettingsPage::CoreHighlightSettingsPage(QWidget *parent)
29     : SettingsPage(tr("Interface"), tr("Remote Highlights"), parent)
30 {
31     ui.setupUi(this);
32
33     setupRuleTable(ui.highlightTable);
34     setupRuleTable(ui.ignoredTable);
35
36     ui.highlightNicksComboBox->addItem(tr("All Nicks from Identity"), QVariant(HighlightRuleManager::AllNicks));
37     ui.highlightNicksComboBox->addItem(tr("Current Nick"), QVariant(HighlightRuleManager::CurrentNick));
38     ui.highlightNicksComboBox->addItem(tr("None"), QVariant(HighlightRuleManager::NoNick));
39
40     coreConnectionStateChanged(Client::isConnected()); // need a core connection!
41     connect(Client::instance(), SIGNAL(coreConnectionStateChanged(bool)), this, SLOT(coreConnectionStateChanged(bool)));
42
43     connect(ui.highlightAdd, SIGNAL(clicked(bool)), this, SLOT(addNewHighlightRow()));
44     connect(ui.highlightRemove, SIGNAL(clicked(bool)), this, SLOT(removeSelectedHighlightRows()));
45     connect(ui.highlightImport, SIGNAL(clicked(bool)), this, SLOT(importRules()));
46
47     connect(ui.ignoredAdd, SIGNAL(clicked(bool)), this, SLOT(addNewIgnoredRow()));
48     connect(ui.ignoredRemove, SIGNAL(clicked(bool)), this, SLOT(removeSelectedIgnoredRows()));
49
50     // TODO: search for a better signal (one that emits everytime a selection has been changed for one item)
51     connect(ui.highlightTable,
52             SIGNAL(itemClicked(QTableWidgetItem * )),
53             this,
54             SLOT(selectHighlightRow(QTableWidgetItem * )));
55     connect(ui.ignoredTable,
56             SIGNAL(itemClicked(QTableWidgetItem * )),
57             this,
58             SLOT(selectIgnoredRow(QTableWidgetItem * )));
59
60     // Update the "Case sensitive" checkbox
61     connect(ui.highlightNicksComboBox,
62             SIGNAL(currentIndexChanged(int)),
63             this,
64             SLOT(highlightNicksChanged(int)));
65
66     connect(ui.highlightNicksComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(widgetHasChanged()));
67     connect(ui.nicksCaseSensitive, SIGNAL(clicked(bool)), this, SLOT(widgetHasChanged()));
68
69     connect(ui.highlightAdd, SIGNAL(clicked()), this, SLOT(widgetHasChanged()));
70     connect(ui.highlightRemove, SIGNAL(clicked()), this, SLOT(widgetHasChanged()));
71
72     connect(ui.ignoredAdd, SIGNAL(clicked()), this, SLOT(widgetHasChanged()));
73     connect(ui.ignoredRemove, SIGNAL(clicked()), this, SLOT(widgetHasChanged()));
74
75     connect(ui.highlightTable,
76             SIGNAL(itemChanged(QTableWidgetItem * )),
77             this,
78             SLOT(highlightTableChanged(QTableWidgetItem * )));
79
80     connect(ui.ignoredTable,
81             SIGNAL(itemChanged(QTableWidgetItem * )),
82             this,
83             SLOT(ignoredTableChanged(QTableWidgetItem * )));
84
85     connect(Client::instance(), SIGNAL(connected()), this, SLOT(clientConnected()));
86 }
87
88 void CoreHighlightSettingsPage::coreConnectionStateChanged(bool state)
89 {
90     setEnabled(state);
91     if (state) {
92         load();
93     } else {
94         revert();
95     }
96 }
97
98 void CoreHighlightSettingsPage::setupRuleTable(QTableWidget *table) const
99 {
100     table->verticalHeader()->hide();
101     table->setShowGrid(false);
102
103     table->horizontalHeaderItem(CoreHighlightSettingsPage::RegExColumn)->setToolTip(
104         tr("<b>RegEx</b>: This option determines if the highlight rule should be interpreted as a <b>regular expression</b> or just as a keyword."));
105     table->horizontalHeaderItem(CoreHighlightSettingsPage::RegExColumn)->setWhatsThis(
106         tr("<b>RegEx</b>: This option determines if the highlight rule should be interpreted as a <b>regular expression</b> or just as a keyword."));
107
108     table->horizontalHeaderItem(CoreHighlightSettingsPage::CsColumn)->setToolTip(
109         tr("<b>CS</b>: This option determines if the highlight rule should be interpreted <b>case sensitive</b>."));
110     table->horizontalHeaderItem(CoreHighlightSettingsPage::CsColumn)->setWhatsThis(
111         tr("<b>CS</b>: This option determines if the highlight rule should be interpreted <b>case sensitive</b>."));
112
113     table->horizontalHeaderItem(CoreHighlightSettingsPage::ChanColumn)->setToolTip(
114         tr("<b>Channel</b>: This regular expression determines for which <b>channels</b> the highlight rule works. Leave blank to match any channel. Put <b>!</b> in the beginning to negate. Case insensitive."));
115     table->horizontalHeaderItem(CoreHighlightSettingsPage::ChanColumn)->setWhatsThis(
116         tr("<b>Channel</b>: This regular expression determines for which <b>channels</b> the highlight rule works. Leave blank to match any channel. Put <b>!</b> in the beginning to negate. Case insensitive."));
117
118 #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
119     table->horizontalHeader()->setResizeMode(CoreHighlightSettingsPage::EnableColumn, QHeaderView::ResizeToContents);
120     table->horizontalHeader()->setResizeMode(CoreHighlightSettingsPage::NameColumn, QHeaderView::Stretch);
121     table->horizontalHeader()->setResizeMode(CoreHighlightSettingsPage::RegExColumn, QHeaderView::ResizeToContents);
122     table->horizontalHeader()->setResizeMode(CoreHighlightSettingsPage::CsColumn, QHeaderView::ResizeToContents);
123     table->horizontalHeader()->setResizeMode(CoreHighlightSettingsPage::ChanColumn, QHeaderView::ResizeToContents);
124 #else
125     table->horizontalHeader()->setSectionResizeMode(CoreHighlightSettingsPage::EnableColumn, QHeaderView::ResizeToContents);
126     table->horizontalHeader()->setSectionResizeMode(CoreHighlightSettingsPage::NameColumn, QHeaderView::Stretch);
127     table->horizontalHeader()->setSectionResizeMode(CoreHighlightSettingsPage::RegExColumn, QHeaderView::ResizeToContents);
128     table->horizontalHeader()->setSectionResizeMode(CoreHighlightSettingsPage::CsColumn, QHeaderView::ResizeToContents);
129     table->horizontalHeader()->setSectionResizeMode(CoreHighlightSettingsPage::ChanColumn, QHeaderView::ResizeToContents);
130 #endif
131 }
132
133 void CoreHighlightSettingsPage::clientConnected()
134 {
135     connect(Client::highlightRuleManager(), SIGNAL(updated()), SLOT(revert()));
136 }
137
138 void CoreHighlightSettingsPage::revert()
139 {
140     if (!hasChanged())
141         return;
142
143     setChangedState(false);
144     load();
145 }
146
147 bool CoreHighlightSettingsPage::hasDefaults() const
148 {
149     return true;
150 }
151
152 void CoreHighlightSettingsPage::defaults()
153 {
154     int highlightNickType = HighlightRuleManager::HighlightNickType::CurrentNick;
155     int defaultIndex = ui.highlightNicksComboBox->findData(QVariant(highlightNickType));
156     ui.highlightNicksComboBox->setCurrentIndex(defaultIndex);
157     ui.nicksCaseSensitive->setChecked(false);
158     emptyHighlightTable();
159     emptyIgnoredTable();
160
161     widgetHasChanged();
162 }
163
164 void CoreHighlightSettingsPage::addNewHighlightRow(bool enable, const QString &name, bool regex, bool cs,
165                                                    const QString &sender, const QString &chanName, bool self)
166 {
167     ui.highlightTable->setRowCount(ui.highlightTable->rowCount() + 1);
168
169     auto *nameItem = new QTableWidgetItem(name);
170
171     auto *regexItem = new QTableWidgetItem("");
172     if (regex)
173         regexItem->setCheckState(Qt::Checked);
174     else
175         regexItem->setCheckState(Qt::Unchecked);
176     regexItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
177
178     auto *csItem = new QTableWidgetItem("");
179     if (cs)
180         csItem->setCheckState(Qt::Checked);
181     else
182         csItem->setCheckState(Qt::Unchecked);
183     csItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
184
185     auto *enableItem = new QTableWidgetItem("");
186     if (enable)
187         enableItem->setCheckState(Qt::Checked);
188     else
189         enableItem->setCheckState(Qt::Unchecked);
190     enableItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
191
192     auto *chanNameItem = new QTableWidgetItem(chanName);
193
194     auto *senderItem = new QTableWidgetItem(sender);
195
196     int lastRow = ui.highlightTable->rowCount() - 1;
197     ui.highlightTable->setItem(lastRow, CoreHighlightSettingsPage::NameColumn, nameItem);
198     ui.highlightTable->setItem(lastRow, CoreHighlightSettingsPage::RegExColumn, regexItem);
199     ui.highlightTable->setItem(lastRow, CoreHighlightSettingsPage::CsColumn, csItem);
200     ui.highlightTable->setItem(lastRow, CoreHighlightSettingsPage::EnableColumn, enableItem);
201     ui.highlightTable->setItem(lastRow, CoreHighlightSettingsPage::SenderColumn, senderItem);
202     ui.highlightTable->setItem(lastRow, CoreHighlightSettingsPage::ChanColumn, chanNameItem);
203
204     if (!self)
205         ui.highlightTable->setCurrentItem(nameItem);
206
207     highlightList << HighlightRuleManager::HighlightRule(name, regex, cs, enable, false, sender, chanName);
208 }
209
210 void CoreHighlightSettingsPage::addNewIgnoredRow(bool enable, const QString &name, bool regex, bool cs,
211                                                  const QString &sender, const QString &chanName, bool self)
212 {
213     ui.ignoredTable->setRowCount(ui.ignoredTable->rowCount() + 1);
214
215     auto *nameItem = new QTableWidgetItem(name);
216
217     auto *regexItem = new QTableWidgetItem("");
218     if (regex)
219         regexItem->setCheckState(Qt::Checked);
220     else
221         regexItem->setCheckState(Qt::Unchecked);
222     regexItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
223
224     auto *csItem = new QTableWidgetItem("");
225     if (cs)
226         csItem->setCheckState(Qt::Checked);
227     else
228         csItem->setCheckState(Qt::Unchecked);
229     csItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
230
231     auto *enableItem = new QTableWidgetItem("");
232     if (enable)
233         enableItem->setCheckState(Qt::Checked);
234     else
235         enableItem->setCheckState(Qt::Unchecked);
236     enableItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
237
238     auto *chanNameItem = new QTableWidgetItem(chanName);
239
240     auto *senderItem = new QTableWidgetItem(sender);
241
242     int lastRow = ui.ignoredTable->rowCount() - 1;
243     ui.ignoredTable->setItem(lastRow, CoreHighlightSettingsPage::NameColumn, nameItem);
244     ui.ignoredTable->setItem(lastRow, CoreHighlightSettingsPage::RegExColumn, regexItem);
245     ui.ignoredTable->setItem(lastRow, CoreHighlightSettingsPage::CsColumn, csItem);
246     ui.ignoredTable->setItem(lastRow, CoreHighlightSettingsPage::EnableColumn, enableItem);
247     ui.ignoredTable->setItem(lastRow, CoreHighlightSettingsPage::SenderColumn, senderItem);
248     ui.ignoredTable->setItem(lastRow, CoreHighlightSettingsPage::ChanColumn, chanNameItem);
249
250     if (!self)
251         ui.ignoredTable->setCurrentItem(nameItem);
252
253     ignoredList << HighlightRuleManager::HighlightRule(name, regex, cs, enable, true, sender, chanName);
254 }
255
256 void CoreHighlightSettingsPage::removeSelectedHighlightRows()
257 {
258     QList<int> selectedRows;
259     QList<QTableWidgetItem *> selectedItemList = ui.highlightTable->selectedItems();
260     for (auto selectedItem : selectedItemList) {
261         selectedRows.append(selectedItem->row());
262     }
263     qSort(selectedRows.begin(), selectedRows.end(), qGreater<int>());
264     int lastRow = -1;
265     for (auto row : selectedRows) {
266         if (row != lastRow) {
267             ui.highlightTable->removeRow(row);
268             highlightList.removeAt(row);
269         }
270         lastRow = row;
271     }
272 }
273
274 void CoreHighlightSettingsPage::removeSelectedIgnoredRows()
275 {
276     QList<int> selectedRows;
277     QList<QTableWidgetItem *> selectedItemList = ui.ignoredTable->selectedItems();
278     for (auto selectedItem : selectedItemList) {
279         selectedRows.append(selectedItem->row());
280     }
281     qSort(selectedRows.begin(), selectedRows.end(), qGreater<int>());
282     int lastRow = -1;
283     for (auto row : selectedRows) {
284         if (row != lastRow) {
285             ui.ignoredTable->removeRow(row);
286             ignoredList.removeAt(row);
287         }
288         lastRow = row;
289     }
290 }
291
292 void CoreHighlightSettingsPage::highlightNicksChanged(const int index) {
293     // Only allow toggling "Case sensitive" when a nickname will be highlighted
294     auto highlightNickType = ui.highlightNicksComboBox->itemData(index).value<int>();
295     ui.nicksCaseSensitive->setEnabled(highlightNickType != HighlightRuleManager::NoNick);
296 }
297
298 void CoreHighlightSettingsPage::selectHighlightRow(QTableWidgetItem *item)
299 {
300     int row = item->row();
301     bool selected = item->isSelected();
302     ui.highlightTable
303         ->setRangeSelected(QTableWidgetSelectionRange(row, 0, row, CoreHighlightSettingsPage::ColumnCount - 1),
304                            selected);
305 }
306
307 void CoreHighlightSettingsPage::selectIgnoredRow(QTableWidgetItem *item)
308 {
309     int row = item->row();
310     bool selected = item->isSelected();
311     ui.ignoredTable
312         ->setRangeSelected(QTableWidgetSelectionRange(row, 0, row, CoreHighlightSettingsPage::ColumnCount - 1),
313                            selected);
314 }
315
316 void CoreHighlightSettingsPage::emptyHighlightTable()
317 {
318     // ui.highlight and highlightList should have the same size, but just to make sure.
319     if (ui.highlightTable->rowCount() != highlightList.size()) {
320         qDebug() << "something is wrong: ui.highlight and highlightList don't have the same size!";
321     }
322     while (ui.highlightTable->rowCount()) {
323         ui.highlightTable->removeRow(0);
324     }
325     highlightList.clear();
326 }
327
328 void CoreHighlightSettingsPage::emptyIgnoredTable()
329 {
330     // ui.highlight and highlightList should have the same size, but just to make sure.
331     if (ui.ignoredTable->rowCount() != ignoredList.size()) {
332         qDebug() << "something is wrong: ui.highlight and highlightList don't have the same size!";
333     }
334     while (ui.ignoredTable->rowCount()) {
335         ui.ignoredTable->removeRow(0);
336     }
337     ignoredList.clear();
338 }
339
340 void CoreHighlightSettingsPage::highlightTableChanged(QTableWidgetItem *item)
341 {
342     if (item->row() + 1 > highlightList.size())
343         return;
344
345     auto highlightRule = highlightList.value(item->row());
346
347
348     switch (item->column()) {
349         case CoreHighlightSettingsPage::EnableColumn:
350             highlightRule.isEnabled = (item->checkState() == Qt::Checked);
351             break;
352         case CoreHighlightSettingsPage::NameColumn:
353             if (item->text() == "")
354                 item->setText(tr("this shouldn't be empty"));
355             highlightRule.name = item->text();
356             break;
357         case CoreHighlightSettingsPage::RegExColumn:
358             highlightRule.isRegEx = (item->checkState() == Qt::Checked);
359             break;
360         case CoreHighlightSettingsPage::CsColumn:
361             highlightRule.isCaseSensitive = (item->checkState() == Qt::Checked);
362             break;
363         case CoreHighlightSettingsPage::SenderColumn:
364             if (!item->text().isEmpty() && item->text().trimmed().isEmpty())
365                 item->setText("");
366             highlightRule.sender = item->text();
367             break;
368         case CoreHighlightSettingsPage::ChanColumn:
369             if (!item->text().isEmpty() && item->text().trimmed().isEmpty())
370                 item->setText("");
371             highlightRule.chanName = item->text();
372             break;
373     }
374     highlightList[item->row()] = highlightRule;
375     emit widgetHasChanged();
376 }
377
378 void CoreHighlightSettingsPage::ignoredTableChanged(QTableWidgetItem *item)
379 {
380     if (item->row() + 1 > ignoredList.size())
381         return;
382
383     auto ignoredRule = ignoredList.value(item->row());
384
385
386     switch (item->column()) {
387         case CoreHighlightSettingsPage::EnableColumn:
388             ignoredRule.isEnabled = (item->checkState() == Qt::Checked);
389             break;
390         case CoreHighlightSettingsPage::NameColumn:
391             if (item->text() == "")
392                 item->setText(tr("this shouldn't be empty"));
393             ignoredRule.name = item->text();
394             break;
395         case CoreHighlightSettingsPage::RegExColumn:
396             ignoredRule.isRegEx = (item->checkState() == Qt::Checked);
397             break;
398         case CoreHighlightSettingsPage::CsColumn:
399             ignoredRule.isCaseSensitive = (item->checkState() == Qt::Checked);
400             break;
401         case CoreHighlightSettingsPage::SenderColumn:
402             if (!item->text().isEmpty() && item->text().trimmed().isEmpty())
403                 item->setText("");
404             ignoredRule.sender = item->text();
405             break;
406         case CoreHighlightSettingsPage::ChanColumn:
407             if (!item->text().isEmpty() && item->text().trimmed().isEmpty())
408                 item->setText("");
409             ignoredRule.chanName = item->text();
410             break;
411     }
412     ignoredList[item->row()] = ignoredRule;
413     emit widgetHasChanged();
414 }
415
416 void CoreHighlightSettingsPage::load()
417 {
418     emptyHighlightTable();
419     emptyIgnoredTable();
420
421     auto ruleManager = Client::highlightRuleManager();
422     if (ruleManager) {
423         for (auto &rule : ruleManager->highlightRuleList()) {
424             if (rule.isInverse) {
425                 addNewIgnoredRow(rule.isEnabled,
426                                  rule.name,
427                                  rule.isRegEx,
428                                  rule.isCaseSensitive,
429                                  rule.sender,
430                                  rule.chanName);
431             }
432             else {
433                 addNewHighlightRow(rule.isEnabled, rule.name, rule.isRegEx, rule.isCaseSensitive, rule.sender,
434                                    rule.chanName);
435             }
436         }
437
438         int highlightNickType = ruleManager->highlightNick();
439         ui.highlightNicksComboBox->setCurrentIndex(ui.highlightNicksComboBox->findData(QVariant(highlightNickType)));
440         // Trigger the initial update of nicksCaseSensitive being enabled or not
441         highlightNicksChanged(ui.highlightNicksComboBox->currentIndex());
442         ui.nicksCaseSensitive->setChecked(ruleManager->nicksCaseSensitive());
443
444         setChangedState(false);
445         _initialized = true;
446     } else {
447         defaults();
448     }
449 }
450
451 void CoreHighlightSettingsPage::save()
452 {
453     if (!hasChanged())
454         return;
455
456     if (!_initialized)
457         return;
458
459     auto ruleManager = Client::highlightRuleManager();
460     if (ruleManager == nullptr)
461         return;
462
463     auto clonedManager = HighlightRuleManager();
464     clonedManager.fromVariantMap(ruleManager->toVariantMap());
465     clonedManager.clear();
466
467     for (auto &rule : highlightList) {
468         clonedManager.addHighlightRule(rule.name, rule.isRegEx, rule.isCaseSensitive, rule.isEnabled, false,
469                                        rule.sender, rule.chanName);
470     }
471
472     for (auto &rule : ignoredList) {
473         clonedManager.addHighlightRule(rule.name, rule.isRegEx, rule.isCaseSensitive, rule.isEnabled, true,
474                                        rule.sender, rule.chanName);
475     }
476
477     auto highlightNickType = ui.highlightNicksComboBox->itemData(ui.highlightNicksComboBox->currentIndex()).value<int>();
478
479     clonedManager.setHighlightNick(HighlightRuleManager::HighlightNickType(highlightNickType));
480     clonedManager.setNicksCaseSensitive(ui.nicksCaseSensitive->isChecked());
481
482     ruleManager->requestUpdate(clonedManager.toVariantMap());
483     setChangedState(false);
484     load();
485 }
486
487 void CoreHighlightSettingsPage::widgetHasChanged()
488 {
489     setChangedState(true);
490 }
491
492 void CoreHighlightSettingsPage::importRules() {
493     NotificationSettings notificationSettings;
494
495     auto clonedManager = HighlightRuleManager();
496     clonedManager.fromVariantMap(Client::highlightRuleManager()->toVariantMap());
497
498     for (const auto &variant : notificationSettings.highlightList()) {
499         auto highlightRule = variant.toMap();
500
501         clonedManager.addHighlightRule(
502                 highlightRule["Name"].toString(),
503                 highlightRule["RegEx"].toBool(),
504                 highlightRule["CS"].toBool(),
505                 highlightRule["Enable"].toBool(),
506                 false,
507                 "",
508                 highlightRule["Channel"].toString()
509         );
510     }
511
512     Client::highlightRuleManager()->requestUpdate(clonedManager.toVariantMap());
513     setChangedState(false);
514     load();
515 }
516
517 bool CoreHighlightSettingsPage::isSelectable() const {
518     return Client::isConnected() && Client::isCoreFeatureEnabled(Quassel::Feature::CoreSideHighlights);
519 }