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:
@@ -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
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user