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);
|
||||
|
||||
@@ -112,6 +112,7 @@ namespace {
|
||||
ChannelView::ChannelView(BaseWidget *parent)
|
||||
: BaseWidget(parent)
|
||||
, sourceChannel_(nullptr)
|
||||
, underlyingChannel_(nullptr)
|
||||
, scrollBar_(new Scrollbar(this))
|
||||
{
|
||||
this->setMouseTracking(true);
|
||||
@@ -557,7 +558,12 @@ ChannelPtr ChannelView::channel()
|
||||
return this->channel_;
|
||||
}
|
||||
|
||||
void ChannelView::setChannel(ChannelPtr channel)
|
||||
bool ChannelView::showScrollbarHighlights() const
|
||||
{
|
||||
return this->channel_->getType() != Channel::Type::TwitchMentions;
|
||||
}
|
||||
|
||||
void ChannelView::setChannel(ChannelPtr underlyingChannel)
|
||||
{
|
||||
/// Clear connections from the last channel
|
||||
this->channelConnections_.clear();
|
||||
@@ -565,31 +571,88 @@ void ChannelView::setChannel(ChannelPtr channel)
|
||||
this->clearMessages();
|
||||
this->scrollBar_->clearHighlights();
|
||||
|
||||
/// make copy of channel and expose
|
||||
this->channel_ = std::make_unique<Channel>(underlyingChannel->getName(),
|
||||
underlyingChannel->getType());
|
||||
|
||||
//
|
||||
// Proxy channel connections
|
||||
// Use a proxy channel to keep filtered messages past the time they are removed from their origin channel
|
||||
//
|
||||
|
||||
this->channelConnections_.push_back(
|
||||
underlyingChannel->messageAppended.connect(
|
||||
[this](MessagePtr &message,
|
||||
boost::optional<MessageFlags> overridingFlags) {
|
||||
if (this->shouldIncludeMessage(message))
|
||||
{
|
||||
// When the message was received in the underlyingChannel,
|
||||
// logging will be handled. Prevent duplications.
|
||||
if (overridingFlags)
|
||||
{
|
||||
overridingFlags.get().set(MessageFlag::DoNotLog);
|
||||
}
|
||||
else
|
||||
{
|
||||
overridingFlags = MessageFlags(MessageFlag::DoNotLog);
|
||||
}
|
||||
|
||||
this->channel_->addMessage(message, overridingFlags);
|
||||
}
|
||||
}));
|
||||
|
||||
this->channelConnections_.push_back(
|
||||
underlyingChannel->messagesAddedAtStart.connect(
|
||||
[this](std::vector<MessagePtr> &messages) {
|
||||
std::vector<MessagePtr> filtered;
|
||||
std::copy_if( //
|
||||
messages.begin(), messages.end(),
|
||||
std::back_inserter(filtered), [this](MessagePtr msg) {
|
||||
return this->shouldIncludeMessage(msg);
|
||||
});
|
||||
|
||||
if (!filtered.empty())
|
||||
this->channel_->addMessagesAtStart(filtered);
|
||||
}));
|
||||
|
||||
this->channelConnections_.push_back(
|
||||
underlyingChannel->messageReplaced.connect(
|
||||
[this](size_t index, MessagePtr replacement) {
|
||||
if (this->shouldIncludeMessage(replacement))
|
||||
this->channel_->replaceMessage(index, replacement);
|
||||
}));
|
||||
|
||||
//
|
||||
// Standard channel connections
|
||||
//
|
||||
|
||||
// on new message
|
||||
this->channelConnections_.push_back(channel->messageAppended.connect(
|
||||
this->channelConnections_.push_back(this->channel_->messageAppended.connect(
|
||||
[this](MessagePtr &message,
|
||||
boost::optional<MessageFlags> overridingFlags) {
|
||||
this->messageAppended(message, overridingFlags);
|
||||
}));
|
||||
|
||||
this->channelConnections_.push_back(channel->messagesAddedAtStart.connect(
|
||||
[this](std::vector<MessagePtr> &messages) {
|
||||
this->messageAddedAtStart(messages);
|
||||
}));
|
||||
this->channelConnections_.push_back(
|
||||
this->channel_->messagesAddedAtStart.connect(
|
||||
[this](std::vector<MessagePtr> &messages) {
|
||||
this->messageAddedAtStart(messages);
|
||||
}));
|
||||
|
||||
// on message removed
|
||||
this->channelConnections_.push_back(
|
||||
channel->messageRemovedFromStart.connect([this](MessagePtr &message) {
|
||||
this->messageRemoveFromStart(message);
|
||||
}));
|
||||
this->channel_->messageRemovedFromStart.connect(
|
||||
[this](MessagePtr &message) {
|
||||
this->messageRemoveFromStart(message);
|
||||
}));
|
||||
|
||||
// on message replaced
|
||||
this->channelConnections_.push_back(channel->messageReplaced.connect(
|
||||
this->channelConnections_.push_back(this->channel_->messageReplaced.connect(
|
||||
[this](size_t index, MessagePtr replacement) {
|
||||
this->messageReplaced(index, replacement);
|
||||
}));
|
||||
|
||||
auto snapshot = channel->getMessageSnapshot();
|
||||
auto snapshot = underlyingChannel->getMessageSnapshot();
|
||||
|
||||
for (size_t i = 0; i < snapshot.size(); i++)
|
||||
{
|
||||
@@ -604,22 +667,26 @@ void ChannelView::setChannel(ChannelPtr channel)
|
||||
this->lastMessageHasAlternateBackground_ =
|
||||
!this->lastMessageHasAlternateBackground_;
|
||||
|
||||
if (channel->shouldIgnoreHighlights())
|
||||
if (underlyingChannel->shouldIgnoreHighlights())
|
||||
{
|
||||
messageLayout->flags.set(MessageLayoutFlag::IgnoreHighlights);
|
||||
}
|
||||
|
||||
this->messages_.pushBack(MessageLayoutPtr(messageLayout), deleted);
|
||||
this->scrollBar_->addHighlight(snapshot[i]->getScrollBarHighlight());
|
||||
if (this->showScrollbarHighlights())
|
||||
{
|
||||
this->scrollBar_->addHighlight(
|
||||
snapshot[i]->getScrollBarHighlight());
|
||||
}
|
||||
}
|
||||
|
||||
this->channel_ = channel;
|
||||
this->underlyingChannel_ = underlyingChannel;
|
||||
|
||||
this->queueLayout();
|
||||
this->queueUpdate();
|
||||
|
||||
// Notifications
|
||||
if (auto tc = dynamic_cast<TwitchChannel *>(channel.get()))
|
||||
if (auto tc = dynamic_cast<TwitchChannel *>(underlyingChannel.get()))
|
||||
{
|
||||
this->connections_.push_back(tc->liveStatusChanged.connect([this]() {
|
||||
this->liveStatusChanged.invoke(); //
|
||||
@@ -627,6 +694,41 @@ void ChannelView::setChannel(ChannelPtr channel)
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelView::setFilters(const QList<QUuid> &ids)
|
||||
{
|
||||
this->channelFilters_ = std::make_shared<FilterSet>(ids);
|
||||
}
|
||||
|
||||
const QList<QUuid> ChannelView::getFilterIds() const
|
||||
{
|
||||
if (!this->channelFilters_)
|
||||
{
|
||||
return QList<QUuid>();
|
||||
}
|
||||
|
||||
return this->channelFilters_->filterIds();
|
||||
}
|
||||
|
||||
FilterSetPtr ChannelView::getFilterSet() const
|
||||
{
|
||||
return this->channelFilters_;
|
||||
}
|
||||
|
||||
bool ChannelView::shouldIncludeMessage(const MessagePtr &m) const
|
||||
{
|
||||
if (this->channelFilters_)
|
||||
{
|
||||
if (getSettings()->excludeUserMessagesFromFilter &&
|
||||
getApp()->accounts->twitch.getCurrent()->getUserName().compare(
|
||||
m->loginName, Qt::CaseInsensitive) == 0)
|
||||
return true;
|
||||
|
||||
return this->channelFilters_->filter(m);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ChannelPtr ChannelView::sourceChannel() const
|
||||
{
|
||||
return this->sourceChannel_;
|
||||
@@ -695,7 +797,7 @@ void ChannelView::messageAppended(MessagePtr &message,
|
||||
}
|
||||
}
|
||||
|
||||
if (this->channel_->getType() != Channel::Type::TwitchMentions)
|
||||
if (this->showScrollbarHighlights())
|
||||
{
|
||||
this->scrollBar_->addHighlight(message->getScrollBarHighlight());
|
||||
}
|
||||
@@ -712,7 +814,8 @@ void ChannelView::messageAddedAtStart(std::vector<MessagePtr> &messages)
|
||||
/// Create message layouts
|
||||
for (size_t i = 0; i < messages.size(); i++)
|
||||
{
|
||||
auto layout = new MessageLayout(messages.at(i));
|
||||
auto message = messages.at(i);
|
||||
auto layout = new MessageLayout(message);
|
||||
|
||||
// alternate color
|
||||
if (!this->lastMessageHasAlternateBackgroundReverse_)
|
||||
@@ -732,15 +835,17 @@ void ChannelView::messageAddedAtStart(std::vector<MessagePtr> &messages)
|
||||
this->scrollBar_->offset(qreal(messages.size()));
|
||||
}
|
||||
|
||||
/// Add highlights
|
||||
std::vector<ScrollbarHighlight> highlights;
|
||||
highlights.reserve(messages.size());
|
||||
for (size_t i = 0; i < messages.size(); i++)
|
||||
if (this->showScrollbarHighlights())
|
||||
{
|
||||
highlights.push_back(messages.at(i)->getScrollBarHighlight());
|
||||
}
|
||||
std::vector<ScrollbarHighlight> highlights;
|
||||
highlights.reserve(messages.size());
|
||||
for (const auto &message : messages)
|
||||
{
|
||||
highlights.push_back(message->getScrollBarHighlight());
|
||||
}
|
||||
|
||||
this->scrollBar_->addHighlightsAtStart(highlights);
|
||||
this->scrollBar_->addHighlightsAtStart(highlights);
|
||||
}
|
||||
|
||||
this->messageWasAdded_ = true;
|
||||
this->queueLayout();
|
||||
@@ -855,7 +960,7 @@ MessageElementFlags ChannelView::getFlags() const
|
||||
{
|
||||
flags.set(MessageElementFlag::ModeratorTools);
|
||||
}
|
||||
if (this->channel_ == app->twitch.server->mentionsChannel)
|
||||
if (this->underlyingChannel_ == app->twitch.server->mentionsChannel)
|
||||
{
|
||||
flags.set(MessageElementFlag::ChannelName);
|
||||
flags.unset(MessageElementFlag::ChannelPointReward);
|
||||
@@ -906,7 +1011,8 @@ void ChannelView::drawMessages(QPainter &painter)
|
||||
bool windowFocused = this->window() == QApplication::activeWindow();
|
||||
|
||||
auto app = getApp();
|
||||
bool isMentions = this->channel_ == app->twitch.server->mentionsChannel;
|
||||
bool isMentions =
|
||||
this->underlyingChannel_ == app->twitch.server->mentionsChannel;
|
||||
|
||||
for (size_t i = start; i < messagesSnapshot.size(); ++i)
|
||||
{
|
||||
@@ -1830,8 +1936,9 @@ void ChannelView::hideEvent(QHideEvent *)
|
||||
void ChannelView::showUserInfoPopup(const QString &userName)
|
||||
{
|
||||
auto *userPopup = new UserInfoPopup(getSettings()->autoCloseUserPopup);
|
||||
userPopup->setData(userName, this->hasSourceChannel() ? this->sourceChannel_
|
||||
: this->channel_);
|
||||
userPopup->setData(userName, this->hasSourceChannel()
|
||||
? this->sourceChannel_
|
||||
: this->underlyingChannel_);
|
||||
QPoint offset(int(150 * this->scale()), int(70 * this->scale()));
|
||||
userPopup->move(QCursor::pos() - offset);
|
||||
userPopup->show();
|
||||
@@ -1871,9 +1978,9 @@ void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link,
|
||||
.replace("{msg-id}", layout->getMessage()->id)
|
||||
.replace("{message}", layout->getMessage()->messageText);
|
||||
|
||||
value =
|
||||
getApp()->commands->execCommand(value, this->channel_, false);
|
||||
this->channel_->sendMessage(value);
|
||||
value = getApp()->commands->execCommand(
|
||||
value, this->underlyingChannel_, false);
|
||||
this->underlyingChannel_->sendMessage(value);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <unordered_set>
|
||||
|
||||
#include "common/FlagsEnum.hpp"
|
||||
#include "controllers/filters/FilterSet.hpp"
|
||||
#include "messages/Image.hpp"
|
||||
#include "messages/LimitedQueue.hpp"
|
||||
#include "messages/LimitedQueueSnapshot.hpp"
|
||||
@@ -76,6 +77,10 @@ public:
|
||||
ChannelPtr channel();
|
||||
void setChannel(ChannelPtr channel_);
|
||||
|
||||
void setFilters(const QList<QUuid> &ids);
|
||||
const QList<QUuid> getFilterIds() const;
|
||||
FilterSetPtr getFilterSet() const;
|
||||
|
||||
ChannelPtr sourceChannel() const;
|
||||
void setSourceChannel(ChannelPtr sourceChannel);
|
||||
bool hasSourceChannel() const;
|
||||
@@ -178,11 +183,20 @@ private:
|
||||
LimitedQueueSnapshot<MessageLayoutPtr> snapshot_;
|
||||
|
||||
ChannelPtr channel_;
|
||||
ChannelPtr underlyingChannel_;
|
||||
ChannelPtr sourceChannel_;
|
||||
|
||||
Scrollbar *scrollBar_;
|
||||
EffectLabel *goToBottom_;
|
||||
|
||||
FilterSetPtr channelFilters_;
|
||||
|
||||
// Returns true if message should be included
|
||||
bool shouldIncludeMessage(const MessagePtr &m) const;
|
||||
|
||||
// Returns whether the scrollbar should have highlights
|
||||
bool showScrollbarHighlights() const;
|
||||
|
||||
// This variable can be used to decide whether or not we should render the
|
||||
// "Show latest messages" button
|
||||
bool showingLatestMessages_ = true;
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
namespace chatterino {
|
||||
|
||||
ChannelPtr SearchPopup::filter(const QString &text, const QString &channelName,
|
||||
const LimitedQueueSnapshot<MessagePtr> &snapshot)
|
||||
const LimitedQueueSnapshot<MessagePtr> &snapshot,
|
||||
FilterSetPtr filterSet)
|
||||
{
|
||||
ChannelPtr channel(new Channel(channelName, Channel::Type::None));
|
||||
|
||||
@@ -40,6 +41,9 @@ ChannelPtr SearchPopup::filter(const QString &text, const QString &channelName,
|
||||
}
|
||||
}
|
||||
|
||||
if (accept && filterSet)
|
||||
accept = filterSet->filter(message);
|
||||
|
||||
// If all predicates match, add the message to the channel
|
||||
if (accept)
|
||||
channel->addMessage(message);
|
||||
@@ -59,6 +63,11 @@ SearchPopup::SearchPopup()
|
||||
});
|
||||
}
|
||||
|
||||
void SearchPopup::setChannelFilters(FilterSetPtr filters)
|
||||
{
|
||||
this->channelFilters_ = filters;
|
||||
}
|
||||
|
||||
void SearchPopup::setChannel(const ChannelPtr &channel)
|
||||
{
|
||||
this->channelName_ = channel->getName();
|
||||
@@ -76,7 +85,8 @@ void SearchPopup::updateWindowTitle()
|
||||
void SearchPopup::search()
|
||||
{
|
||||
this->channelView_->setChannel(filter(this->searchInput_->text(),
|
||||
this->channelName_, this->snapshot_));
|
||||
this->channelName_, this->snapshot_,
|
||||
this->channelFilters_));
|
||||
}
|
||||
|
||||
void SearchPopup::initLayout()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ForwardDecl.hpp"
|
||||
#include "controllers/filters/FilterSet.hpp"
|
||||
#include "messages/LimitedQueueSnapshot.hpp"
|
||||
#include "messages/search/MessagePredicate.hpp"
|
||||
#include "widgets/BasePopup.hpp"
|
||||
@@ -17,6 +18,7 @@ public:
|
||||
SearchPopup();
|
||||
|
||||
virtual void setChannel(const ChannelPtr &channel);
|
||||
virtual void setChannelFilters(FilterSetPtr filters);
|
||||
|
||||
protected:
|
||||
virtual void updateWindowTitle();
|
||||
@@ -32,12 +34,14 @@ private:
|
||||
* @param text the search query -- will be parsed for MessagePredicates
|
||||
* @param channelName name of the channel to be returned
|
||||
* @param snapshot list of messages to filter
|
||||
* @param filterSet channel filter to apply
|
||||
*
|
||||
* @return a ChannelPtr with "channelName" and the filtered messages from
|
||||
* "snapshot"
|
||||
*/
|
||||
static ChannelPtr filter(const QString &text, const QString &channelName,
|
||||
const LimitedQueueSnapshot<MessagePtr> &snapshot);
|
||||
const LimitedQueueSnapshot<MessagePtr> &snapshot,
|
||||
FilterSetPtr filterSet);
|
||||
|
||||
/**
|
||||
* @brief Checks the input for tags and registers their corresponding
|
||||
@@ -53,6 +57,7 @@ private:
|
||||
QLineEdit *searchInput_{};
|
||||
ChannelView *channelView_{};
|
||||
QString channelName_{};
|
||||
FilterSetPtr channelFilters_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
#include "FiltersPage.hpp"
|
||||
|
||||
#include "controllers/filters/FilterModel.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/LayoutCreator.hpp"
|
||||
#include "widgets/dialogs/ChannelFilterEditorDialog.hpp"
|
||||
#include "widgets/helper/EditableModelView.hpp"
|
||||
|
||||
#include <QTableView>
|
||||
|
||||
#define FILTERS_DOCUMENTATION "https://wiki.chatterino.com/Filters/"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
FiltersPage::FiltersPage()
|
||||
{
|
||||
LayoutCreator<FiltersPage> layoutCreator(this);
|
||||
auto layout = layoutCreator.setLayoutType<QVBoxLayout>();
|
||||
|
||||
layout.emplace<QLabel>(
|
||||
"Selectively display messages in Splits using channel filters. Set "
|
||||
"filters under a Split menu.");
|
||||
EditableModelView *view =
|
||||
layout
|
||||
.emplace<EditableModelView>(
|
||||
(new FilterModel(nullptr))
|
||||
->initialized(&getSettings()->filterRecords))
|
||||
.getElement();
|
||||
|
||||
view->setTitles({"Name", "Filter", "Valid"});
|
||||
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||
QHeaderView::Interactive);
|
||||
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||
1, QHeaderView::Stretch);
|
||||
|
||||
QTimer::singleShot(1, [view] {
|
||||
view->getTableView()->resizeColumnsToContents();
|
||||
view->getTableView()->setColumnWidth(0, 150);
|
||||
view->getTableView()->setColumnWidth(2, 125);
|
||||
});
|
||||
|
||||
view->addButtonPressed.connect([] {
|
||||
ChannelFilterEditorDialog d;
|
||||
if (d.exec() == QDialog::Accepted)
|
||||
{
|
||||
getSettings()->filterRecords.append(
|
||||
std::make_shared<FilterRecord>(d.getTitle(), d.getFilter()));
|
||||
}
|
||||
});
|
||||
|
||||
auto quickAddButton = new QPushButton("Quick Add");
|
||||
QObject::connect(quickAddButton, &QPushButton::pressed, [] {
|
||||
getSettings()->filterRecords.append(std::make_shared<FilterRecord>(
|
||||
"My filter", "message.content contains \"hello\""));
|
||||
});
|
||||
view->addCustomButton(quickAddButton);
|
||||
|
||||
QObject::connect(view->getTableView(), &QTableView::clicked,
|
||||
[this, view](const QModelIndex &clicked) {
|
||||
this->tableCellClicked(clicked, view);
|
||||
});
|
||||
|
||||
auto filterHelpLabel =
|
||||
new QLabel(QString("<a href='%1'><span "
|
||||
"style='color:#99f'>filter info</span></a>")
|
||||
.arg(FILTERS_DOCUMENTATION));
|
||||
filterHelpLabel->setOpenExternalLinks(true);
|
||||
view->addCustomButton(filterHelpLabel);
|
||||
|
||||
layout.append(
|
||||
this->createCheckBox("Do not filter my own messages",
|
||||
getSettings()->excludeUserMessagesFromFilter));
|
||||
}
|
||||
|
||||
void FiltersPage::onShow()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
void FiltersPage::tableCellClicked(const QModelIndex &clicked,
|
||||
EditableModelView *view)
|
||||
{
|
||||
// valid column
|
||||
if (clicked.column() == 2)
|
||||
{
|
||||
QMessageBox popup;
|
||||
|
||||
filterparser::FilterParser f(
|
||||
view->getModel()->data(clicked.siblingAtColumn(1)).toString());
|
||||
|
||||
if (f.valid())
|
||||
{
|
||||
popup.setIcon(QMessageBox::Icon::Information);
|
||||
popup.setWindowTitle("Valid filter");
|
||||
popup.setText("Filter is valid");
|
||||
popup.setInformativeText(
|
||||
QString("Parsed as:\n%1").arg(f.filterString()));
|
||||
}
|
||||
else
|
||||
{
|
||||
popup.setIcon(QMessageBox::Icon::Warning);
|
||||
popup.setWindowTitle("Invalid filter");
|
||||
popup.setText(QString("Parsing errors occured:"));
|
||||
popup.setInformativeText(f.errors().join("\n"));
|
||||
}
|
||||
|
||||
popup.exec();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "widgets/helper/EditableModelView.hpp"
|
||||
#include "widgets/settingspages/SettingsPage.hpp"
|
||||
|
||||
#include <QStringListModel>
|
||||
|
||||
class QVBoxLayout;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class FiltersPage : public SettingsPage
|
||||
{
|
||||
public:
|
||||
FiltersPage();
|
||||
|
||||
void onShow() final;
|
||||
|
||||
private:
|
||||
void tableCellClicked(const QModelIndex &clicked, EditableModelView *view);
|
||||
|
||||
QStringListModel userListModel_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "widgets/Window.hpp"
|
||||
#include "widgets/dialogs/QualityPopup.hpp"
|
||||
#include "widgets/dialogs/SelectChannelDialog.hpp"
|
||||
#include "widgets/dialogs/SelectChannelFiltersDialog.hpp"
|
||||
#include "widgets/dialogs/TextInputDialog.hpp"
|
||||
#include "widgets/dialogs/UserInfoPopup.hpp"
|
||||
#include "widgets/helper/ChannelView.hpp"
|
||||
@@ -749,10 +750,33 @@ void Split::copyToClipboard()
|
||||
crossPlatformCopy(this->view_->getSelectedText());
|
||||
}
|
||||
|
||||
void Split::setFiltersDialog()
|
||||
{
|
||||
SelectChannelFiltersDialog d(this->getFilters(), this);
|
||||
d.setWindowTitle("Select filters");
|
||||
|
||||
if (d.exec() == QDialog::Accepted)
|
||||
{
|
||||
this->setFilters(d.getSelection());
|
||||
}
|
||||
}
|
||||
|
||||
void Split::setFilters(const QList<QUuid> ids)
|
||||
{
|
||||
this->view_->setFilters(ids);
|
||||
this->header_->updateChannelText();
|
||||
}
|
||||
|
||||
const QList<QUuid> Split::getFilters() const
|
||||
{
|
||||
return this->view_->getFilterIds();
|
||||
}
|
||||
|
||||
void Split::showSearch()
|
||||
{
|
||||
SearchPopup *popup = new SearchPopup();
|
||||
|
||||
popup->setChannelFilters(this->view_->getFilterSet());
|
||||
popup->setAttribute(Qt::WA_DeleteOnClose);
|
||||
popup->setChannel(this->getChannel());
|
||||
popup->show();
|
||||
|
||||
@@ -53,6 +53,9 @@ public:
|
||||
ChannelPtr getChannel();
|
||||
void setChannel(IndirectChannel newChannel);
|
||||
|
||||
void setFilters(const QList<QUuid> ids);
|
||||
const QList<QUuid> getFilters() const;
|
||||
|
||||
void setModerationMode(bool value);
|
||||
bool getModerationMode() const;
|
||||
|
||||
@@ -132,6 +135,7 @@ public slots:
|
||||
void openInStreamlink();
|
||||
void openWithCustomScheme();
|
||||
void copyToClipboard();
|
||||
void setFiltersDialog();
|
||||
void showSearch();
|
||||
void showViewerList();
|
||||
void openSubPage();
|
||||
|
||||
@@ -716,6 +716,7 @@ void SplitContainer::applyFromDescriptorRecursively(
|
||||
auto *split = new Split(this);
|
||||
split->setChannel(WindowManager::decodeChannel(splitNode));
|
||||
split->setModerationMode(splitNode.moderationMode_);
|
||||
split->setFilters(splitNode.filters_);
|
||||
|
||||
this->appendSplit(split);
|
||||
}
|
||||
@@ -748,6 +749,7 @@ void SplitContainer::applyFromDescriptorRecursively(
|
||||
auto *split = new Split(this);
|
||||
split->setChannel(WindowManager::decodeChannel(splitNode));
|
||||
split->setModerationMode(splitNode.moderationMode_);
|
||||
split->setFilters(splitNode.filters_);
|
||||
|
||||
Node *_node = new Node();
|
||||
_node->parent_ = node;
|
||||
|
||||
@@ -327,6 +327,7 @@ std::unique_ptr<QMenu> SplitHeader::createMainMenu()
|
||||
QKeySequence("Ctrl+N"));
|
||||
menu->addAction("Search", this->split_, &Split::showSearch,
|
||||
QKeySequence("Ctrl+F"));
|
||||
menu->addAction("Set filters", this->split_, &Split::setFiltersDialog);
|
||||
menu->addSeparator();
|
||||
#ifdef USEWEBENGINE
|
||||
this->dropdownMenu.addAction("Start watching", this, [this] {
|
||||
@@ -700,6 +701,11 @@ void SplitHeader::updateChannelText()
|
||||
}
|
||||
}
|
||||
|
||||
if (!title.isEmpty() && this->split_->getFilters().size() != 0)
|
||||
{
|
||||
title += " - filtered";
|
||||
}
|
||||
|
||||
this->titleLabel_->setText(title.isEmpty() ? "<empty>" : title);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user