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