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);
+138 -31
View File
@@ -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;
+14
View File
@@ -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;
+12 -2
View File
@@ -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()
+6 -1
View File
@@ -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
+111
View File
@@ -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
+25
View File
@@ -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
+24
View File
@@ -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();
+4
View File
@@ -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();
+2
View File
@@ -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;
+6
View File
@@ -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);
}