Rework sync protocol for highlight rules
[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 <QMessageBox>
23 #include <QTableWidget>
24
25 #include "client.h"
26 #include "corehighlightsettingspage.h"
27 #include "icon.h"
28 #include "qtui.h"
29
30 CoreHighlightSettingsPage::CoreHighlightSettingsPage(QWidget *parent)
31     : SettingsPage(tr("Interface"),
32                    // In Monolithic mode, local highlights are replaced by remote highlights
33                    Quassel::runMode() == Quassel::Monolithic ?
34                        tr("Highlights") : tr("Remote Highlights"),
35                    parent)
36 {
37     ui.setupUi(this);
38
39     setupRuleTable(ui.highlightTable);
40     setupRuleTable(ui.ignoredTable);
41
42     ui.highlightNicksComboBox->addItem(tr("All Nicks from Identity"), QVariant(HighlightRuleManager::AllNicks));
43     ui.highlightNicksComboBox->addItem(tr("Current Nick"), QVariant(HighlightRuleManager::CurrentNick));
44     ui.highlightNicksComboBox->addItem(tr("None"), QVariant(HighlightRuleManager::NoNick));
45
46     coreConnectionStateChanged(Client::isConnected()); // need a core connection!
47     connect(Client::instance(), SIGNAL(coreConnectionStateChanged(bool)), this, SLOT(coreConnectionStateChanged(bool)));
48
49     connect(ui.highlightAdd, SIGNAL(clicked(bool)), this, SLOT(addNewHighlightRow()));
50     connect(ui.highlightRemove, SIGNAL(clicked(bool)), this, SLOT(removeSelectedHighlightRows()));
51     connect(ui.highlightImport, SIGNAL(clicked(bool)), this, SLOT(importRules()));
52
53     connect(ui.ignoredAdd, SIGNAL(clicked(bool)), this, SLOT(addNewIgnoredRow()));
54     connect(ui.ignoredRemove, SIGNAL(clicked(bool)), this, SLOT(removeSelectedIgnoredRows()));
55
56     // TODO: search for a better signal (one that emits everytime a selection has been changed for one item)
57     connect(ui.highlightTable,
58             SIGNAL(itemClicked(QTableWidgetItem * )),
59             this,
60             SLOT(selectHighlightRow(QTableWidgetItem * )));
61     connect(ui.ignoredTable,
62             SIGNAL(itemClicked(QTableWidgetItem * )),
63             this,
64             SLOT(selectIgnoredRow(QTableWidgetItem * )));
65
66     // Update the "Case sensitive" checkbox
67     connect(ui.highlightNicksComboBox,
68             SIGNAL(currentIndexChanged(int)),
69             this,
70             SLOT(highlightNicksChanged(int)));
71
72     connect(ui.highlightNicksComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(widgetHasChanged()));
73     connect(ui.nicksCaseSensitive, SIGNAL(clicked(bool)), this, SLOT(widgetHasChanged()));
74
75     connect(ui.highlightAdd, SIGNAL(clicked()), this, SLOT(widgetHasChanged()));
76     connect(ui.highlightRemove, SIGNAL(clicked()), this, SLOT(widgetHasChanged()));
77
78     connect(ui.ignoredAdd, SIGNAL(clicked()), this, SLOT(widgetHasChanged()));
79     connect(ui.ignoredRemove, SIGNAL(clicked()), this, SLOT(widgetHasChanged()));
80
81     connect(ui.highlightTable,
82             SIGNAL(itemChanged(QTableWidgetItem * )),
83             this,
84             SLOT(highlightTableChanged(QTableWidgetItem * )));
85
86     connect(ui.ignoredTable,
87             SIGNAL(itemChanged(QTableWidgetItem * )),
88             this,
89             SLOT(ignoredTableChanged(QTableWidgetItem * )));
90
91     connect(Client::instance(), SIGNAL(connected()), this, SLOT(clientConnected()));
92
93     // Warning icon
94     ui.coreUnsupportedIcon->setPixmap(icon::get("dialog-warning").pixmap(16));
95
96     // Set up client/monolithic remote highlights information
97     if (Quassel::runMode() == Quassel::Monolithic) {
98         // We're running in Monolithic mode, local highlights are considered legacy
99         ui.highlightImport->setText(tr("Import Legacy"));
100         ui.highlightImport->setToolTip(tr("Import highlight rules configured in <i>%1</i>.")
101                                        .arg(tr("Legacy Highlights").replace(" ", "&nbsp;")));
102         // Re-use translations of "Legacy Highlights" as this is a word-for-word reference, forcing
103         // all spaces to be non-breaking
104     } else {
105         // We're running in client/split mode, local highlights are distinguished from remote
106         ui.highlightImport->setText(tr("Import Local"));
107         ui.highlightImport->setToolTip(tr("Import highlight rules configured in <i>%1</i>.")
108                                        .arg(tr("Local Highlights").replace(" ", "&nbsp;")));
109         // Re-use translations of "Local Highlights" as this is a word-for-word reference, forcing
110         // all spaces to be non-breaking
111     }
112 }
113
114 void CoreHighlightSettingsPage::coreConnectionStateChanged(bool state)
115 {
116     updateCoreSupportStatus(state);
117     setEnabled(state);
118     if (state) {
119         load();
120     } else {
121         revert();
122     }
123 }
124
125 void CoreHighlightSettingsPage::setupRuleTable(QTableWidget *table) const
126 {
127     table->verticalHeader()->hide();
128     table->setShowGrid(false);
129
130     table->horizontalHeaderItem(CoreHighlightSettingsPage::EnableColumn)->setToolTip(
131                 tr("Enable/disable this rule"));
132     table->horizontalHeaderItem(CoreHighlightSettingsPage::EnableColumn)->setWhatsThis(
133                 table->horizontalHeaderItem(CoreHighlightSettingsPage::EnableColumn)->toolTip());
134
135     table->horizontalHeaderItem(CoreHighlightSettingsPage::NameColumn)->setToolTip(
136                 tr("Phrase to match"));
137     table->horizontalHeaderItem(CoreHighlightSettingsPage::NameColumn)->setWhatsThis(
138                 table->horizontalHeaderItem(CoreHighlightSettingsPage::NameColumn)->toolTip());
139
140     table->horizontalHeaderItem(CoreHighlightSettingsPage::RegExColumn)->setToolTip(
141                 tr("<b>RegEx</b>: This option determines if the highlight rule, <i>Sender</i>, and "
142                    "<i>Channel</i> should be interpreted as <b>regular expressions</b> or just as "
143                    "keywords."));
144     table->horizontalHeaderItem(CoreHighlightSettingsPage::RegExColumn)->setWhatsThis(
145                 table->horizontalHeaderItem(CoreHighlightSettingsPage::RegExColumn)->toolTip());
146
147     table->horizontalHeaderItem(CoreHighlightSettingsPage::CsColumn)->setToolTip(
148                 tr("<b>CS</b>: This option determines if the highlight rule, <i>Sender</i>, and "
149                    "<i>Channel</i> should be interpreted <b>case sensitive</b>."));
150     table->horizontalHeaderItem(CoreHighlightSettingsPage::CsColumn)->setWhatsThis(
151                 table->horizontalHeaderItem(CoreHighlightSettingsPage::CsColumn)->toolTip());
152
153     table->horizontalHeaderItem(CoreHighlightSettingsPage::SenderColumn)->setToolTip(
154                 tr("<p><b>Sender</b>: Semicolon separated list of <i>nick!ident@host</i> names, "
155                    "leave blank to match any nickname.</p>"
156                    "<p><i>Example:</i><br />"
157                    "<i>Alice!*; Bob!*@example.com; Carol*!*; !Caroline!*</i><br />"
158                    "would match on <i>Alice</i>, <i>Bob</i> with hostmask <i>example.com</i>, and "
159                    "any nickname starting with <i>Carol</i> except for <i>Caroline</i><br />"
160                    "<p>If only inverted names are specified, it will match anything except for "
161                    "what's specified (implicit wildcard).</p>"
162                    "<p><i>Example:</i><br />"
163                    "<i>!Announce*!*; !Wheatley!aperture@*</i><br />"
164                    "would match anything except for <i>Wheatley</i> with ident <i>aperture</i> or "
165                    "any nickname starting with <i>Announce</i></p>"));
166     table->horizontalHeaderItem(CoreHighlightSettingsPage::SenderColumn)->setWhatsThis(
167                 table->horizontalHeaderItem(CoreHighlightSettingsPage::SenderColumn)->toolTip());
168
169     table->horizontalHeaderItem(CoreHighlightSettingsPage::ChanColumn)->setToolTip(
170                 tr("<p><b>Channel</b>: Semicolon separated list of channel names, leave blank to "
171                    "match any name.</p>"
172                    "<p><i>Example:</i><br />"
173                    "<i>#quassel*; #foobar; !#quasseldroid</i><br />"
174                    "would match on <i>#foobar</i> and any channel starting with <i>#quassel</i> "
175                    "except for <i>#quasseldroid</i><br />"
176                    "<p>If only inverted names are specified, it will match anything except for "
177                    "what's specified (implicit wildcard).</p>"
178                    "<p><i>Example:</i><br />"
179                    "<i>!#quassel*; !#foobar</i><br />"
180                    "would match anything except for <i>#foobar</i> or any channel starting with "
181                    "<i>#quassel</i></p>"));
182     table->horizontalHeaderItem(CoreHighlightSettingsPage::ChanColumn)->setWhatsThis(
183                 table->horizontalHeaderItem(CoreHighlightSettingsPage::ChanColumn)->toolTip());
184
185 #if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
186     table->horizontalHeader()->setResizeMode(CoreHighlightSettingsPage::EnableColumn, QHeaderView::ResizeToContents);
187     table->horizontalHeader()->setResizeMode(CoreHighlightSettingsPage::NameColumn, QHeaderView::Stretch);
188     table->horizontalHeader()->setResizeMode(CoreHighlightSettingsPage::RegExColumn, QHeaderView::ResizeToContents);
189     table->horizontalHeader()->setResizeMode(CoreHighlightSettingsPage::CsColumn, QHeaderView::ResizeToContents);
190     table->horizontalHeader()->setResizeMode(CoreHighlightSettingsPage::ChanColumn, QHeaderView::ResizeToContents);
191 #else
192     table->horizontalHeader()->setSectionResizeMode(CoreHighlightSettingsPage::EnableColumn, QHeaderView::ResizeToContents);
193     table->horizontalHeader()->setSectionResizeMode(CoreHighlightSettingsPage::NameColumn, QHeaderView::Stretch);
194     table->horizontalHeader()->setSectionResizeMode(CoreHighlightSettingsPage::RegExColumn, QHeaderView::ResizeToContents);
195     table->horizontalHeader()->setSectionResizeMode(CoreHighlightSettingsPage::CsColumn, QHeaderView::ResizeToContents);
196     table->horizontalHeader()->setSectionResizeMode(CoreHighlightSettingsPage::ChanColumn, QHeaderView::ResizeToContents);
197 #endif
198 }
199
200 void CoreHighlightSettingsPage::updateCoreSupportStatus(bool state)
201 {
202     // Assume connected state as enforced by the settings page UI
203     if (!state || Client::isCoreFeatureEnabled(Quassel::Feature::CoreSideHighlights)) {
204         // Either disconnected or core supports highlights, enable highlight configuration and hide
205         // warning.  Don't show the warning needlessly when disconnected.
206         ui.highlightsConfigWidget->setEnabled(true);
207         ui.coreUnsupportedWidget->setVisible(false);
208     } else {
209         // Core does not support highlights, show warning and disable highlight configuration
210         ui.highlightsConfigWidget->setEnabled(false);
211         ui.coreUnsupportedWidget->setVisible(true);
212     }
213 }
214
215 void CoreHighlightSettingsPage::clientConnected()
216 {
217     connect(Client::highlightRuleManager(), SIGNAL(updated()), SLOT(revert()));
218 }
219
220 void CoreHighlightSettingsPage::revert()
221 {
222     if (!hasChanged())
223         return;
224
225     setChangedState(false);
226     load();
227 }
228
229 bool CoreHighlightSettingsPage::hasDefaults() const
230 {
231     return true;
232 }
233
234 void CoreHighlightSettingsPage::defaults()
235 {
236     int highlightNickType = HighlightRuleManager::HighlightNickType::CurrentNick;
237     int defaultIndex = ui.highlightNicksComboBox->findData(QVariant(highlightNickType));
238     ui.highlightNicksComboBox->setCurrentIndex(defaultIndex);
239     ui.nicksCaseSensitive->setChecked(false);
240     emptyHighlightTable();
241     emptyIgnoredTable();
242
243     widgetHasChanged();
244 }
245
246 void CoreHighlightSettingsPage::addNewHighlightRow(bool enable, int id, const QString &name, bool regex, bool cs,
247                                                    const QString &sender, const QString &chanName, bool self)
248 {
249     ui.highlightTable->setRowCount(ui.highlightTable->rowCount() + 1);
250
251     if (id < 0) {
252         id = nextId();
253     }
254
255     auto *nameItem = new QTableWidgetItem(name);
256
257     auto *regexItem = new QTableWidgetItem("");
258     if (regex)
259         regexItem->setCheckState(Qt::Checked);
260     else
261         regexItem->setCheckState(Qt::Unchecked);
262     regexItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
263
264     auto *csItem = new QTableWidgetItem("");
265     if (cs)
266         csItem->setCheckState(Qt::Checked);
267     else
268         csItem->setCheckState(Qt::Unchecked);
269     csItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
270
271     auto *enableItem = new QTableWidgetItem("");
272     if (enable)
273         enableItem->setCheckState(Qt::Checked);
274     else
275         enableItem->setCheckState(Qt::Unchecked);
276     enableItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
277
278     auto *senderItem = new QTableWidgetItem(sender);
279
280     auto *chanNameItem = new QTableWidgetItem(chanName);
281
282     enableItem->setToolTip(tr("Enable/disable this rule"));
283     nameItem->setToolTip(tr("Phrase to match"));
284     regexItem->setToolTip(
285                 tr("<b>RegEx</b>: This option determines if the highlight rule, <i>Sender</i>, and "
286                    "<i>Channel</i> should be interpreted as <b>regular expressions</b> or just as "
287                    "keywords."));
288     csItem->setToolTip(
289                 tr("<b>CS</b>: This option determines if the highlight rule, <i>Sender</i>, and "
290                    "<i>Channel</i> should be interpreted <b>case sensitive</b>."));
291     senderItem->setToolTip(
292                 tr("<p><b>Sender</b>: Semicolon separated list of <i>nick!ident@host</i> names, "
293                    "leave blank to match any nickname.</p>"
294                    "<p><i>Example:</i><br />"
295                    "<i>Alice!*; Bob!*@example.com; Carol*!*; !Caroline!*</i><br />"
296                    "would match on <i>Alice</i>, <i>Bob</i> with hostmask <i>example.com</i>, and "
297                    "any nickname starting with <i>Carol</i> except for <i>Caroline</i><br />"
298                    "<p>If only inverted names are specified, it will match anything except for "
299                    "what's specified (implicit wildcard).</p>"
300                    "<p><i>Example:</i><br />"
301                    "<i>!Announce*!*; !Wheatley!aperture@*</i><br />"
302                    "would match anything except for <i>Wheatley</i> with ident <i>aperture</i> or "
303                    "any nickname starting with <i>Announce</i></p>"));
304     chanNameItem->setToolTip(
305                 tr("<p><b>Channel</b>: Semicolon separated list of channel names, leave blank to "
306                    "match any name.</p>"
307                    "<p><i>Example:</i><br />"
308                    "<i>#quassel*; #foobar; !#quasseldroid</i><br />"
309                    "would match on <i>#foobar</i> and any channel starting with <i>#quassel</i> "
310                    "except for <i>#quasseldroid</i><br />"
311                    "<p>If only inverted names are specified, it will match anything except for "
312                    "what's specified (implicit wildcard).</p>"
313                    "<p><i>Example:</i><br />"
314                    "<i>!#quassel*; !#foobar</i><br />"
315                    "would match anything except for <i>#foobar</i> or any channel starting with "
316                    "<i>#quassel</i></p>"));
317
318     int lastRow = ui.highlightTable->rowCount() - 1;
319     ui.highlightTable->setItem(lastRow, CoreHighlightSettingsPage::NameColumn, nameItem);
320     ui.highlightTable->setItem(lastRow, CoreHighlightSettingsPage::RegExColumn, regexItem);
321     ui.highlightTable->setItem(lastRow, CoreHighlightSettingsPage::CsColumn, csItem);
322     ui.highlightTable->setItem(lastRow, CoreHighlightSettingsPage::EnableColumn, enableItem);
323     ui.highlightTable->setItem(lastRow, CoreHighlightSettingsPage::SenderColumn, senderItem);
324     ui.highlightTable->setItem(lastRow, CoreHighlightSettingsPage::ChanColumn, chanNameItem);
325
326     if (!self)
327         ui.highlightTable->setCurrentItem(nameItem);
328
329     highlightList << HighlightRuleManager::HighlightRule(id, name, regex, cs, enable, false, sender, chanName);
330 }
331
332 void CoreHighlightSettingsPage::addNewIgnoredRow(bool enable, int id, const QString &name, bool regex, bool cs,
333                                                  const QString &sender, const QString &chanName, bool self)
334 {
335     ui.ignoredTable->setRowCount(ui.ignoredTable->rowCount() + 1);
336
337     if (id < 0) {
338         id = nextId();
339     }
340
341     auto *nameItem = new QTableWidgetItem(name);
342
343     auto *regexItem = new QTableWidgetItem("");
344     if (regex)
345         regexItem->setCheckState(Qt::Checked);
346     else
347         regexItem->setCheckState(Qt::Unchecked);
348     regexItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
349
350     auto *csItem = new QTableWidgetItem("");
351     if (cs)
352         csItem->setCheckState(Qt::Checked);
353     else
354         csItem->setCheckState(Qt::Unchecked);
355     csItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
356
357     auto *enableItem = new QTableWidgetItem("");
358     if (enable)
359         enableItem->setCheckState(Qt::Checked);
360     else
361         enableItem->setCheckState(Qt::Unchecked);
362     enableItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
363
364     auto *chanNameItem = new QTableWidgetItem(chanName);
365
366     auto *senderItem = new QTableWidgetItem(sender);
367
368     enableItem->setToolTip(tr("Enable/disable this rule"));
369     nameItem->setToolTip(tr("Phrase to match"));
370     regexItem->setToolTip(
371                 tr("<b>RegEx</b>: This option determines if the highlight rule should be "
372                    "interpreted as a <b>regular expression</b> or just as a keyword."));
373     csItem->setToolTip(
374                 tr("<b>CS</b>: This option determines if the highlight rule should be interpreted "
375                    "<b>case sensitive</b>."));
376     senderItem->setToolTip(
377                 tr("<b>Sender</b>: This option specifies which sender nicknames match.  Leave "
378                    "blank to match any nickname."));
379     chanNameItem->setToolTip(
380                 tr("<p><b>Channel</b>: Semicolon separated list of channel names.</p>"
381                    "<p><i>Example:</i><br />"
382                    "<i>#quassel*; #foobar; !#quasseldroid</i><br />"
383                    "would match on #foobar and any channel starting with <i>#quassel</i> except "
384                    "for <i>#quasseldroid</i><br />"
385                    "<p>If only inverted names are specified, it will match anything except for "
386                    "what's specified (implicit wildcard).</p>"
387                    "<p><i>Example:</i><br />"
388                    "<i>!#quassel*; !#foobar</i><br />"
389                    "would match anything except for #foobar or any channel starting with "
390                    "<i>#quassel</i></p>"));
391
392     int lastRow = ui.ignoredTable->rowCount() - 1;
393     ui.ignoredTable->setItem(lastRow, CoreHighlightSettingsPage::NameColumn, nameItem);
394     ui.ignoredTable->setItem(lastRow, CoreHighlightSettingsPage::RegExColumn, regexItem);
395     ui.ignoredTable->setItem(lastRow, CoreHighlightSettingsPage::CsColumn, csItem);
396     ui.ignoredTable->setItem(lastRow, CoreHighlightSettingsPage::EnableColumn, enableItem);
397     ui.ignoredTable->setItem(lastRow, CoreHighlightSettingsPage::SenderColumn, senderItem);
398     ui.ignoredTable->setItem(lastRow, CoreHighlightSettingsPage::ChanColumn, chanNameItem);
399
400     if (!self)
401         ui.ignoredTable->setCurrentItem(nameItem);
402
403     ignoredList << HighlightRuleManager::HighlightRule(id, name, regex, cs, enable, true, sender, chanName);
404 }
405
406 void CoreHighlightSettingsPage::removeSelectedHighlightRows()
407 {
408     QList<int> selectedRows;
409     QList<QTableWidgetItem *> selectedItemList = ui.highlightTable->selectedItems();
410     for (auto selectedItem : selectedItemList) {
411         selectedRows.append(selectedItem->row());
412     }
413     qSort(selectedRows.begin(), selectedRows.end(), qGreater<int>());
414     int lastRow = -1;
415     for (auto row : selectedRows) {
416         if (row != lastRow) {
417             ui.highlightTable->removeRow(row);
418             highlightList.removeAt(row);
419         }
420         lastRow = row;
421     }
422 }
423
424 void CoreHighlightSettingsPage::removeSelectedIgnoredRows()
425 {
426     QList<int> selectedRows;
427     QList<QTableWidgetItem *> selectedItemList = ui.ignoredTable->selectedItems();
428     for (auto selectedItem : selectedItemList) {
429         selectedRows.append(selectedItem->row());
430     }
431     qSort(selectedRows.begin(), selectedRows.end(), qGreater<int>());
432     int lastRow = -1;
433     for (auto row : selectedRows) {
434         if (row != lastRow) {
435             ui.ignoredTable->removeRow(row);
436             ignoredList.removeAt(row);
437         }
438         lastRow = row;
439     }
440 }
441
442 void CoreHighlightSettingsPage::highlightNicksChanged(const int index) {
443     // Only allow toggling "Case sensitive" when a nickname will be highlighted
444     auto highlightNickType = ui.highlightNicksComboBox->itemData(index).value<int>();
445     ui.nicksCaseSensitive->setEnabled(highlightNickType != HighlightRuleManager::NoNick);
446 }
447
448 void CoreHighlightSettingsPage::selectHighlightRow(QTableWidgetItem *item)
449 {
450     int row = item->row();
451     bool selected = item->isSelected();
452     ui.highlightTable
453         ->setRangeSelected(QTableWidgetSelectionRange(row, 0, row, CoreHighlightSettingsPage::ColumnCount - 1),
454                            selected);
455 }
456
457 void CoreHighlightSettingsPage::selectIgnoredRow(QTableWidgetItem *item)
458 {
459     int row = item->row();
460     bool selected = item->isSelected();
461     ui.ignoredTable
462         ->setRangeSelected(QTableWidgetSelectionRange(row, 0, row, CoreHighlightSettingsPage::ColumnCount - 1),
463                            selected);
464 }
465
466 void CoreHighlightSettingsPage::emptyHighlightTable()
467 {
468     // ui.highlight and highlightList should have the same size, but just to make sure.
469     if (ui.highlightTable->rowCount() != highlightList.size()) {
470         qDebug() << "something is wrong: ui.highlight and highlightList don't have the same size!";
471     }
472     while (ui.highlightTable->rowCount()) {
473         ui.highlightTable->removeRow(0);
474     }
475     highlightList.clear();
476 }
477
478 void CoreHighlightSettingsPage::emptyIgnoredTable()
479 {
480     // ui.highlight and highlightList should have the same size, but just to make sure.
481     if (ui.ignoredTable->rowCount() != ignoredList.size()) {
482         qDebug() << "something is wrong: ui.highlight and highlightList don't have the same size!";
483     }
484     while (ui.ignoredTable->rowCount()) {
485         ui.ignoredTable->removeRow(0);
486     }
487     ignoredList.clear();
488 }
489
490 void CoreHighlightSettingsPage::highlightTableChanged(QTableWidgetItem *item)
491 {
492     if (item->row() + 1 > highlightList.size())
493         return;
494
495     auto highlightRule = highlightList.value(item->row());
496
497
498     switch (item->column()) {
499         case CoreHighlightSettingsPage::EnableColumn:
500             highlightRule.isEnabled = (item->checkState() == Qt::Checked);
501             break;
502         case CoreHighlightSettingsPage::NameColumn:
503             if (item->text() == "")
504                 item->setText(tr("this shouldn't be empty"));
505             highlightRule.name = item->text();
506             break;
507         case CoreHighlightSettingsPage::RegExColumn:
508             highlightRule.isRegEx = (item->checkState() == Qt::Checked);
509             break;
510         case CoreHighlightSettingsPage::CsColumn:
511             highlightRule.isCaseSensitive = (item->checkState() == Qt::Checked);
512             break;
513         case CoreHighlightSettingsPage::SenderColumn:
514             if (!item->text().isEmpty() && item->text().trimmed().isEmpty())
515                 item->setText("");
516             highlightRule.sender = item->text();
517             break;
518         case CoreHighlightSettingsPage::ChanColumn:
519             if (!item->text().isEmpty() && item->text().trimmed().isEmpty())
520                 item->setText("");
521             highlightRule.chanName = item->text();
522             break;
523     }
524     highlightList[item->row()] = highlightRule;
525     emit widgetHasChanged();
526 }
527
528 void CoreHighlightSettingsPage::ignoredTableChanged(QTableWidgetItem *item)
529 {
530     if (item->row() + 1 > ignoredList.size())
531         return;
532
533     auto ignoredRule = ignoredList.value(item->row());
534
535
536     switch (item->column()) {
537         case CoreHighlightSettingsPage::EnableColumn:
538             ignoredRule.isEnabled = (item->checkState() == Qt::Checked);
539             break;
540         case CoreHighlightSettingsPage::NameColumn:
541             if (item->text() == "")
542                 item->setText(tr("this shouldn't be empty"));
543             ignoredRule.name = item->text();
544             break;
545         case CoreHighlightSettingsPage::RegExColumn:
546             ignoredRule.isRegEx = (item->checkState() == Qt::Checked);
547             break;
548         case CoreHighlightSettingsPage::CsColumn:
549             ignoredRule.isCaseSensitive = (item->checkState() == Qt::Checked);
550             break;
551         case CoreHighlightSettingsPage::SenderColumn:
552             if (!item->text().isEmpty() && item->text().trimmed().isEmpty())
553                 item->setText("");
554             ignoredRule.sender = item->text();
555             break;
556         case CoreHighlightSettingsPage::ChanColumn:
557             if (!item->text().isEmpty() && item->text().trimmed().isEmpty())
558                 item->setText("");
559             ignoredRule.chanName = item->text();
560             break;
561     }
562     ignoredList[item->row()] = ignoredRule;
563     emit widgetHasChanged();
564 }
565
566 void CoreHighlightSettingsPage::load()
567 {
568     emptyHighlightTable();
569     emptyIgnoredTable();
570
571     auto ruleManager = Client::highlightRuleManager();
572     if (ruleManager) {
573         for (auto &rule : ruleManager->highlightRuleList()) {
574             if (rule.isInverse) {
575                 addNewIgnoredRow(rule.isEnabled,
576                                  rule.id,
577                                  rule.name,
578                                  rule.isRegEx,
579                                  rule.isCaseSensitive,
580                                  rule.sender,
581                                  rule.chanName);
582             }
583             else {
584                 addNewHighlightRow(rule.isEnabled, rule.id, rule.name, rule.isRegEx, rule.isCaseSensitive, rule.sender,
585                                    rule.chanName);
586             }
587         }
588
589         int highlightNickType = ruleManager->highlightNick();
590         ui.highlightNicksComboBox->setCurrentIndex(ui.highlightNicksComboBox->findData(QVariant(highlightNickType)));
591         // Trigger the initial update of nicksCaseSensitive being enabled or not
592         highlightNicksChanged(ui.highlightNicksComboBox->currentIndex());
593         ui.nicksCaseSensitive->setChecked(ruleManager->nicksCaseSensitive());
594
595         setChangedState(false);
596         _initialized = true;
597     } else {
598         defaults();
599     }
600 }
601
602 void CoreHighlightSettingsPage::save()
603 {
604     if (!hasChanged())
605         return;
606
607     if (!_initialized)
608         return;
609
610     auto ruleManager = Client::highlightRuleManager();
611     if (ruleManager == nullptr)
612         return;
613
614     auto clonedManager = HighlightRuleManager();
615     clonedManager.fromVariantMap(ruleManager->toVariantMap());
616     clonedManager.clear();
617
618     for (auto &rule : highlightList) {
619         clonedManager.addHighlightRule(rule.id, rule.name, rule.isRegEx, rule.isCaseSensitive, rule.isEnabled, false,
620                                        rule.sender, rule.chanName);
621     }
622
623     for (auto &rule : ignoredList) {
624         clonedManager.addHighlightRule(rule.id, rule.name, rule.isRegEx, rule.isCaseSensitive, rule.isEnabled, true,
625                                        rule.sender, rule.chanName);
626     }
627
628     auto highlightNickType = ui.highlightNicksComboBox->itemData(ui.highlightNicksComboBox->currentIndex()).value<int>();
629
630     clonedManager.setHighlightNick(HighlightRuleManager::HighlightNickType(highlightNickType));
631     clonedManager.setNicksCaseSensitive(ui.nicksCaseSensitive->isChecked());
632
633     ruleManager->requestUpdate(clonedManager.toVariantMap());
634     setChangedState(false);
635     load();
636 }
637
638 int CoreHighlightSettingsPage::nextId() {
639     int max = 0;
640     for (int i = 0; i < highlightList.count(); i++) {
641         int id = highlightList[i].id;
642         if (id > max) {
643             max = id;
644         }
645     }
646     for (int i = 0; i < ignoredList.count(); i++) {
647         int id = ignoredList[i].id;
648         if (id > max) {
649             max = id;
650         }
651     }
652     return max+1;
653 }
654
655 void CoreHighlightSettingsPage::widgetHasChanged()
656 {
657     setChangedState(true);
658 }
659
660 void CoreHighlightSettingsPage::on_coreUnsupportedDetails_clicked()
661 {
662     // Re-use translations of "Local Highlights" as this is a word-for-word reference, forcing all
663     // spaces to non-breaking
664     const QString localHighlightsName = tr("Local Highlights").replace(" ", "&nbsp;");
665
666     const QString remoteHighlightsMsgText =
667             QString("<p><b>%1</b></p></br><p>%2</p></br><p>%3</p>"
668                     ).arg(tr("Your Quassel core is too old to support remote highlights"),
669                           tr("You need a Quassel core v0.13.0 or newer to configure remote "
670                              "highlights."),
671                           tr("You can still configure highlights for this device only in "
672                              "<i>%1</i>.").arg(localHighlightsName));
673
674     QMessageBox::warning(this,
675                          tr("Remote Highlights unsupported"),
676                          remoteHighlightsMsgText);
677 }
678
679 void CoreHighlightSettingsPage::importRules() {
680     NotificationSettings notificationSettings;
681
682     const auto localHighlightList = notificationSettings.highlightList();
683
684     // Re-use translations of "Legacy/Local Highlights" as this is a word-for-word reference,
685     // forcing all spaces to non-breaking
686     QString localHighlightsName;
687     if (Quassel::runMode() == Quassel::Monolithic) {
688         localHighlightsName = tr("Legacy Highlights").replace(" ", "&nbsp;");
689     } else {
690         localHighlightsName = tr("Local Highlights").replace(" ", "&nbsp;");
691     }
692
693     if (localHighlightList.count() == 0) {
694         // No highlight rules exist to import, do nothing
695         QMessageBox::information(this,
696                                  tr("No highlights to import"),
697                                  tr("No highlight rules in <i>%1</i>."
698                                     ).arg(localHighlightsName));
699         return;
700     }
701
702     int ret = QMessageBox::question(this,
703                                     tr("Import highlights?"),
704                                     tr("Import all highlight rules from <i>%1</i>?"
705                                        ).arg(localHighlightsName),
706                                     QMessageBox::Yes|QMessageBox::No,
707                                     QMessageBox::No);
708
709     if (ret != QMessageBox::Yes) {
710         // Only two options, Yes or No, return if not Yes
711         return;
712     }
713
714     auto clonedManager = HighlightRuleManager();
715     clonedManager.fromVariantMap(Client::highlightRuleManager()->toVariantMap());
716
717     for (const auto &variant : notificationSettings.highlightList()) {
718         auto highlightRule = variant.toMap();
719
720         clonedManager.addHighlightRule(
721                 clonedManager.nextId(),
722                 highlightRule["Name"].toString(),
723                 highlightRule["RegEx"].toBool(),
724                 highlightRule["CS"].toBool(),
725                 highlightRule["Enable"].toBool(),
726                 false,
727                 "",
728                 highlightRule["Channel"].toString()
729         );
730     }
731
732     Client::highlightRuleManager()->requestUpdate(clonedManager.toVariantMap());
733     setChangedState(false);
734     load();
735
736     // Give a heads-up that all succeeded
737     QMessageBox::information(this,
738                              tr("Imported highlights"),
739                              tr("%1 highlight rules successfully imported."
740                                 ).arg(QString::number(localHighlightList.count())));
741 }
742
743 bool CoreHighlightSettingsPage::isSelectable() const {
744     return Client::isConnected();
745     // We check for Quassel::Feature::CoreSideHighlights when loading this page, allowing us to show
746     // a friendly error message.
747 }