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