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