Advanced channel filters (#1748)

Adds custom channel filters complete with their own mini-language. Filters can be created in settings, and applied by clicking the three dots to open the Split menu and selecting "Set filters".
This commit is contained in:
dnsge
2020-10-18 15:16:56 +02:00
committed by Rasmus Karlsson
parent 812cbdf4f9
commit 4199a01b96
41 changed files with 2189 additions and 43 deletions
@@ -0,0 +1,227 @@
#include "ChannelFilterEditorDialog.hpp"
#include "controllers/filters/parser/FilterParser.hpp"
namespace chatterino {
namespace {
const QStringList friendlyBinaryOps = {
"and", "or", "+", "-", "*", "/",
"%", "equals", "not equals", "<", ">", "<=",
">=", "contains", "starts with", "ends with", "(nothing)"};
const QStringList realBinaryOps = {
"&&", "||", "+", "-", "*", "/",
"%", "==", "!=", "<", ">", "<=",
">=", "contains", "startswith", "endswith", ""};
} // namespace
ChannelFilterEditorDialog::ChannelFilterEditorDialog(QWidget *parent)
: QDialog(parent)
{
auto vbox = new QVBoxLayout(this);
auto filterVbox = new QVBoxLayout;
auto buttonBox = new QHBoxLayout;
auto okButton = new QPushButton("OK");
auto cancelButton = new QPushButton("Cancel");
okButton->setDefault(true);
cancelButton->setDefault(false);
auto helpLabel =
new QLabel(QString("<a href='%1'><span "
"style='color:#99f'>variable help</span></a>")
.arg("https://github.com/Chatterino/chatterino2/blob/"
"master/docs/Filters.md#variables"));
helpLabel->setOpenExternalLinks(true);
buttonBox->addWidget(helpLabel);
buttonBox->addStretch(1);
buttonBox->addWidget(cancelButton);
buttonBox->addWidget(okButton);
QObject::connect(okButton, &QAbstractButton::clicked, [this] {
this->accept();
this->close();
});
QObject::connect(cancelButton, &QAbstractButton::clicked, [this] {
this->reject();
this->close();
});
this->setWindowFlags(
(this->windowFlags() & ~(Qt::WindowContextHelpButtonHint)) |
Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint);
this->setWindowTitle("Channel Filter Creator");
auto titleInput = new QLineEdit;
titleInput->setPlaceholderText("Filter name");
titleInput->setText("My filter");
this->titleInput_ = titleInput;
filterVbox->addWidget(titleInput);
auto left = new ChannelFilterEditorDialog::ValueSpecifier;
auto right = new ChannelFilterEditorDialog::ValueSpecifier;
auto exp =
new ChannelFilterEditorDialog::BinaryOperationSpecifier(left, right);
this->expressionSpecifier_ = exp;
filterVbox->addLayout(exp->layout());
vbox->addLayout(filterVbox);
vbox->addLayout(buttonBox);
// setup default values
left->setType("Variable");
left->setValue("message.content");
exp->setOperation("contains");
right->setType("Text");
right->setValue("hello");
}
const QString ChannelFilterEditorDialog::getFilter() const
{
return this->expressionSpecifier_->expressionText();
}
const QString ChannelFilterEditorDialog::getTitle() const
{
return this->titleInput_->text();
}
ChannelFilterEditorDialog::ValueSpecifier::ValueSpecifier()
{
this->typeCombo_ = new QComboBox;
this->varCombo_ = new QComboBox;
this->valueInput_ = new QLineEdit;
this->layout_ = new QHBoxLayout;
this->typeCombo_->insertItems(0, {"Text", "Number", "Variable"});
this->varCombo_->insertItems(0, filterparser::validIdentifiersMap.values());
this->layout_->addWidget(this->typeCombo_);
this->layout_->addWidget(this->varCombo_, 1);
this->layout_->addWidget(this->valueInput_, 1);
this->layout_->setContentsMargins(5, 5, 5, 5);
QObject::connect( //
this->typeCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged),
[this](int index) {
const auto isNumber = (index == 1);
const auto isVariable = (index == 2);
this->valueInput_->setVisible(!isVariable);
this->varCombo_->setVisible(isVariable);
this->valueInput_->setValidator(
isNumber ? (new QIntValidator(0, INT_MAX)) : nullptr);
this->valueInput_->clear();
});
this->varCombo_->hide();
this->typeCombo_->setCurrentIndex(0);
}
void ChannelFilterEditorDialog::ValueSpecifier::setEnabled(bool enabled)
{
this->typeCombo_->setEnabled(enabled);
this->varCombo_->setEnabled(enabled);
this->valueInput_->setEnabled(enabled);
}
void ChannelFilterEditorDialog::ValueSpecifier::setType(const QString &type)
{
this->typeCombo_->setCurrentText(type);
}
void ChannelFilterEditorDialog::ValueSpecifier::setValue(const QString &value)
{
if (this->typeCombo_->currentIndex() == 2)
{
this->varCombo_->setCurrentText(
filterparser::validIdentifiersMap.value(value));
}
else
{
this->valueInput_->setText(value);
}
}
QLayout *ChannelFilterEditorDialog::ValueSpecifier::layout() const
{
return this->layout_;
}
QString ChannelFilterEditorDialog::ValueSpecifier::expressionText()
{
switch (this->typeCombo_->currentIndex())
{
case 0: // text
return QString("\"%1\"").arg(
this->valueInput_->text().replace("\"", "\\\""));
case 1: // number
return this->valueInput_->text();
case 2: // variable
return filterparser::validIdentifiersMap.key(
this->varCombo_->currentText());
default:
return "";
}
}
ChannelFilterEditorDialog::BinaryOperationSpecifier::BinaryOperationSpecifier(
ExpressionSpecifier *left, ExpressionSpecifier *right)
: left_(left)
, right_(right)
{
this->opCombo_ = new QComboBox;
this->layout_ = new QVBoxLayout;
this->opCombo_->insertItems(0, friendlyBinaryOps);
this->layout_->addLayout(this->left_->layout());
this->layout_->addWidget(this->opCombo_);
this->layout_->addLayout(this->right_->layout());
this->layout_->setContentsMargins(5, 5, 5, 5);
QObject::connect( //
this->opCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged),
[this](int index) {
// disable if set to "(nothing)"
this->right_->setEnabled(!realBinaryOps.at(index).isEmpty());
});
}
void ChannelFilterEditorDialog::BinaryOperationSpecifier::setEnabled(
bool enabled)
{
this->opCombo_->setEnabled(enabled);
this->left_->setEnabled(enabled);
this->right_->setEnabled(enabled);
}
void ChannelFilterEditorDialog::BinaryOperationSpecifier::setOperation(
const QString &op)
{
this->opCombo_->setCurrentText(op);
}
QLayout *ChannelFilterEditorDialog::BinaryOperationSpecifier::layout() const
{
return this->layout_;
}
QString ChannelFilterEditorDialog::BinaryOperationSpecifier::expressionText()
{
QString opText = realBinaryOps.at(this->opCombo_->currentIndex());
if (opText.isEmpty())
{
return this->left_->expressionText();
}
return QString("(%1) %2 (%3)")
.arg(this->left_->expressionText())
.arg(opText)
.arg(this->right_->expressionText());
}
} // namespace chatterino
@@ -0,0 +1,61 @@
#pragma once
namespace chatterino {
class ChannelFilterEditorDialog : public QDialog
{
public:
ChannelFilterEditorDialog(QWidget *parent = nullptr);
const QString getFilter() const;
const QString getTitle() const;
private:
class ExpressionSpecifier
{
public:
virtual QLayout *layout() const = 0;
virtual QString expressionText() = 0;
virtual void setEnabled(bool enabled) = 0;
};
class ValueSpecifier : public ExpressionSpecifier
{
public:
ValueSpecifier();
QLayout *layout() const override;
QString expressionText() override;
void setEnabled(bool enabled) override;
void setType(const QString &type);
void setValue(const QString &value);
private:
QComboBox *typeCombo_, *varCombo_;
QHBoxLayout *layout_;
QLineEdit *valueInput_;
};
class BinaryOperationSpecifier : public ExpressionSpecifier
{
public:
BinaryOperationSpecifier(ExpressionSpecifier *left,
ExpressionSpecifier *right);
QLayout *layout() const override;
QString expressionText() override;
void setEnabled(bool enabled) override;
void setOperation(const QString &op);
private:
QComboBox *opCombo_;
QVBoxLayout *layout_;
ExpressionSpecifier *left_, *right_;
};
QString startFilter_;
ExpressionSpecifier *expressionSpecifier_;
QLineEdit *titleInput_;
};
} // namespace chatterino
@@ -0,0 +1,80 @@
#include "SelectChannelFiltersDialog.hpp"
#include "singletons/Settings.hpp"
namespace chatterino {
SelectChannelFiltersDialog::SelectChannelFiltersDialog(
const QList<QUuid> &previousSelection, QWidget *parent)
: QDialog(parent)
{
auto vbox = new QVBoxLayout(this);
auto itemVbox = new QVBoxLayout;
auto buttonBox = new QHBoxLayout;
auto okButton = new QPushButton("OK");
auto cancelButton = new QPushButton("Cancel");
vbox->addLayout(itemVbox);
vbox->addLayout(buttonBox);
buttonBox->addStretch(1);
buttonBox->addWidget(cancelButton);
buttonBox->addWidget(okButton);
QObject::connect(okButton, &QAbstractButton::clicked, [this] {
this->accept();
this->close();
});
QObject::connect(cancelButton, &QAbstractButton::clicked, [this] {
this->reject();
this->close();
});
this->setWindowFlags(
(this->windowFlags() & ~(Qt::WindowContextHelpButtonHint)) |
Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint);
auto availableFilters = getCSettings().filterRecords.readOnly();
if (availableFilters->size() == 0)
{
auto text = new QLabel("No filters defined");
itemVbox->addWidget(text);
}
else
{
for (const auto &f : *availableFilters)
{
auto checkbox = new QCheckBox(f->getName(), this);
bool alreadySelected = previousSelection.contains(f->getId());
checkbox->setCheckState(alreadySelected
? Qt::CheckState::Checked
: Qt::CheckState::Unchecked);
if (alreadySelected)
{
this->currentSelection_.append(f->getId());
}
QObject::connect(checkbox, &QCheckBox::stateChanged,
[this, id = f->getId()](int state) {
if (state == 0)
{
this->currentSelection_.removeOne(id);
}
else
{
this->currentSelection_.append(id);
}
});
itemVbox->addWidget(checkbox);
}
}
}
const QList<QUuid> &SelectChannelFiltersDialog::getSelection() const
{
return this->currentSelection_;
}
} // namespace chatterino
@@ -0,0 +1,19 @@
#pragma once
#include <QDialog>
namespace chatterino {
class SelectChannelFiltersDialog : public QDialog
{
public:
SelectChannelFiltersDialog(const QList<QUuid> &previousSelection,
QWidget *parent = nullptr);
const QList<QUuid> &getSelection() const;
private:
QList<QUuid> currentSelection_;
};
} // namespace chatterino
+2
View File
@@ -11,6 +11,7 @@
#include "widgets/settingspages/AccountsPage.hpp"
#include "widgets/settingspages/CommandPage.hpp"
#include "widgets/settingspages/ExternalToolsPage.hpp"
#include "widgets/settingspages/FiltersPage.hpp"
#include "widgets/settingspages/GeneralPage.hpp"
#include "widgets/settingspages/HighlightingPage.hpp"
#include "widgets/settingspages/IgnoresPage.hpp"
@@ -161,6 +162,7 @@ void SettingsDialog::addTabs()
this->addTab([]{return new CommandPage;}, "Commands", ":/settings/commands.svg");
this->addTab([]{return new HighlightingPage;}, "Highlights", ":/settings/notifications.svg");
this->addTab([]{return new IgnoresPage;}, "Ignores", ":/settings/ignore.svg");
this->addTab([]{return new FiltersPage;}, "Filters", ":/settings/filters.svg");
this->ui_.tabContainer->addSpacing(16);
this->addTab([]{return new KeyboardSettingsPage;}, "Keybindings", ":/settings/keybinds.svg");
this->addTab([]{return new ModerationPage;}, "Moderation", ":/settings/moderation.svg", SettingsTabId::Moderation);