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