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