mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
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:
parent
812cbdf4f9
commit
4199a01b96
41 changed files with 2189 additions and 43 deletions
|
@ -2,8 +2,9 @@
|
||||||
|
|
||||||
## Unversioned
|
## Unversioned
|
||||||
|
|
||||||
- Minor: Improved viewer list window.
|
- Major: Added "Channel Filters". See https://wiki.chatterino.com/Filters/ for how they work or how to configure them. (#1748)
|
||||||
- Major: Added Streamer Mode configuration (under `Settings -> General`), where you can select which features of Chatterino should behave differently when you are in Streamer Mode. (#2001)
|
- Major: Added Streamer Mode configuration (under `Settings -> General`), where you can select which features of Chatterino should behave differently when you are in Streamer Mode. (#2001)
|
||||||
|
- Minor: Improved viewer list window.
|
||||||
- Minor: Added emote completion with `:` to the whispers channel (#2075)
|
- Minor: Added emote completion with `:` to the whispers channel (#2075)
|
||||||
- Minor: Made the current channels emotes appear at the top of the emote picker popup. (#2057)
|
- Minor: Made the current channels emotes appear at the top of the emote picker popup. (#2057)
|
||||||
- Minor: Added viewer list button to twitch channel header. (#1978)
|
- Minor: Added viewer list button to twitch channel header. (#1978)
|
||||||
|
|
|
@ -131,6 +131,10 @@ SOURCES += \
|
||||||
src/controllers/commands/Command.cpp \
|
src/controllers/commands/Command.cpp \
|
||||||
src/controllers/commands/CommandController.cpp \
|
src/controllers/commands/CommandController.cpp \
|
||||||
src/controllers/commands/CommandModel.cpp \
|
src/controllers/commands/CommandModel.cpp \
|
||||||
|
src/controllers/filters/FilterModel.cpp \
|
||||||
|
src/controllers/filters/parser/FilterParser.cpp \
|
||||||
|
src/controllers/filters/parser/Tokenizer.cpp \
|
||||||
|
src/controllers/filters/parser/Types.cpp \
|
||||||
src/controllers/highlights/HighlightBlacklistModel.cpp \
|
src/controllers/highlights/HighlightBlacklistModel.cpp \
|
||||||
src/controllers/highlights/HighlightModel.cpp \
|
src/controllers/highlights/HighlightModel.cpp \
|
||||||
src/controllers/highlights/HighlightPhrase.cpp \
|
src/controllers/highlights/HighlightPhrase.cpp \
|
||||||
|
@ -232,6 +236,7 @@ SOURCES += \
|
||||||
src/widgets/BasePopup.cpp \
|
src/widgets/BasePopup.cpp \
|
||||||
src/widgets/BaseWidget.cpp \
|
src/widgets/BaseWidget.cpp \
|
||||||
src/widgets/BaseWindow.cpp \
|
src/widgets/BaseWindow.cpp \
|
||||||
|
src/widgets/dialogs/ChannelFilterEditorDialog.cpp \
|
||||||
src/widgets/dialogs/ColorPickerDialog.cpp \
|
src/widgets/dialogs/ColorPickerDialog.cpp \
|
||||||
src/widgets/dialogs/EmotePopup.cpp \
|
src/widgets/dialogs/EmotePopup.cpp \
|
||||||
src/widgets/dialogs/IrcConnectionEditor.cpp \
|
src/widgets/dialogs/IrcConnectionEditor.cpp \
|
||||||
|
@ -240,6 +245,7 @@ SOURCES += \
|
||||||
src/widgets/dialogs/NotificationPopup.cpp \
|
src/widgets/dialogs/NotificationPopup.cpp \
|
||||||
src/widgets/dialogs/QualityPopup.cpp \
|
src/widgets/dialogs/QualityPopup.cpp \
|
||||||
src/widgets/dialogs/SelectChannelDialog.cpp \
|
src/widgets/dialogs/SelectChannelDialog.cpp \
|
||||||
|
src/widgets/dialogs/SelectChannelFiltersDialog.cpp \
|
||||||
src/widgets/dialogs/SettingsDialog.cpp \
|
src/widgets/dialogs/SettingsDialog.cpp \
|
||||||
src/widgets/listview/GenericItemDelegate.cpp \
|
src/widgets/listview/GenericItemDelegate.cpp \
|
||||||
src/widgets/dialogs/switcher/NewTabItem.cpp \
|
src/widgets/dialogs/switcher/NewTabItem.cpp \
|
||||||
|
@ -275,6 +281,7 @@ SOURCES += \
|
||||||
src/widgets/settingspages/AccountsPage.cpp \
|
src/widgets/settingspages/AccountsPage.cpp \
|
||||||
src/widgets/settingspages/CommandPage.cpp \
|
src/widgets/settingspages/CommandPage.cpp \
|
||||||
src/widgets/settingspages/ExternalToolsPage.cpp \
|
src/widgets/settingspages/ExternalToolsPage.cpp \
|
||||||
|
src/widgets/settingspages/FiltersPage.cpp \
|
||||||
src/widgets/settingspages/GeneralPage.cpp \
|
src/widgets/settingspages/GeneralPage.cpp \
|
||||||
src/widgets/settingspages/HighlightingPage.cpp \
|
src/widgets/settingspages/HighlightingPage.cpp \
|
||||||
src/widgets/settingspages/IgnoresPage.cpp \
|
src/widgets/settingspages/IgnoresPage.cpp \
|
||||||
|
@ -336,6 +343,12 @@ HEADERS += \
|
||||||
src/controllers/commands/Command.hpp \
|
src/controllers/commands/Command.hpp \
|
||||||
src/controllers/commands/CommandController.hpp \
|
src/controllers/commands/CommandController.hpp \
|
||||||
src/controllers/commands/CommandModel.hpp \
|
src/controllers/commands/CommandModel.hpp \
|
||||||
|
src/controllers/filters/FilterModel.hpp \
|
||||||
|
src/controllers/filters/FilterRecord.hpp \
|
||||||
|
src/controllers/filters/FilterSet.hpp \
|
||||||
|
src/controllers/filters/parser/FilterParser.hpp \
|
||||||
|
src/controllers/filters/parser/Tokenizer.hpp \
|
||||||
|
src/controllers/filters/parser/Types.hpp \
|
||||||
src/controllers/highlights/HighlightBlacklistModel.hpp \
|
src/controllers/highlights/HighlightBlacklistModel.hpp \
|
||||||
src/controllers/highlights/HighlightBlacklistUser.hpp \
|
src/controllers/highlights/HighlightBlacklistUser.hpp \
|
||||||
src/controllers/highlights/HighlightModel.hpp \
|
src/controllers/highlights/HighlightModel.hpp \
|
||||||
|
@ -470,6 +483,7 @@ HEADERS += \
|
||||||
src/widgets/BasePopup.hpp \
|
src/widgets/BasePopup.hpp \
|
||||||
src/widgets/BaseWidget.hpp \
|
src/widgets/BaseWidget.hpp \
|
||||||
src/widgets/BaseWindow.hpp \
|
src/widgets/BaseWindow.hpp \
|
||||||
|
src/widgets/dialogs/ChannelFilterEditorDialog.hpp \
|
||||||
src/widgets/dialogs/ColorPickerDialog.hpp \
|
src/widgets/dialogs/ColorPickerDialog.hpp \
|
||||||
src/widgets/dialogs/EmotePopup.hpp \
|
src/widgets/dialogs/EmotePopup.hpp \
|
||||||
src/widgets/dialogs/IrcConnectionEditor.hpp \
|
src/widgets/dialogs/IrcConnectionEditor.hpp \
|
||||||
|
@ -478,6 +492,7 @@ HEADERS += \
|
||||||
src/widgets/dialogs/NotificationPopup.hpp \
|
src/widgets/dialogs/NotificationPopup.hpp \
|
||||||
src/widgets/dialogs/QualityPopup.hpp \
|
src/widgets/dialogs/QualityPopup.hpp \
|
||||||
src/widgets/dialogs/SelectChannelDialog.hpp \
|
src/widgets/dialogs/SelectChannelDialog.hpp \
|
||||||
|
src/widgets/dialogs/SelectChannelFiltersDialog.hpp \
|
||||||
src/widgets/dialogs/SettingsDialog.hpp \
|
src/widgets/dialogs/SettingsDialog.hpp \
|
||||||
src/widgets/dialogs/switcher/AbstractSwitcherItem.hpp \
|
src/widgets/dialogs/switcher/AbstractSwitcherItem.hpp \
|
||||||
src/widgets/listview/GenericItemDelegate.hpp \
|
src/widgets/listview/GenericItemDelegate.hpp \
|
||||||
|
@ -517,6 +532,7 @@ HEADERS += \
|
||||||
src/widgets/settingspages/AccountsPage.hpp \
|
src/widgets/settingspages/AccountsPage.hpp \
|
||||||
src/widgets/settingspages/CommandPage.hpp \
|
src/widgets/settingspages/CommandPage.hpp \
|
||||||
src/widgets/settingspages/ExternalToolsPage.hpp \
|
src/widgets/settingspages/ExternalToolsPage.hpp \
|
||||||
|
src/widgets/settingspages/FiltersPage.hpp \
|
||||||
src/widgets/settingspages/GeneralPage.hpp \
|
src/widgets/settingspages/GeneralPage.hpp \
|
||||||
src/widgets/settingspages/HighlightingPage.hpp \
|
src/widgets/settingspages/HighlightingPage.hpp \
|
||||||
src/widgets/settingspages/IgnoresPage.hpp \
|
src/widgets/settingspages/IgnoresPage.hpp \
|
||||||
|
|
|
@ -6,6 +6,8 @@ from _generate_resources import *
|
||||||
ignored_files = ['qt.conf', 'resources.qrc', 'resources_autogenerated.qrc', 'windows.rc',
|
ignored_files = ['qt.conf', 'resources.qrc', 'resources_autogenerated.qrc', 'windows.rc',
|
||||||
'generate_resources.py', '_generate_resources.py']
|
'generate_resources.py', '_generate_resources.py']
|
||||||
|
|
||||||
|
ignored_names = ['.gitignore', '.DS_Store']
|
||||||
|
|
||||||
# to ignore all files in a/b, add a/b to ignored_directories.
|
# to ignore all files in a/b, add a/b to ignored_directories.
|
||||||
# this will ignore a/b/c/d.txt and a/b/xd.txt
|
# this will ignore a/b/c/d.txt and a/b/xd.txt
|
||||||
ignored_directories = ['__pycache__', 'linuxinstall']
|
ignored_directories = ['__pycache__', 'linuxinstall']
|
||||||
|
@ -16,7 +18,7 @@ def isNotIgnored(file):
|
||||||
if file.parent.as_posix().startswith(ignored_directory):
|
if file.parent.as_posix().startswith(ignored_directory):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return file.as_posix() not in ignored_files
|
return file.as_posix() not in ignored_files and file.name not in ignored_names
|
||||||
|
|
||||||
all_files = sorted(list(filter(isNotIgnored, \
|
all_files = sorted(list(filter(isNotIgnored, \
|
||||||
filter(Path.is_file, Path('.').glob('**/*')))))
|
filter(Path.is_file, Path('.').glob('**/*')))))
|
||||||
|
@ -24,7 +26,7 @@ image_files = sorted(list(filter(isNotIgnored, \
|
||||||
filter(Path.is_file, Path('.').glob('**/*.png')))))
|
filter(Path.is_file, Path('.').glob('**/*.png')))))
|
||||||
|
|
||||||
with open('./resources_autogenerated.qrc', 'w') as out:
|
with open('./resources_autogenerated.qrc', 'w') as out:
|
||||||
out.write(resources_header)
|
out.write(resources_header + '\n')
|
||||||
for file in all_files:
|
for file in all_files:
|
||||||
out.write(f" <file>{file.as_posix()}</file>\n")
|
out.write(f" <file>{file.as_posix()}</file>\n")
|
||||||
out.write(resources_footer)
|
out.write(resources_footer)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="/"> <file>.gitignore</file>
|
<qresource prefix="/">
|
||||||
<file>avatars/fourtf.png</file>
|
<file>avatars/fourtf.png</file>
|
||||||
<file>avatars/pajlada.png</file>
|
<file>avatars/pajlada.png</file>
|
||||||
<file>buttons/addSplit.png</file>
|
<file>buttons/addSplit.png</file>
|
||||||
|
@ -70,6 +70,7 @@
|
||||||
<file>settings/commands.svg</file>
|
<file>settings/commands.svg</file>
|
||||||
<file>settings/emote.svg</file>
|
<file>settings/emote.svg</file>
|
||||||
<file>settings/externaltools.svg</file>
|
<file>settings/externaltools.svg</file>
|
||||||
|
<file>settings/filters.svg</file>
|
||||||
<file>settings/ignore.svg</file>
|
<file>settings/ignore.svg</file>
|
||||||
<file>settings/keybinds.svg</file>
|
<file>settings/keybinds.svg</file>
|
||||||
<file>settings/moderation.svg</file>
|
<file>settings/moderation.svg</file>
|
||||||
|
|
16
resources/settings/filters.svg
Normal file
16
resources/settings/filters.svg
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="200" height="200" version="1.1" viewBox="0 0 52.916665 52.916668" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||||
|
<metadata>
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||||
|
<dc:title/>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g transform="matrix(1.0131 0 0 1.1431 -.34702 -3.8339)" fill="none" shape-rendering="auto" stroke="#fff" stroke-linejoin="round">
|
||||||
|
<path d="m1.3325 8.3773 12.563 14.985 8.2998 9.8998v11.278l8.5255-3.7176v-7.56l8.2998-9.8998 12.563-14.985h-25.126z" color="#000000" color-rendering="auto" dominant-baseline="auto" image-rendering="auto" solid-color="#000000" stop-color="#000000" stroke-width="1.3229" style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-east-asian:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;font-variation-settings:normal;inline-size:0;isolation:auto;mix-blend-mode:normal;shape-margin:0;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"/>
|
||||||
|
<path d="m1.1964 6.1085 0.20934 0.25005 12.469 14.871 8.2012 9.7809v15.879l8.7652-3.8224v-12.057l8.2012-9.7809 12.677-15.121h-25.261zm0.65129 0.30432h49.221l-12.258 14.621-8.2729 9.8662v11.967l-8.1566 3.5569v-15.524l-8.2729-9.8662z" color="#000000" color-rendering="auto" dominant-baseline="auto" image-rendering="auto" solid-color="#000000" stop-color="#000000" stroke-width="1.708" style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-east-asian:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;font-variation-settings:normal;inline-size:0;isolation:auto;mix-blend-mode:normal;shape-margin:0;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -224,6 +224,14 @@ void Channel::replaceMessage(MessagePtr message, MessagePtr replacement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Channel::replaceMessage(size_t index, MessagePtr replacement)
|
||||||
|
{
|
||||||
|
if (this->messages_.replaceItem(index, replacement))
|
||||||
|
{
|
||||||
|
this->messageReplaced.invoke(index, replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Channel::deleteMessage(QString messageID)
|
void Channel::deleteMessage(QString messageID)
|
||||||
{
|
{
|
||||||
LimitedQueueSnapshot<MessagePtr> snapshot = this->getMessageSnapshot();
|
LimitedQueueSnapshot<MessagePtr> snapshot = this->getMessageSnapshot();
|
||||||
|
|
|
@ -72,6 +72,7 @@ public:
|
||||||
void addOrReplaceTimeout(MessagePtr message);
|
void addOrReplaceTimeout(MessagePtr message);
|
||||||
void disableAllMessages();
|
void disableAllMessages();
|
||||||
void replaceMessage(MessagePtr message, MessagePtr replacement);
|
void replaceMessage(MessagePtr message, MessagePtr replacement);
|
||||||
|
void replaceMessage(size_t index, MessagePtr replacement);
|
||||||
void deleteMessage(QString messageID);
|
void deleteMessage(QString messageID);
|
||||||
void clearMessages();
|
void clearMessages();
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,23 @@ namespace {
|
||||||
return descriptor;
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QList<QUuid> loadFilters(QJsonValue val)
|
||||||
|
{
|
||||||
|
QList<QUuid> filterIds;
|
||||||
|
|
||||||
|
if (!val.isUndefined())
|
||||||
|
{
|
||||||
|
const auto array = val.toArray();
|
||||||
|
filterIds.reserve(array.size());
|
||||||
|
for (const auto &id : array)
|
||||||
|
{
|
||||||
|
filterIds.append(QUuid::fromString(id.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterIds;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void SplitDescriptor::loadFromJSON(SplitDescriptor &descriptor,
|
void SplitDescriptor::loadFromJSON(SplitDescriptor &descriptor,
|
||||||
|
@ -85,6 +102,7 @@ void SplitDescriptor::loadFromJSON(SplitDescriptor &descriptor,
|
||||||
{
|
{
|
||||||
descriptor.channelName_ = data.value("name").toString();
|
descriptor.channelName_ = data.value("name").toString();
|
||||||
}
|
}
|
||||||
|
descriptor.filters_ = loadFilters(root.value("filters"));
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowLayout WindowLayout::loadFromFile(const QString &path)
|
WindowLayout WindowLayout::loadFromFile(const QString &path)
|
||||||
|
|
|
@ -37,6 +37,8 @@ struct SplitDescriptor {
|
||||||
// Whether "Moderation Mode" (the sword icon) is enabled in this split or not
|
// Whether "Moderation Mode" (the sword icon) is enabled in this split or not
|
||||||
bool moderationMode_{false};
|
bool moderationMode_{false};
|
||||||
|
|
||||||
|
QList<QUuid> filters_;
|
||||||
|
|
||||||
static void loadFromJSON(SplitDescriptor &descriptor,
|
static void loadFromJSON(SplitDescriptor &descriptor,
|
||||||
const QJsonObject &root, const QJsonObject &data);
|
const QJsonObject &root, const QJsonObject &data);
|
||||||
};
|
};
|
||||||
|
|
41
src/controllers/filters/FilterModel.cpp
Normal file
41
src/controllers/filters/FilterModel.cpp
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
#include "FilterModel.hpp"
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
|
#include "singletons/Settings.hpp"
|
||||||
|
#include "util/StandardItemHelper.hpp"
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
// commandmodel
|
||||||
|
FilterModel::FilterModel(QObject *parent)
|
||||||
|
: SignalVectorModel<FilterRecordPtr>(3, parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// turn a vector item into a model row
|
||||||
|
FilterRecordPtr FilterModel::getItemFromRow(std::vector<QStandardItem *> &row,
|
||||||
|
const FilterRecordPtr &original)
|
||||||
|
{
|
||||||
|
auto item =
|
||||||
|
std::make_shared<FilterRecord>(row[0]->data(Qt::DisplayRole).toString(),
|
||||||
|
row[1]->data(Qt::DisplayRole).toString(),
|
||||||
|
original->getId()); // persist id
|
||||||
|
|
||||||
|
// force 'valid' column to update
|
||||||
|
setBoolItem(row[2], item->valid(), false, false);
|
||||||
|
setStringItem(row[2], item->valid() ? "Valid" : "Show errors");
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// turns a row in the model into a vector item
|
||||||
|
void FilterModel::getRowFromItem(const FilterRecordPtr &item,
|
||||||
|
std::vector<QStandardItem *> &row)
|
||||||
|
{
|
||||||
|
setStringItem(row[0], item->getName());
|
||||||
|
setStringItem(row[1], item->getFilter());
|
||||||
|
setBoolItem(row[2], item->valid(), false, false);
|
||||||
|
setStringItem(row[2], item->valid() ? "Valid" : "Show errors");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
26
src/controllers/filters/FilterModel.hpp
Normal file
26
src/controllers/filters/FilterModel.hpp
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "common/SignalVectorModel.hpp"
|
||||||
|
#include "controllers/filters/FilterRecord.hpp"
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class FilterModel : public SignalVectorModel<FilterRecordPtr>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit FilterModel(QObject *parent);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// turn a vector item into a model row
|
||||||
|
virtual FilterRecordPtr getItemFromRow(
|
||||||
|
std::vector<QStandardItem *> &row,
|
||||||
|
const FilterRecordPtr &original) override;
|
||||||
|
|
||||||
|
// turns a row in the model into a vector item
|
||||||
|
virtual void getRowFromItem(const FilterRecordPtr &item,
|
||||||
|
std::vector<QStandardItem *> &row) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
123
src/controllers/filters/FilterRecord.hpp
Normal file
123
src/controllers/filters/FilterRecord.hpp
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "util/RapidJsonSerializeQString.hpp"
|
||||||
|
#include "util/RapidjsonHelpers.hpp"
|
||||||
|
|
||||||
|
#include "controllers/filters/parser/FilterParser.hpp"
|
||||||
|
#include "controllers/filters/parser/Types.hpp"
|
||||||
|
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QString>
|
||||||
|
#include <pajlada/serialize.hpp>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class FilterRecord
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool operator==(const FilterRecord &other) const
|
||||||
|
{
|
||||||
|
return std::tie(this->name_, this->filter_, this->id_) ==
|
||||||
|
std::tie(other.name_, other.filter_, other.id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterRecord(const QString &name, const QString &filter)
|
||||||
|
: name_(name)
|
||||||
|
, filter_(filter)
|
||||||
|
, id_(QUuid::createUuid())
|
||||||
|
, parser_(std::make_unique<filterparser::FilterParser>(filter))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterRecord(const QString &name, const QString &filter, const QUuid &id)
|
||||||
|
: name_(name)
|
||||||
|
, filter_(filter)
|
||||||
|
, id_(id)
|
||||||
|
, parser_(std::make_unique<filterparser::FilterParser>(filter))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString &getName() const
|
||||||
|
{
|
||||||
|
return this->name_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString &getFilter() const
|
||||||
|
{
|
||||||
|
return this->filter_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUuid &getId() const
|
||||||
|
{
|
||||||
|
return this->id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool valid() const
|
||||||
|
{
|
||||||
|
return this->parser_->valid();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filter(const MessagePtr &message) const
|
||||||
|
{
|
||||||
|
return this->parser_->execute(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filter(const filterparser::ContextMap &context) const
|
||||||
|
{
|
||||||
|
return this->parser_->execute(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString name_;
|
||||||
|
QString filter_;
|
||||||
|
QUuid id_;
|
||||||
|
|
||||||
|
std::unique_ptr<filterparser::FilterParser> parser_;
|
||||||
|
};
|
||||||
|
|
||||||
|
using FilterRecordPtr = std::shared_ptr<FilterRecord>;
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
|
namespace pajlada {
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct Serialize<chatterino::FilterRecordPtr> {
|
||||||
|
static rapidjson::Value get(const chatterino::FilterRecordPtr &value,
|
||||||
|
rapidjson::Document::AllocatorType &a)
|
||||||
|
{
|
||||||
|
rapidjson::Value ret(rapidjson::kObjectType);
|
||||||
|
|
||||||
|
chatterino::rj::set(ret, "name", value->getName(), a);
|
||||||
|
chatterino::rj::set(ret, "filter", value->getFilter(), a);
|
||||||
|
chatterino::rj::set(ret, "id",
|
||||||
|
value->getId().toString(QUuid::WithoutBraces), a);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct Deserialize<chatterino::FilterRecordPtr> {
|
||||||
|
static chatterino::FilterRecordPtr get(const rapidjson::Value &value)
|
||||||
|
{
|
||||||
|
if (!value.IsObject())
|
||||||
|
{
|
||||||
|
return std::make_shared<chatterino::FilterRecord>(QString(),
|
||||||
|
QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString _name, _filter, _id;
|
||||||
|
|
||||||
|
chatterino::rj::getSafe(value, "name", _name);
|
||||||
|
chatterino::rj::getSafe(value, "filter", _filter);
|
||||||
|
chatterino::rj::getSafe(value, "id", _id);
|
||||||
|
|
||||||
|
return std::make_shared<chatterino::FilterRecord>(
|
||||||
|
_name, _filter, QUuid::fromString(_id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace pajlada
|
85
src/controllers/filters/FilterSet.hpp
Normal file
85
src/controllers/filters/FilterSet.hpp
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "controllers/filters/FilterRecord.hpp"
|
||||||
|
#include "singletons/Settings.hpp"
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class FilterSet
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FilterSet()
|
||||||
|
{
|
||||||
|
this->listener_ =
|
||||||
|
getCSettings().filterRecords.delayedItemsChanged.connect(
|
||||||
|
[this] { this->reloadFilters(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterSet(const QList<QUuid> &filterIds)
|
||||||
|
{
|
||||||
|
auto filters = getCSettings().filterRecords.readOnly();
|
||||||
|
for (const auto &f : *filters)
|
||||||
|
{
|
||||||
|
if (filterIds.contains(f->getId()))
|
||||||
|
this->filters_.insert(f->getId(), f);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->listener_ =
|
||||||
|
getCSettings().filterRecords.delayedItemsChanged.connect(
|
||||||
|
[this] { this->reloadFilters(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
~FilterSet()
|
||||||
|
{
|
||||||
|
this->listener_.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filter(const MessagePtr &m) const
|
||||||
|
{
|
||||||
|
if (this->filters_.size() == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
filterparser::ContextMap context = filterparser::buildContextMap(m);
|
||||||
|
for (const auto &f : this->filters_.values())
|
||||||
|
{
|
||||||
|
if (!f->valid() || !f->filter(context))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QList<QUuid> filterIds() const
|
||||||
|
{
|
||||||
|
return this->filters_.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QMap<QUuid, FilterRecordPtr> filters_;
|
||||||
|
pajlada::Signals::Connection listener_;
|
||||||
|
|
||||||
|
void reloadFilters()
|
||||||
|
{
|
||||||
|
auto filters = getCSettings().filterRecords.readOnly();
|
||||||
|
for (const auto &key : this->filters_.keys())
|
||||||
|
{
|
||||||
|
bool found = false;
|
||||||
|
for (const auto &f : *filters)
|
||||||
|
{
|
||||||
|
if (f->getId() == key)
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
this->filters_.insert(key, f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found)
|
||||||
|
{
|
||||||
|
this->filters_.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using FilterSetPtr = std::shared_ptr<FilterSet>;
|
||||||
|
|
||||||
|
} // namespace chatterino
|
302
src/controllers/filters/parser/FilterParser.cpp
Normal file
302
src/controllers/filters/parser/FilterParser.cpp
Normal file
|
@ -0,0 +1,302 @@
|
||||||
|
#include "FilterParser.hpp"
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
|
#include "controllers/filters/parser/Types.hpp"
|
||||||
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
|
|
||||||
|
namespace filterparser {
|
||||||
|
|
||||||
|
ContextMap buildContextMap(const MessagePtr &m)
|
||||||
|
{
|
||||||
|
auto watchingChannel =
|
||||||
|
chatterino::getApp()->twitch.server->watchingChannel.get();
|
||||||
|
|
||||||
|
/* Known Identifiers
|
||||||
|
*
|
||||||
|
* author.badges
|
||||||
|
* author.color
|
||||||
|
* author.name
|
||||||
|
* author.no_color
|
||||||
|
* author.subbed
|
||||||
|
* author.sub_length
|
||||||
|
*
|
||||||
|
* channel.name
|
||||||
|
* channel.watching
|
||||||
|
*
|
||||||
|
* flags.highlighted
|
||||||
|
* flags.points_redeemed
|
||||||
|
* flags.sub_message
|
||||||
|
* flags.system_message
|
||||||
|
* flags.whisper
|
||||||
|
*
|
||||||
|
* message.content
|
||||||
|
* message.length
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
using MessageFlag = chatterino::MessageFlag;
|
||||||
|
|
||||||
|
QStringList badges;
|
||||||
|
badges.reserve(m->badges.size());
|
||||||
|
for (const auto &e : m->badges)
|
||||||
|
{
|
||||||
|
badges << e.key_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool watching = !watchingChannel->getName().isEmpty() &&
|
||||||
|
watchingChannel->getName().compare(
|
||||||
|
m->channelName, Qt::CaseInsensitive) == 0;
|
||||||
|
|
||||||
|
bool subscribed = badges.contains("subscriber");
|
||||||
|
int subLength = (subscribed && m->badgeInfos.count("subscriber") != 0)
|
||||||
|
? m->badgeInfos.at("subscriber").toInt()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
{"author.badges", std::move(badges)},
|
||||||
|
{"author.color", m->usernameColor},
|
||||||
|
{"author.name", m->displayName},
|
||||||
|
{"author.no_color", !m->usernameColor.isValid()},
|
||||||
|
{"author.subbed", subscribed},
|
||||||
|
{"author.sub_length", subLength},
|
||||||
|
|
||||||
|
{"channel.name", m->channelName},
|
||||||
|
{"channel.watching", watching},
|
||||||
|
|
||||||
|
{"flags.highlighted", m->flags.has(MessageFlag::Highlighted)},
|
||||||
|
{"flags.points_redeemed", m->flags.has(MessageFlag::RedeemedHighlight)},
|
||||||
|
{"flags.sub_message", m->flags.has(MessageFlag::Subscription)},
|
||||||
|
{"flags.system_message", m->flags.has(MessageFlag::System)},
|
||||||
|
{"flags.whisper", m->flags.has(MessageFlag::Whisper)},
|
||||||
|
|
||||||
|
{"message.content", m->messageText},
|
||||||
|
{"message.length", m->messageText.length()},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterParser::FilterParser(const QString &text)
|
||||||
|
: text_(text)
|
||||||
|
, tokenizer_(Tokenizer(text))
|
||||||
|
, builtExpression_(this->parseExpression(true))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FilterParser::execute(const MessagePtr &message) const
|
||||||
|
{
|
||||||
|
auto context = buildContextMap(message);
|
||||||
|
return this->execute(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FilterParser::execute(const ContextMap &context) const
|
||||||
|
{
|
||||||
|
return this->builtExpression_->execute(context).toBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FilterParser::valid() const
|
||||||
|
{
|
||||||
|
return this->valid_;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpressionPtr FilterParser::parseExpression(bool top)
|
||||||
|
{
|
||||||
|
auto e = this->parseAnd();
|
||||||
|
while (this->tokenizer_.hasNext() &&
|
||||||
|
this->tokenizer_.nextTokenType() == TokenType::OR)
|
||||||
|
{
|
||||||
|
this->tokenizer_.next();
|
||||||
|
auto nextAnd = this->parseAnd();
|
||||||
|
e = std::make_unique<BinaryOperation>(TokenType::OR, std::move(e),
|
||||||
|
std::move(nextAnd));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->tokenizer_.hasNext() && top)
|
||||||
|
{
|
||||||
|
this->errorLog(QString("Unexpected token at end: %1")
|
||||||
|
.arg(this->tokenizer_.preview()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpressionPtr FilterParser::parseAnd()
|
||||||
|
{
|
||||||
|
auto e = this->parseUnary();
|
||||||
|
while (this->tokenizer_.hasNext() &&
|
||||||
|
this->tokenizer_.nextTokenType() == TokenType::AND)
|
||||||
|
{
|
||||||
|
this->tokenizer_.next();
|
||||||
|
auto nextUnary = this->parseUnary();
|
||||||
|
e = std::make_unique<BinaryOperation>(TokenType::AND, std::move(e),
|
||||||
|
std::move(nextUnary));
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpressionPtr FilterParser::parseUnary()
|
||||||
|
{
|
||||||
|
if (this->tokenizer_.hasNext() && this->tokenizer_.nextTokenIsUnaryOp())
|
||||||
|
{
|
||||||
|
this->tokenizer_.next();
|
||||||
|
auto type = this->tokenizer_.tokenType();
|
||||||
|
auto nextCondition = this->parseCondition();
|
||||||
|
return std::make_unique<UnaryOperation>(type, std::move(nextCondition));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return this->parseCondition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpressionPtr FilterParser::parseParentheses()
|
||||||
|
{
|
||||||
|
// Don't call .next() before calling this method
|
||||||
|
assert(this->tokenizer_.nextTokenType() == TokenType::LP);
|
||||||
|
|
||||||
|
this->tokenizer_.next();
|
||||||
|
auto e = this->parseExpression();
|
||||||
|
if (this->tokenizer_.hasNext() &&
|
||||||
|
this->tokenizer_.nextTokenType() == TokenType::RP)
|
||||||
|
{
|
||||||
|
this->tokenizer_.next();
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const auto message =
|
||||||
|
this->tokenizer_.hasNext()
|
||||||
|
? QString("Missing closing parentheses: got %1")
|
||||||
|
.arg(this->tokenizer_.preview())
|
||||||
|
: "Missing closing parentheses at end of statement";
|
||||||
|
this->errorLog(message);
|
||||||
|
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpressionPtr FilterParser::parseCondition()
|
||||||
|
{
|
||||||
|
ExpressionPtr value;
|
||||||
|
// parse expression wrapped in parentheses
|
||||||
|
if (this->tokenizer_.hasNext() &&
|
||||||
|
this->tokenizer_.nextTokenType() == TokenType::LP)
|
||||||
|
{
|
||||||
|
// get value inside parentheses
|
||||||
|
value = this->parseParentheses();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// get current value
|
||||||
|
value = this->parseValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// expecting an operator or nothing
|
||||||
|
while (this->tokenizer_.hasNext())
|
||||||
|
{
|
||||||
|
if (this->tokenizer_.nextTokenIsBinaryOp())
|
||||||
|
{
|
||||||
|
this->tokenizer_.next();
|
||||||
|
auto type = this->tokenizer_.tokenType();
|
||||||
|
auto nextValue = this->parseValue();
|
||||||
|
return std::make_unique<BinaryOperation>(type, std::move(value),
|
||||||
|
std::move(nextValue));
|
||||||
|
}
|
||||||
|
else if (this->tokenizer_.nextTokenIsMathOp())
|
||||||
|
{
|
||||||
|
this->tokenizer_.next();
|
||||||
|
auto type = this->tokenizer_.tokenType();
|
||||||
|
auto nextValue = this->parseValue();
|
||||||
|
value = std::make_unique<BinaryOperation>(type, std::move(value),
|
||||||
|
std::move(nextValue));
|
||||||
|
}
|
||||||
|
else if (this->tokenizer_.nextTokenType() == TokenType::RP)
|
||||||
|
{
|
||||||
|
// RP, so move on
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (!this->tokenizer_.nextTokenIsOp())
|
||||||
|
{
|
||||||
|
this->errorLog(QString("Expected an operator but got %1 %2")
|
||||||
|
.arg(this->tokenizer_.preview())
|
||||||
|
.arg(tokenTypeToInfoString(
|
||||||
|
this->tokenizer_.nextTokenType())));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpressionPtr FilterParser::parseValue()
|
||||||
|
{
|
||||||
|
// parse a literal or an expression wrapped in parenthsis
|
||||||
|
if (this->tokenizer_.hasNext())
|
||||||
|
{
|
||||||
|
auto type = this->tokenizer_.nextTokenType();
|
||||||
|
if (type == TokenType::INT)
|
||||||
|
{
|
||||||
|
return std::make_unique<ValueExpression>(
|
||||||
|
this->tokenizer_.next().toInt(), type);
|
||||||
|
}
|
||||||
|
else if (type == TokenType::STRING)
|
||||||
|
{
|
||||||
|
auto before = this->tokenizer_.next();
|
||||||
|
// remove quote marks
|
||||||
|
auto val = before.mid(1);
|
||||||
|
val.chop(1);
|
||||||
|
val = val.replace("\\\"", "\"");
|
||||||
|
return std::make_unique<ValueExpression>(val, type);
|
||||||
|
}
|
||||||
|
else if (type == TokenType::IDENTIFIER)
|
||||||
|
{
|
||||||
|
return std::make_unique<ValueExpression>(this->tokenizer_.next(),
|
||||||
|
type);
|
||||||
|
}
|
||||||
|
else if (type == TokenType::LP)
|
||||||
|
{
|
||||||
|
return this->parseParentheses();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->tokenizer_.next();
|
||||||
|
this->errorLog(QString("Expected value but got %1 %2")
|
||||||
|
.arg(this->tokenizer_.current())
|
||||||
|
.arg(tokenTypeToInfoString(type)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->errorLog("Unexpected end of statement");
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_unique<ValueExpression>(0, TokenType::INT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FilterParser::errorLog(const QString &text, bool expand)
|
||||||
|
{
|
||||||
|
this->valid_ = false;
|
||||||
|
if (expand || this->parseLog_.size() == 0)
|
||||||
|
{
|
||||||
|
this->parseLog_ << text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList &FilterParser::errors() const
|
||||||
|
{
|
||||||
|
return this->parseLog_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString FilterParser::debugString() const
|
||||||
|
{
|
||||||
|
return this->builtExpression_->debug();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString FilterParser::filterString() const
|
||||||
|
{
|
||||||
|
return this->builtExpression_->filterString();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace filterparser
|
39
src/controllers/filters/parser/FilterParser.hpp
Normal file
39
src/controllers/filters/parser/FilterParser.hpp
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "controllers/filters/parser/Tokenizer.hpp"
|
||||||
|
#include "controllers/filters/parser/Types.hpp"
|
||||||
|
|
||||||
|
namespace filterparser {
|
||||||
|
|
||||||
|
ContextMap buildContextMap(const MessagePtr &m);
|
||||||
|
|
||||||
|
class FilterParser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FilterParser(const QString &text);
|
||||||
|
bool execute(const MessagePtr &message) const;
|
||||||
|
bool execute(const ContextMap &context) const;
|
||||||
|
bool valid() const;
|
||||||
|
|
||||||
|
const QStringList &errors() const;
|
||||||
|
const QString debugString() const;
|
||||||
|
const QString filterString() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ExpressionPtr parseExpression(bool top = false);
|
||||||
|
ExpressionPtr parseAnd();
|
||||||
|
ExpressionPtr parseUnary();
|
||||||
|
ExpressionPtr parseParentheses();
|
||||||
|
ExpressionPtr parseCondition();
|
||||||
|
ExpressionPtr parseValue();
|
||||||
|
|
||||||
|
void errorLog(const QString &text, bool expand = false);
|
||||||
|
|
||||||
|
QStringList parseLog_;
|
||||||
|
bool valid_ = true;
|
||||||
|
|
||||||
|
QString text_;
|
||||||
|
Tokenizer tokenizer_;
|
||||||
|
ExpressionPtr builtExpression_;
|
||||||
|
};
|
||||||
|
} // namespace filterparser
|
174
src/controllers/filters/parser/Tokenizer.cpp
Normal file
174
src/controllers/filters/parser/Tokenizer.cpp
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
#include "controllers/filters/parser/Tokenizer.hpp"
|
||||||
|
|
||||||
|
namespace filterparser {
|
||||||
|
|
||||||
|
Tokenizer::Tokenizer(const QString &text)
|
||||||
|
{
|
||||||
|
QRegularExpressionMatchIterator i = tokenRegex.globalMatch(text);
|
||||||
|
while (i.hasNext())
|
||||||
|
{
|
||||||
|
auto text = i.next().captured();
|
||||||
|
this->tokens_ << text;
|
||||||
|
this->tokenTypes_ << this->tokenize(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Tokenizer::hasNext() const
|
||||||
|
{
|
||||||
|
return this->i_ < this->tokens_.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Tokenizer::next()
|
||||||
|
{
|
||||||
|
this->i_++;
|
||||||
|
return this->tokens_.at(this->i_ - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Tokenizer::current() const
|
||||||
|
{
|
||||||
|
return this->tokens_.at(this->i_ - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Tokenizer::preview() const
|
||||||
|
{
|
||||||
|
if (this->hasNext())
|
||||||
|
return this->tokens_.at(this->i_);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenType Tokenizer::nextTokenType() const
|
||||||
|
{
|
||||||
|
return this->tokenTypes_.at(this->i_);
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenType Tokenizer::tokenType() const
|
||||||
|
{
|
||||||
|
return this->tokenTypes_.at(this->i_ - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Tokenizer::nextTokenIsOp() const
|
||||||
|
{
|
||||||
|
return this->typeIsOp(this->nextTokenType());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Tokenizer::nextTokenIsBinaryOp() const
|
||||||
|
{
|
||||||
|
return this->typeIsBinaryOp(this->nextTokenType());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Tokenizer::nextTokenIsUnaryOp() const
|
||||||
|
{
|
||||||
|
return this->typeIsUnaryOp(this->nextTokenType());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Tokenizer::nextTokenIsMathOp() const
|
||||||
|
{
|
||||||
|
return this->typeIsMathOp(this->nextTokenType());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tokenizer::debug()
|
||||||
|
{
|
||||||
|
if (this->i_ > 0)
|
||||||
|
{
|
||||||
|
qDebug() << "= current" << this->tokens_.at(this->i_ - 1);
|
||||||
|
qDebug() << "= current type" << this->tokenTypes_.at(this->i_ - 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "= no current";
|
||||||
|
}
|
||||||
|
if (this->hasNext())
|
||||||
|
{
|
||||||
|
qDebug() << "= next" << this->tokens_.at(this->i_);
|
||||||
|
qDebug() << "= next type" << this->tokenTypes_.at(this->i_);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "= no next";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList Tokenizer::allTokens()
|
||||||
|
{
|
||||||
|
return this->tokens_;
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenType Tokenizer::tokenize(const QString &text)
|
||||||
|
{
|
||||||
|
if (text == "&&")
|
||||||
|
return TokenType::AND;
|
||||||
|
else if (text == "||")
|
||||||
|
return TokenType::OR;
|
||||||
|
else if (text == "(")
|
||||||
|
return TokenType::LP;
|
||||||
|
else if (text == ")")
|
||||||
|
return TokenType::RP;
|
||||||
|
else if (text == "+")
|
||||||
|
return TokenType::PLUS;
|
||||||
|
else if (text == "-")
|
||||||
|
return TokenType::MINUS;
|
||||||
|
else if (text == "*")
|
||||||
|
return TokenType::MULTIPLY;
|
||||||
|
else if (text == "/")
|
||||||
|
return TokenType::DIVIDE;
|
||||||
|
else if (text == "==")
|
||||||
|
return TokenType::EQ;
|
||||||
|
else if (text == "!=")
|
||||||
|
return TokenType::NEQ;
|
||||||
|
else if (text == "%")
|
||||||
|
return TokenType::MOD;
|
||||||
|
else if (text == "<")
|
||||||
|
return TokenType::LT;
|
||||||
|
else if (text == ">")
|
||||||
|
return TokenType::GT;
|
||||||
|
else if (text == "<=")
|
||||||
|
return TokenType::LTE;
|
||||||
|
else if (text == ">=")
|
||||||
|
return TokenType::GTE;
|
||||||
|
else if (text == "contains")
|
||||||
|
return TokenType::CONTAINS;
|
||||||
|
else if (text == "startswith")
|
||||||
|
return TokenType::STARTS_WITH;
|
||||||
|
else if (text == "endswith")
|
||||||
|
return TokenType::ENDS_WITH;
|
||||||
|
else if (text == "!")
|
||||||
|
return TokenType::NOT;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (text.front() == '"' && text.back() == '"')
|
||||||
|
return TokenType::STRING;
|
||||||
|
|
||||||
|
if (validIdentifiersMap.keys().contains(text))
|
||||||
|
return TokenType::IDENTIFIER;
|
||||||
|
|
||||||
|
bool flag;
|
||||||
|
if (text.toInt(&flag); flag)
|
||||||
|
return TokenType::INT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TokenType::NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Tokenizer::typeIsOp(TokenType token)
|
||||||
|
{
|
||||||
|
return typeIsBinaryOp(token) || typeIsUnaryOp(token) ||
|
||||||
|
typeIsMathOp(token) || token == TokenType::AND ||
|
||||||
|
token == TokenType::OR;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Tokenizer::typeIsBinaryOp(TokenType token)
|
||||||
|
{
|
||||||
|
return token > TokenType::BINARY_START && token < TokenType::BINARY_END;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Tokenizer::typeIsUnaryOp(TokenType token)
|
||||||
|
{
|
||||||
|
return token > TokenType::UNARY_START && token < TokenType::UNARY_END;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Tokenizer::typeIsMathOp(TokenType token)
|
||||||
|
{
|
||||||
|
return token > TokenType::MATH_START && token < TokenType::MATH_END;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace filterparser
|
65
src/controllers/filters/parser/Tokenizer.hpp
Normal file
65
src/controllers/filters/parser/Tokenizer.hpp
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "controllers/filters/parser/Types.hpp"
|
||||||
|
|
||||||
|
namespace filterparser {
|
||||||
|
|
||||||
|
static const QMap<QString, QString> validIdentifiersMap = {
|
||||||
|
{"author.badges", "author badges"},
|
||||||
|
{"author.color", "author color"},
|
||||||
|
{"author.name", "author name"},
|
||||||
|
{"author.no_color", "author has no color?"},
|
||||||
|
{"author.subbed", "author subscribed?"},
|
||||||
|
{"author.sub_length", "author sub length"},
|
||||||
|
{"channel.name", "channel name"},
|
||||||
|
{"channel.watching", "/watching channel?"},
|
||||||
|
{"flags.highlighted", "highlighted?"},
|
||||||
|
{"flags.points_redeemed", "redeemed points?"},
|
||||||
|
{"flags.sub_message", "sub/resub message?"},
|
||||||
|
{"flags.system_message", "system message?"},
|
||||||
|
{"flags.whisper", "whisper message?"},
|
||||||
|
{"message.content", "message text"},
|
||||||
|
{"message.length", "message length"}};
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
static const QRegularExpression tokenRegex(
|
||||||
|
QString("\\\"((\\\\\")|[^\\\"])*\\\"|") + // String literal
|
||||||
|
QString("[\\w\\.]+|") + // Identifier or reserved keyword
|
||||||
|
QString("(<=?|>=?|!=?|==|\\|\\||&&|\\+|-|\\*|\\/|%)+|") + // Operator
|
||||||
|
QString("[\\(\\)]") // Parentheses
|
||||||
|
);
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
class Tokenizer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Tokenizer(const QString &text);
|
||||||
|
|
||||||
|
bool hasNext() const;
|
||||||
|
QString next();
|
||||||
|
QString current() const;
|
||||||
|
QString preview() const;
|
||||||
|
TokenType nextTokenType() const;
|
||||||
|
TokenType tokenType() const;
|
||||||
|
|
||||||
|
bool nextTokenIsOp() const;
|
||||||
|
bool nextTokenIsBinaryOp() const;
|
||||||
|
bool nextTokenIsUnaryOp() const;
|
||||||
|
bool nextTokenIsMathOp() const;
|
||||||
|
|
||||||
|
void debug();
|
||||||
|
const QStringList allTokens();
|
||||||
|
|
||||||
|
static bool typeIsOp(TokenType token);
|
||||||
|
static bool typeIsBinaryOp(TokenType token);
|
||||||
|
static bool typeIsUnaryOp(TokenType token);
|
||||||
|
static bool typeIsMathOp(TokenType token);
|
||||||
|
|
||||||
|
private:
|
||||||
|
int i_ = 0;
|
||||||
|
QStringList tokens_;
|
||||||
|
QList<TokenType> tokenTypes_;
|
||||||
|
|
||||||
|
TokenType tokenize(const QString &text);
|
||||||
|
};
|
||||||
|
} // namespace filterparser
|
363
src/controllers/filters/parser/Types.cpp
Normal file
363
src/controllers/filters/parser/Types.cpp
Normal file
|
@ -0,0 +1,363 @@
|
||||||
|
#include "controllers/filters/parser/Types.hpp"
|
||||||
|
|
||||||
|
namespace filterparser {
|
||||||
|
|
||||||
|
bool convertVariantTypes(QVariant &a, QVariant &b, int type)
|
||||||
|
{
|
||||||
|
return a.convert(type) && b.convert(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool variantTypesMatch(QVariant &a, QVariant &b, QVariant::Type type)
|
||||||
|
{
|
||||||
|
return a.type() == type && b.type() == type;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString tokenTypeToInfoString(TokenType type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case CONTROL_START:
|
||||||
|
case CONTROL_END:
|
||||||
|
case BINARY_START:
|
||||||
|
case BINARY_END:
|
||||||
|
case UNARY_START:
|
||||||
|
case UNARY_END:
|
||||||
|
case MATH_START:
|
||||||
|
case MATH_END:
|
||||||
|
case OTHER_START:
|
||||||
|
case NONE:
|
||||||
|
return "<unknown>";
|
||||||
|
case AND:
|
||||||
|
return "<and>";
|
||||||
|
case OR:
|
||||||
|
return "<or>";
|
||||||
|
case LP:
|
||||||
|
return "<left parenthesis>";
|
||||||
|
case RP:
|
||||||
|
return "<right parenthesis>";
|
||||||
|
case PLUS:
|
||||||
|
return "<plus>";
|
||||||
|
case MINUS:
|
||||||
|
return "<minus>";
|
||||||
|
case MULTIPLY:
|
||||||
|
return "<multiply>";
|
||||||
|
case DIVIDE:
|
||||||
|
return "<divide>";
|
||||||
|
case MOD:
|
||||||
|
return "<modulus>";
|
||||||
|
case EQ:
|
||||||
|
return "<equals>";
|
||||||
|
case NEQ:
|
||||||
|
return "<not equals>";
|
||||||
|
case LT:
|
||||||
|
return "<less than>";
|
||||||
|
case GT:
|
||||||
|
return "<greater than>";
|
||||||
|
case LTE:
|
||||||
|
return "<less than equal>";
|
||||||
|
case GTE:
|
||||||
|
return "<greater than equal>";
|
||||||
|
case CONTAINS:
|
||||||
|
return "<contains>";
|
||||||
|
case STARTS_WITH:
|
||||||
|
return "<starts with>";
|
||||||
|
case ENDS_WITH:
|
||||||
|
return "<ends with>";
|
||||||
|
case NOT:
|
||||||
|
return "<not>";
|
||||||
|
case STRING:
|
||||||
|
return "<string>";
|
||||||
|
case INT:
|
||||||
|
return "<int>";
|
||||||
|
case IDENTIFIER:
|
||||||
|
return "<identifier>";
|
||||||
|
default:
|
||||||
|
return "<unknown>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueExpression
|
||||||
|
|
||||||
|
ValueExpression::ValueExpression(QVariant value, TokenType type)
|
||||||
|
: value_(value)
|
||||||
|
, type_(type){};
|
||||||
|
|
||||||
|
QVariant ValueExpression::execute(const ContextMap &context) const
|
||||||
|
{
|
||||||
|
if (this->type_ == TokenType::IDENTIFIER)
|
||||||
|
{
|
||||||
|
return context.value(this->value_.toString());
|
||||||
|
}
|
||||||
|
return this->value_;
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenType ValueExpression::type()
|
||||||
|
{
|
||||||
|
return this->type_;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ValueExpression::debug() const
|
||||||
|
{
|
||||||
|
return this->value_.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ValueExpression::filterString() const
|
||||||
|
{
|
||||||
|
switch (this->type_)
|
||||||
|
{
|
||||||
|
case INT:
|
||||||
|
return QString::number(this->value_.toInt());
|
||||||
|
case STRING:
|
||||||
|
return QString("\"%1\"").arg(
|
||||||
|
this->value_.toString().replace("\"", "\\\""));
|
||||||
|
case IDENTIFIER:
|
||||||
|
return this->value_.toString();
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinaryOperation
|
||||||
|
|
||||||
|
BinaryOperation::BinaryOperation(TokenType op, ExpressionPtr left,
|
||||||
|
ExpressionPtr right)
|
||||||
|
: op_(op)
|
||||||
|
, left_(std::move(left))
|
||||||
|
, right_(std::move(right))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant BinaryOperation::execute(const ContextMap &context) const
|
||||||
|
{
|
||||||
|
auto left = this->left_->execute(context);
|
||||||
|
auto right = this->right_->execute(context);
|
||||||
|
switch (this->op_)
|
||||||
|
{
|
||||||
|
case PLUS:
|
||||||
|
if (left.type() == QVariant::Type::String &&
|
||||||
|
right.canConvert(QMetaType::QString))
|
||||||
|
{
|
||||||
|
return left.toString().append(right.toString());
|
||||||
|
}
|
||||||
|
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||||
|
{
|
||||||
|
return left.toInt() + right.toInt();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
case MINUS:
|
||||||
|
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||||
|
return left.toInt() - right.toInt();
|
||||||
|
return 0;
|
||||||
|
case MULTIPLY:
|
||||||
|
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||||
|
return left.toInt() * right.toInt();
|
||||||
|
return 0;
|
||||||
|
case DIVIDE:
|
||||||
|
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||||
|
return left.toInt() / right.toInt();
|
||||||
|
return 0;
|
||||||
|
case MOD:
|
||||||
|
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||||
|
return left.toInt() % right.toInt();
|
||||||
|
return 0;
|
||||||
|
case OR:
|
||||||
|
if (convertVariantTypes(left, right, QMetaType::Bool))
|
||||||
|
return left.toBool() || right.toBool();
|
||||||
|
return false;
|
||||||
|
case AND:
|
||||||
|
if (convertVariantTypes(left, right, QMetaType::Bool))
|
||||||
|
return left.toBool() && right.toBool();
|
||||||
|
return false;
|
||||||
|
case EQ:
|
||||||
|
if (variantTypesMatch(left, right, QVariant::Type::String))
|
||||||
|
{
|
||||||
|
return left.toString().compare(right.toString(),
|
||||||
|
Qt::CaseInsensitive) == 0;
|
||||||
|
}
|
||||||
|
return left == right;
|
||||||
|
case NEQ:
|
||||||
|
if (variantTypesMatch(left, right, QVariant::Type::String))
|
||||||
|
{
|
||||||
|
return left.toString().compare(right.toString(),
|
||||||
|
Qt::CaseInsensitive) != 0;
|
||||||
|
}
|
||||||
|
return left != right;
|
||||||
|
case LT:
|
||||||
|
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||||
|
return left.toInt() < right.toInt();
|
||||||
|
return false;
|
||||||
|
case GT:
|
||||||
|
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||||
|
return left.toInt() > right.toInt();
|
||||||
|
return false;
|
||||||
|
case LTE:
|
||||||
|
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||||
|
return left.toInt() <= right.toInt();
|
||||||
|
return false;
|
||||||
|
case GTE:
|
||||||
|
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||||
|
return left.toInt() >= right.toInt();
|
||||||
|
return false;
|
||||||
|
case CONTAINS:
|
||||||
|
if (left.type() == QVariant::Type::StringList &&
|
||||||
|
right.canConvert(QMetaType::QString))
|
||||||
|
{
|
||||||
|
return left.toStringList().contains(right.toString(),
|
||||||
|
Qt::CaseInsensitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left.type() == QVariant::Type::Map &&
|
||||||
|
right.canConvert(QMetaType::QString))
|
||||||
|
{
|
||||||
|
return left.toMap().contains(right.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left.canConvert(QMetaType::QString) &&
|
||||||
|
right.canConvert(QMetaType::QString))
|
||||||
|
{
|
||||||
|
return left.toString().contains(right.toString(),
|
||||||
|
Qt::CaseInsensitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
case STARTS_WITH:
|
||||||
|
if (left.type() == QVariant::Type::StringList &&
|
||||||
|
right.canConvert(QMetaType::QString))
|
||||||
|
{
|
||||||
|
auto list = left.toStringList();
|
||||||
|
return !list.isEmpty() &&
|
||||||
|
list.first().compare(right.toString(),
|
||||||
|
Qt::CaseInsensitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left.canConvert(QMetaType::QString) &&
|
||||||
|
right.canConvert(QMetaType::QString))
|
||||||
|
{
|
||||||
|
return left.toString().startsWith(right.toString(),
|
||||||
|
Qt::CaseInsensitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case ENDS_WITH:
|
||||||
|
if (left.type() == QVariant::Type::StringList &&
|
||||||
|
right.canConvert(QMetaType::QString))
|
||||||
|
{
|
||||||
|
auto list = left.toStringList();
|
||||||
|
return !list.isEmpty() &&
|
||||||
|
list.last().compare(right.toString(),
|
||||||
|
Qt::CaseInsensitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left.canConvert(QMetaType::QString) &&
|
||||||
|
right.canConvert(QMetaType::QString))
|
||||||
|
{
|
||||||
|
return left.toString().endsWith(right.toString(),
|
||||||
|
Qt::CaseInsensitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString BinaryOperation::debug() const
|
||||||
|
{
|
||||||
|
return QString("(%1 %2 %3)")
|
||||||
|
.arg(this->left_->debug(), tokenTypeToInfoString(this->op_),
|
||||||
|
this->right_->debug());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString BinaryOperation::filterString() const
|
||||||
|
{
|
||||||
|
const auto opText = [&]() -> QString {
|
||||||
|
switch (this->op_)
|
||||||
|
{
|
||||||
|
case AND:
|
||||||
|
return "&&";
|
||||||
|
case OR:
|
||||||
|
return "||";
|
||||||
|
case PLUS:
|
||||||
|
return "+";
|
||||||
|
case MINUS:
|
||||||
|
return "-";
|
||||||
|
case MULTIPLY:
|
||||||
|
return "*";
|
||||||
|
case DIVIDE:
|
||||||
|
return "/";
|
||||||
|
case MOD:
|
||||||
|
return "%";
|
||||||
|
case EQ:
|
||||||
|
return "==";
|
||||||
|
case NEQ:
|
||||||
|
return "!=";
|
||||||
|
case LT:
|
||||||
|
return "<";
|
||||||
|
case GT:
|
||||||
|
return ">";
|
||||||
|
case LTE:
|
||||||
|
return "<=";
|
||||||
|
case GTE:
|
||||||
|
return ">=";
|
||||||
|
case CONTAINS:
|
||||||
|
return "contains";
|
||||||
|
case STARTS_WITH:
|
||||||
|
return "startswith";
|
||||||
|
case ENDS_WITH:
|
||||||
|
return "endswith";
|
||||||
|
default:
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
return QString("(%1) %2 (%3)")
|
||||||
|
.arg(this->left_->filterString())
|
||||||
|
.arg(opText)
|
||||||
|
.arg(this->right_->filterString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnaryOperation
|
||||||
|
|
||||||
|
UnaryOperation::UnaryOperation(TokenType op, ExpressionPtr right)
|
||||||
|
: op_(op)
|
||||||
|
, right_(std::move(right))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant UnaryOperation::execute(const ContextMap &context) const
|
||||||
|
{
|
||||||
|
auto right = this->right_->execute(context);
|
||||||
|
switch (this->op_)
|
||||||
|
{
|
||||||
|
case NOT:
|
||||||
|
if (right.canConvert<bool>())
|
||||||
|
return !right.toBool();
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString UnaryOperation::debug() const
|
||||||
|
{
|
||||||
|
return QString("(%1 %2)").arg(tokenTypeToInfoString(this->op_),
|
||||||
|
this->right_->debug());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString UnaryOperation::filterString() const
|
||||||
|
{
|
||||||
|
const auto opText = [&]() -> QString {
|
||||||
|
switch (this->op_)
|
||||||
|
{
|
||||||
|
case NOT:
|
||||||
|
return "!";
|
||||||
|
default:
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
return QString("%1(%2)").arg(opText).arg(this->right_->filterString());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace filterparser
|
125
src/controllers/filters/parser/Types.hpp
Normal file
125
src/controllers/filters/parser/Types.hpp
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "messages/Message.hpp"
|
||||||
|
|
||||||
|
namespace filterparser {
|
||||||
|
|
||||||
|
using MessagePtr = std::shared_ptr<const chatterino::Message>;
|
||||||
|
using ContextMap = QMap<QString, QVariant>;
|
||||||
|
|
||||||
|
enum TokenType {
|
||||||
|
// control
|
||||||
|
CONTROL_START = 0,
|
||||||
|
AND = 1,
|
||||||
|
OR = 2,
|
||||||
|
LP = 3,
|
||||||
|
RP = 4,
|
||||||
|
CONTROL_END = 9,
|
||||||
|
|
||||||
|
// binary operator
|
||||||
|
BINARY_START = 10,
|
||||||
|
EQ = 11,
|
||||||
|
NEQ = 12,
|
||||||
|
LT = 13,
|
||||||
|
GT = 14,
|
||||||
|
LTE = 15,
|
||||||
|
GTE = 16,
|
||||||
|
CONTAINS = 17,
|
||||||
|
STARTS_WITH = 18,
|
||||||
|
ENDS_WITH = 19,
|
||||||
|
BINARY_END = 49,
|
||||||
|
|
||||||
|
// unary operator
|
||||||
|
UNARY_START = 50,
|
||||||
|
NOT = 51,
|
||||||
|
UNARY_END = 99,
|
||||||
|
|
||||||
|
// math operators
|
||||||
|
MATH_START = 100,
|
||||||
|
PLUS = 101,
|
||||||
|
MINUS = 102,
|
||||||
|
MULTIPLY = 103,
|
||||||
|
DIVIDE = 104,
|
||||||
|
MOD = 105,
|
||||||
|
MATH_END = 149,
|
||||||
|
|
||||||
|
// other types
|
||||||
|
OTHER_START = 150,
|
||||||
|
STRING = 151,
|
||||||
|
INT = 152,
|
||||||
|
IDENTIFIER = 153,
|
||||||
|
|
||||||
|
NONE = 200
|
||||||
|
};
|
||||||
|
|
||||||
|
bool convertVariantTypes(QVariant &a, QVariant &b, int type);
|
||||||
|
QString tokenTypeToInfoString(TokenType type);
|
||||||
|
|
||||||
|
class Expression
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~Expression() = default;
|
||||||
|
|
||||||
|
virtual QVariant execute(const ContextMap &) const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual QString debug() const
|
||||||
|
{
|
||||||
|
return "(false)";
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual QString filterString() const
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using ExpressionPtr = std::unique_ptr<Expression>;
|
||||||
|
|
||||||
|
class ValueExpression : public Expression
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ValueExpression(QVariant value, TokenType type);
|
||||||
|
TokenType type();
|
||||||
|
|
||||||
|
QVariant execute(const ContextMap &context) const override;
|
||||||
|
QString debug() const override;
|
||||||
|
QString filterString() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QVariant value_;
|
||||||
|
TokenType type_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BinaryOperation : public Expression
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
BinaryOperation(TokenType op, ExpressionPtr left, ExpressionPtr right);
|
||||||
|
|
||||||
|
QVariant execute(const ContextMap &context) const override;
|
||||||
|
QString debug() const override;
|
||||||
|
QString filterString() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
TokenType op_;
|
||||||
|
ExpressionPtr left_;
|
||||||
|
ExpressionPtr right_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class UnaryOperation : public Expression
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
UnaryOperation(TokenType op, ExpressionPtr right);
|
||||||
|
|
||||||
|
QVariant execute(const ContextMap &context) const override;
|
||||||
|
QString debug() const override;
|
||||||
|
QString filterString() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
TokenType op_;
|
||||||
|
ExpressionPtr right_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace filterparser
|
|
@ -1,6 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "common/FlagsEnum.hpp"
|
#include "common/FlagsEnum.hpp"
|
||||||
|
#include "providers/twitch/TwitchBadge.hpp"
|
||||||
#include "widgets/helper/ScrollbarHighlight.hpp"
|
#include "widgets/helper/ScrollbarHighlight.hpp"
|
||||||
|
|
||||||
#include <QTime>
|
#include <QTime>
|
||||||
|
@ -58,6 +59,10 @@ struct Message : boost::noncopyable {
|
||||||
QString displayName;
|
QString displayName;
|
||||||
QString localizedName;
|
QString localizedName;
|
||||||
QString timeoutUser;
|
QString timeoutUser;
|
||||||
|
QString channelName;
|
||||||
|
QColor usernameColor;
|
||||||
|
std::vector<Badge> badges;
|
||||||
|
std::map<QString, QString> badgeInfos;
|
||||||
std::shared_ptr<QColor> highlightColor;
|
std::shared_ptr<QColor> highlightColor;
|
||||||
uint32_t count = 1;
|
uint32_t count = 1;
|
||||||
std::vector<std::unique_ptr<MessageElement>> elements;
|
std::vector<std::unique_ptr<MessageElement>> elements;
|
||||||
|
|
|
@ -115,7 +115,6 @@ bool MessageLayout::layout(int width, float scale, MessageElementFlags flags)
|
||||||
void MessageLayout::actuallyLayout(int width, MessageElementFlags flags)
|
void MessageLayout::actuallyLayout(int width, MessageElementFlags flags)
|
||||||
{
|
{
|
||||||
this->layoutCount_++;
|
this->layoutCount_++;
|
||||||
|
|
||||||
auto messageFlags = this->message_->flags;
|
auto messageFlags = this->message_->flags;
|
||||||
|
|
||||||
if (this->flags.has(MessageLayoutFlag::Expanded) ||
|
if (this->flags.has(MessageLayoutFlag::Expanded) ||
|
||||||
|
@ -272,6 +271,9 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex,
|
||||||
void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/,
|
void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/,
|
||||||
Selection & /*selection*/)
|
Selection & /*selection*/)
|
||||||
{
|
{
|
||||||
|
if (buffer->isNull())
|
||||||
|
return;
|
||||||
|
|
||||||
auto app = getApp();
|
auto app = getApp();
|
||||||
auto settings = getSettings();
|
auto settings = getSettings();
|
||||||
|
|
||||||
|
|
|
@ -188,6 +188,8 @@ MessagePtr TwitchMessageBuilder::build()
|
||||||
this->senderIsBroadcaster = true;
|
this->senderIsBroadcaster = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->message().channelName = this->channel->getName();
|
||||||
|
|
||||||
this->parseMessageID();
|
this->parseMessageID();
|
||||||
|
|
||||||
this->parseRoomID();
|
this->parseRoomID();
|
||||||
|
@ -531,6 +533,7 @@ void TwitchMessageBuilder::parseUsernameColor()
|
||||||
if (const auto color = iterator.value().toString(); !color.isEmpty())
|
if (const auto color = iterator.value().toString(); !color.isEmpty())
|
||||||
{
|
{
|
||||||
this->usernameColor_ = QColor(color);
|
this->usernameColor_ = QColor(color);
|
||||||
|
this->message().usernameColor = this->usernameColor_;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -538,6 +541,7 @@ void TwitchMessageBuilder::parseUsernameColor()
|
||||||
if (getSettings()->colorizeNicknames && this->tags.contains("user-id"))
|
if (getSettings()->colorizeNicknames && this->tags.contains("user-id"))
|
||||||
{
|
{
|
||||||
this->usernameColor_ = getRandomColor(this->tags.value("user-id"));
|
this->usernameColor_ = getRandomColor(this->tags.value("user-id"));
|
||||||
|
this->message().usernameColor = this->usernameColor_;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1072,6 +1076,9 @@ void TwitchMessageBuilder::appendTwitchBadges()
|
||||||
this->emplace<BadgeElement>(badgeEmote.get(), badge.flag_)
|
this->emplace<BadgeElement>(badgeEmote.get(), badge.flag_)
|
||||||
->setTooltip(tooltip);
|
->setTooltip(tooltip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->message().badges = badges;
|
||||||
|
this->message().badgeInfos = badgeInfos;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TwitchMessageBuilder::appendChatterinoBadges()
|
void TwitchMessageBuilder::appendChatterinoBadges()
|
||||||
|
|
|
@ -21,6 +21,7 @@ ConcurrentSettings::ConcurrentSettings()
|
||||||
, blacklistedUsers(*new SignalVector<HighlightBlacklistUser>())
|
, blacklistedUsers(*new SignalVector<HighlightBlacklistUser>())
|
||||||
, ignoredMessages(*new SignalVector<IgnorePhrase>())
|
, ignoredMessages(*new SignalVector<IgnorePhrase>())
|
||||||
, mutedChannels(*new SignalVector<QString>())
|
, mutedChannels(*new SignalVector<QString>())
|
||||||
|
, filterRecords(*new SignalVector<FilterRecordPtr>())
|
||||||
, moderationActions(*new SignalVector<ModerationAction>)
|
, moderationActions(*new SignalVector<ModerationAction>)
|
||||||
{
|
{
|
||||||
persist(this->highlightedMessages, "/highlighting/highlights");
|
persist(this->highlightedMessages, "/highlighting/highlights");
|
||||||
|
@ -28,6 +29,7 @@ ConcurrentSettings::ConcurrentSettings()
|
||||||
persist(this->highlightedUsers, "/highlighting/users");
|
persist(this->highlightedUsers, "/highlighting/users");
|
||||||
persist(this->ignoredMessages, "/ignore/phrases");
|
persist(this->ignoredMessages, "/ignore/phrases");
|
||||||
persist(this->mutedChannels, "/pings/muted");
|
persist(this->mutedChannels, "/pings/muted");
|
||||||
|
persist(this->filterRecords, "/filtering/filters");
|
||||||
// tagged users?
|
// tagged users?
|
||||||
persist(this->moderationActions, "/moderation/actions");
|
persist(this->moderationActions, "/moderation/actions");
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#include "BaseSettings.hpp"
|
#include "BaseSettings.hpp"
|
||||||
#include "common/Channel.hpp"
|
#include "common/Channel.hpp"
|
||||||
#include "common/SignalVector.hpp"
|
#include "common/SignalVector.hpp"
|
||||||
|
#include "controllers/filters/FilterRecord.hpp"
|
||||||
#include "controllers/highlights/HighlightPhrase.hpp"
|
#include "controllers/highlights/HighlightPhrase.hpp"
|
||||||
#include "controllers/moderationactions/ModerationAction.hpp"
|
#include "controllers/moderationactions/ModerationAction.hpp"
|
||||||
#include "singletons/Toasts.hpp"
|
#include "singletons/Toasts.hpp"
|
||||||
|
@ -20,6 +21,7 @@ class HighlightPhrase;
|
||||||
class HighlightBlacklistUser;
|
class HighlightBlacklistUser;
|
||||||
class IgnorePhrase;
|
class IgnorePhrase;
|
||||||
class TaggedUser;
|
class TaggedUser;
|
||||||
|
class FilterRecord;
|
||||||
|
|
||||||
/// Settings which are availlable for reading on all threads.
|
/// Settings which are availlable for reading on all threads.
|
||||||
class ConcurrentSettings
|
class ConcurrentSettings
|
||||||
|
@ -32,6 +34,7 @@ public:
|
||||||
SignalVector<HighlightBlacklistUser> &blacklistedUsers;
|
SignalVector<HighlightBlacklistUser> &blacklistedUsers;
|
||||||
SignalVector<IgnorePhrase> &ignoredMessages;
|
SignalVector<IgnorePhrase> &ignoredMessages;
|
||||||
SignalVector<QString> &mutedChannels;
|
SignalVector<QString> &mutedChannels;
|
||||||
|
SignalVector<FilterRecordPtr> &filterRecords;
|
||||||
//SignalVector<TaggedUser> &taggedUsers;
|
//SignalVector<TaggedUser> &taggedUsers;
|
||||||
SignalVector<ModerationAction> &moderationActions;
|
SignalVector<ModerationAction> &moderationActions;
|
||||||
|
|
||||||
|
@ -255,6 +258,10 @@ public:
|
||||||
|
|
||||||
BoolSetting longAlerts = {"/highlighting/alerts", false};
|
BoolSetting longAlerts = {"/highlighting/alerts", false};
|
||||||
|
|
||||||
|
/// Filtering
|
||||||
|
BoolSetting excludeUserMessagesFromFilter = {
|
||||||
|
"/filtering/excludeUserMessages", false};
|
||||||
|
|
||||||
/// Logging
|
/// Logging
|
||||||
BoolSetting enableLogging = {"/logging/enabled", false};
|
BoolSetting enableLogging = {"/logging/enabled", false};
|
||||||
|
|
||||||
|
|
|
@ -407,7 +407,7 @@ void WindowManager::save()
|
||||||
// splits
|
// splits
|
||||||
QJsonObject splits;
|
QJsonObject splits;
|
||||||
|
|
||||||
this->encodeNodeRecusively(tab->getBaseNode(), splits);
|
this->encodeNodeRecursively(tab->getBaseNode(), splits);
|
||||||
|
|
||||||
tab_obj.insert("splits2", splits);
|
tab_obj.insert("splits2", splits);
|
||||||
tabs_arr.append(tab_obj);
|
tabs_arr.append(tab_obj);
|
||||||
|
@ -454,16 +454,22 @@ void WindowManager::queueSave()
|
||||||
this->saveTimer->start(10s);
|
this->saveTimer->start(10s);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WindowManager::encodeNodeRecusively(SplitNode *node, QJsonObject &obj)
|
void WindowManager::encodeNodeRecursively(SplitNode *node, QJsonObject &obj)
|
||||||
{
|
{
|
||||||
switch (node->getType())
|
switch (node->getType())
|
||||||
{
|
{
|
||||||
case SplitNode::_Split: {
|
case SplitNode::_Split: {
|
||||||
obj.insert("type", "split");
|
obj.insert("type", "split");
|
||||||
obj.insert("moderationMode", node->getSplit()->getModerationMode());
|
obj.insert("moderationMode", node->getSplit()->getModerationMode());
|
||||||
|
|
||||||
QJsonObject split;
|
QJsonObject split;
|
||||||
encodeChannel(node->getSplit()->getIndirectChannel(), split);
|
encodeChannel(node->getSplit()->getIndirectChannel(), split);
|
||||||
obj.insert("data", split);
|
obj.insert("data", split);
|
||||||
|
|
||||||
|
QJsonArray filters;
|
||||||
|
encodeFilters(node->getSplit(), filters);
|
||||||
|
obj.insert("filters", filters);
|
||||||
|
|
||||||
obj.insert("flexh", node->getHorizontalFlex());
|
obj.insert("flexh", node->getHorizontalFlex());
|
||||||
obj.insert("flexv", node->getVerticalFlex());
|
obj.insert("flexv", node->getVerticalFlex());
|
||||||
}
|
}
|
||||||
|
@ -478,7 +484,7 @@ void WindowManager::encodeNodeRecusively(SplitNode *node, QJsonObject &obj)
|
||||||
for (const std::unique_ptr<SplitNode> &n : node->getChildren())
|
for (const std::unique_ptr<SplitNode> &n : node->getChildren())
|
||||||
{
|
{
|
||||||
QJsonObject subObj;
|
QJsonObject subObj;
|
||||||
this->encodeNodeRecusively(n.get(), subObj);
|
this->encodeNodeRecursively(n.get(), subObj);
|
||||||
items_arr.append(subObj);
|
items_arr.append(subObj);
|
||||||
}
|
}
|
||||||
obj.insert("items", items_arr);
|
obj.insert("items", items_arr);
|
||||||
|
@ -526,6 +532,17 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WindowManager::encodeFilters(Split *split, QJsonArray &arr)
|
||||||
|
{
|
||||||
|
assertInGuiThread();
|
||||||
|
|
||||||
|
auto filters = split->getFilters();
|
||||||
|
for (const auto &f : filters)
|
||||||
|
{
|
||||||
|
arr.append(f.toString(QUuid::WithoutBraces));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IndirectChannel WindowManager::decodeChannel(const SplitDescriptor &descriptor)
|
IndirectChannel WindowManager::decodeChannel(const SplitDescriptor &descriptor)
|
||||||
{
|
{
|
||||||
assertInGuiThread();
|
assertInGuiThread();
|
||||||
|
|
|
@ -26,6 +26,7 @@ public:
|
||||||
WindowManager();
|
WindowManager();
|
||||||
|
|
||||||
static void encodeChannel(IndirectChannel channel, QJsonObject &obj);
|
static void encodeChannel(IndirectChannel channel, QJsonObject &obj);
|
||||||
|
static void encodeFilters(Split *split, QJsonArray &arr);
|
||||||
static IndirectChannel decodeChannel(const SplitDescriptor &descriptor);
|
static IndirectChannel decodeChannel(const SplitDescriptor &descriptor);
|
||||||
|
|
||||||
void showSettingsDialog(
|
void showSettingsDialog(
|
||||||
|
@ -89,7 +90,7 @@ public:
|
||||||
pajlada::Signals::NoArgSignal miscUpdate;
|
pajlada::Signals::NoArgSignal miscUpdate;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void encodeNodeRecusively(SplitContainer::Node *node, QJsonObject &obj);
|
void encodeNodeRecursively(SplitContainer::Node *node, QJsonObject &obj);
|
||||||
|
|
||||||
// Load window layout from the window-layout.json file
|
// Load window layout from the window-layout.json file
|
||||||
WindowLayout loadWindowLayoutFromFile() const;
|
WindowLayout loadWindowLayoutFromFile() const;
|
||||||
|
|
227
src/widgets/dialogs/ChannelFilterEditorDialog.cpp
Normal file
227
src/widgets/dialogs/ChannelFilterEditorDialog.cpp
Normal file
|
@ -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
|
61
src/widgets/dialogs/ChannelFilterEditorDialog.hpp
Normal file
61
src/widgets/dialogs/ChannelFilterEditorDialog.hpp
Normal file
|
@ -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
|
80
src/widgets/dialogs/SelectChannelFiltersDialog.cpp
Normal file
80
src/widgets/dialogs/SelectChannelFiltersDialog.cpp
Normal file
|
@ -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
|
19
src/widgets/dialogs/SelectChannelFiltersDialog.hpp
Normal file
19
src/widgets/dialogs/SelectChannelFiltersDialog.hpp
Normal file
|
@ -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/AccountsPage.hpp"
|
||||||
#include "widgets/settingspages/CommandPage.hpp"
|
#include "widgets/settingspages/CommandPage.hpp"
|
||||||
#include "widgets/settingspages/ExternalToolsPage.hpp"
|
#include "widgets/settingspages/ExternalToolsPage.hpp"
|
||||||
|
#include "widgets/settingspages/FiltersPage.hpp"
|
||||||
#include "widgets/settingspages/GeneralPage.hpp"
|
#include "widgets/settingspages/GeneralPage.hpp"
|
||||||
#include "widgets/settingspages/HighlightingPage.hpp"
|
#include "widgets/settingspages/HighlightingPage.hpp"
|
||||||
#include "widgets/settingspages/IgnoresPage.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 CommandPage;}, "Commands", ":/settings/commands.svg");
|
||||||
this->addTab([]{return new HighlightingPage;}, "Highlights", ":/settings/notifications.svg");
|
this->addTab([]{return new HighlightingPage;}, "Highlights", ":/settings/notifications.svg");
|
||||||
this->addTab([]{return new IgnoresPage;}, "Ignores", ":/settings/ignore.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->ui_.tabContainer->addSpacing(16);
|
||||||
this->addTab([]{return new KeyboardSettingsPage;}, "Keybindings", ":/settings/keybinds.svg");
|
this->addTab([]{return new KeyboardSettingsPage;}, "Keybindings", ":/settings/keybinds.svg");
|
||||||
this->addTab([]{return new ModerationPage;}, "Moderation", ":/settings/moderation.svg", SettingsTabId::Moderation);
|
this->addTab([]{return new ModerationPage;}, "Moderation", ":/settings/moderation.svg", SettingsTabId::Moderation);
|
||||||
|
|
|
@ -112,6 +112,7 @@ namespace {
|
||||||
ChannelView::ChannelView(BaseWidget *parent)
|
ChannelView::ChannelView(BaseWidget *parent)
|
||||||
: BaseWidget(parent)
|
: BaseWidget(parent)
|
||||||
, sourceChannel_(nullptr)
|
, sourceChannel_(nullptr)
|
||||||
|
, underlyingChannel_(nullptr)
|
||||||
, scrollBar_(new Scrollbar(this))
|
, scrollBar_(new Scrollbar(this))
|
||||||
{
|
{
|
||||||
this->setMouseTracking(true);
|
this->setMouseTracking(true);
|
||||||
|
@ -557,7 +558,12 @@ ChannelPtr ChannelView::channel()
|
||||||
return this->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
|
/// Clear connections from the last channel
|
||||||
this->channelConnections_.clear();
|
this->channelConnections_.clear();
|
||||||
|
@ -565,31 +571,88 @@ void ChannelView::setChannel(ChannelPtr channel)
|
||||||
this->clearMessages();
|
this->clearMessages();
|
||||||
this->scrollBar_->clearHighlights();
|
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
|
// on new message
|
||||||
this->channelConnections_.push_back(channel->messageAppended.connect(
|
this->channelConnections_.push_back(this->channel_->messageAppended.connect(
|
||||||
[this](MessagePtr &message,
|
[this](MessagePtr &message,
|
||||||
boost::optional<MessageFlags> overridingFlags) {
|
boost::optional<MessageFlags> overridingFlags) {
|
||||||
this->messageAppended(message, overridingFlags);
|
this->messageAppended(message, overridingFlags);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this->channelConnections_.push_back(channel->messagesAddedAtStart.connect(
|
this->channelConnections_.push_back(
|
||||||
[this](std::vector<MessagePtr> &messages) {
|
this->channel_->messagesAddedAtStart.connect(
|
||||||
this->messageAddedAtStart(messages);
|
[this](std::vector<MessagePtr> &messages) {
|
||||||
}));
|
this->messageAddedAtStart(messages);
|
||||||
|
}));
|
||||||
|
|
||||||
// on message removed
|
// on message removed
|
||||||
this->channelConnections_.push_back(
|
this->channelConnections_.push_back(
|
||||||
channel->messageRemovedFromStart.connect([this](MessagePtr &message) {
|
this->channel_->messageRemovedFromStart.connect(
|
||||||
this->messageRemoveFromStart(message);
|
[this](MessagePtr &message) {
|
||||||
}));
|
this->messageRemoveFromStart(message);
|
||||||
|
}));
|
||||||
|
|
||||||
// on message replaced
|
// 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](size_t index, MessagePtr replacement) {
|
||||||
this->messageReplaced(index, replacement);
|
this->messageReplaced(index, replacement);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
auto snapshot = channel->getMessageSnapshot();
|
auto snapshot = underlyingChannel->getMessageSnapshot();
|
||||||
|
|
||||||
for (size_t i = 0; i < snapshot.size(); i++)
|
for (size_t i = 0; i < snapshot.size(); i++)
|
||||||
{
|
{
|
||||||
|
@ -604,22 +667,26 @@ void ChannelView::setChannel(ChannelPtr channel)
|
||||||
this->lastMessageHasAlternateBackground_ =
|
this->lastMessageHasAlternateBackground_ =
|
||||||
!this->lastMessageHasAlternateBackground_;
|
!this->lastMessageHasAlternateBackground_;
|
||||||
|
|
||||||
if (channel->shouldIgnoreHighlights())
|
if (underlyingChannel->shouldIgnoreHighlights())
|
||||||
{
|
{
|
||||||
messageLayout->flags.set(MessageLayoutFlag::IgnoreHighlights);
|
messageLayout->flags.set(MessageLayoutFlag::IgnoreHighlights);
|
||||||
}
|
}
|
||||||
|
|
||||||
this->messages_.pushBack(MessageLayoutPtr(messageLayout), deleted);
|
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->queueLayout();
|
||||||
this->queueUpdate();
|
this->queueUpdate();
|
||||||
|
|
||||||
// Notifications
|
// 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->connections_.push_back(tc->liveStatusChanged.connect([this]() {
|
||||||
this->liveStatusChanged.invoke(); //
|
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
|
ChannelPtr ChannelView::sourceChannel() const
|
||||||
{
|
{
|
||||||
return this->sourceChannel_;
|
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());
|
this->scrollBar_->addHighlight(message->getScrollBarHighlight());
|
||||||
}
|
}
|
||||||
|
@ -712,7 +814,8 @@ void ChannelView::messageAddedAtStart(std::vector<MessagePtr> &messages)
|
||||||
/// Create message layouts
|
/// Create message layouts
|
||||||
for (size_t i = 0; i < messages.size(); i++)
|
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
|
// alternate color
|
||||||
if (!this->lastMessageHasAlternateBackgroundReverse_)
|
if (!this->lastMessageHasAlternateBackgroundReverse_)
|
||||||
|
@ -732,15 +835,17 @@ void ChannelView::messageAddedAtStart(std::vector<MessagePtr> &messages)
|
||||||
this->scrollBar_->offset(qreal(messages.size()));
|
this->scrollBar_->offset(qreal(messages.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add highlights
|
if (this->showScrollbarHighlights())
|
||||||
std::vector<ScrollbarHighlight> highlights;
|
|
||||||
highlights.reserve(messages.size());
|
|
||||||
for (size_t i = 0; i < messages.size(); i++)
|
|
||||||
{
|
{
|
||||||
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->messageWasAdded_ = true;
|
||||||
this->queueLayout();
|
this->queueLayout();
|
||||||
|
@ -855,7 +960,7 @@ MessageElementFlags ChannelView::getFlags() const
|
||||||
{
|
{
|
||||||
flags.set(MessageElementFlag::ModeratorTools);
|
flags.set(MessageElementFlag::ModeratorTools);
|
||||||
}
|
}
|
||||||
if (this->channel_ == app->twitch.server->mentionsChannel)
|
if (this->underlyingChannel_ == app->twitch.server->mentionsChannel)
|
||||||
{
|
{
|
||||||
flags.set(MessageElementFlag::ChannelName);
|
flags.set(MessageElementFlag::ChannelName);
|
||||||
flags.unset(MessageElementFlag::ChannelPointReward);
|
flags.unset(MessageElementFlag::ChannelPointReward);
|
||||||
|
@ -906,7 +1011,8 @@ void ChannelView::drawMessages(QPainter &painter)
|
||||||
bool windowFocused = this->window() == QApplication::activeWindow();
|
bool windowFocused = this->window() == QApplication::activeWindow();
|
||||||
|
|
||||||
auto app = getApp();
|
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)
|
for (size_t i = start; i < messagesSnapshot.size(); ++i)
|
||||||
{
|
{
|
||||||
|
@ -1830,8 +1936,9 @@ void ChannelView::hideEvent(QHideEvent *)
|
||||||
void ChannelView::showUserInfoPopup(const QString &userName)
|
void ChannelView::showUserInfoPopup(const QString &userName)
|
||||||
{
|
{
|
||||||
auto *userPopup = new UserInfoPopup(getSettings()->autoCloseUserPopup);
|
auto *userPopup = new UserInfoPopup(getSettings()->autoCloseUserPopup);
|
||||||
userPopup->setData(userName, this->hasSourceChannel() ? this->sourceChannel_
|
userPopup->setData(userName, this->hasSourceChannel()
|
||||||
: this->channel_);
|
? this->sourceChannel_
|
||||||
|
: this->underlyingChannel_);
|
||||||
QPoint offset(int(150 * this->scale()), int(70 * this->scale()));
|
QPoint offset(int(150 * this->scale()), int(70 * this->scale()));
|
||||||
userPopup->move(QCursor::pos() - offset);
|
userPopup->move(QCursor::pos() - offset);
|
||||||
userPopup->show();
|
userPopup->show();
|
||||||
|
@ -1871,9 +1978,9 @@ void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link,
|
||||||
.replace("{msg-id}", layout->getMessage()->id)
|
.replace("{msg-id}", layout->getMessage()->id)
|
||||||
.replace("{message}", layout->getMessage()->messageText);
|
.replace("{message}", layout->getMessage()->messageText);
|
||||||
|
|
||||||
value =
|
value = getApp()->commands->execCommand(
|
||||||
getApp()->commands->execCommand(value, this->channel_, false);
|
value, this->underlyingChannel_, false);
|
||||||
this->channel_->sendMessage(value);
|
this->underlyingChannel_->sendMessage(value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
#include "common/FlagsEnum.hpp"
|
#include "common/FlagsEnum.hpp"
|
||||||
|
#include "controllers/filters/FilterSet.hpp"
|
||||||
#include "messages/Image.hpp"
|
#include "messages/Image.hpp"
|
||||||
#include "messages/LimitedQueue.hpp"
|
#include "messages/LimitedQueue.hpp"
|
||||||
#include "messages/LimitedQueueSnapshot.hpp"
|
#include "messages/LimitedQueueSnapshot.hpp"
|
||||||
|
@ -76,6 +77,10 @@ public:
|
||||||
ChannelPtr channel();
|
ChannelPtr channel();
|
||||||
void setChannel(ChannelPtr channel_);
|
void setChannel(ChannelPtr channel_);
|
||||||
|
|
||||||
|
void setFilters(const QList<QUuid> &ids);
|
||||||
|
const QList<QUuid> getFilterIds() const;
|
||||||
|
FilterSetPtr getFilterSet() const;
|
||||||
|
|
||||||
ChannelPtr sourceChannel() const;
|
ChannelPtr sourceChannel() const;
|
||||||
void setSourceChannel(ChannelPtr sourceChannel);
|
void setSourceChannel(ChannelPtr sourceChannel);
|
||||||
bool hasSourceChannel() const;
|
bool hasSourceChannel() const;
|
||||||
|
@ -178,11 +183,20 @@ private:
|
||||||
LimitedQueueSnapshot<MessageLayoutPtr> snapshot_;
|
LimitedQueueSnapshot<MessageLayoutPtr> snapshot_;
|
||||||
|
|
||||||
ChannelPtr channel_;
|
ChannelPtr channel_;
|
||||||
|
ChannelPtr underlyingChannel_;
|
||||||
ChannelPtr sourceChannel_;
|
ChannelPtr sourceChannel_;
|
||||||
|
|
||||||
Scrollbar *scrollBar_;
|
Scrollbar *scrollBar_;
|
||||||
EffectLabel *goToBottom_;
|
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
|
// This variable can be used to decide whether or not we should render the
|
||||||
// "Show latest messages" button
|
// "Show latest messages" button
|
||||||
bool showingLatestMessages_ = true;
|
bool showingLatestMessages_ = true;
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
ChannelPtr SearchPopup::filter(const QString &text, const QString &channelName,
|
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));
|
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 all predicates match, add the message to the channel
|
||||||
if (accept)
|
if (accept)
|
||||||
channel->addMessage(message);
|
channel->addMessage(message);
|
||||||
|
@ -59,6 +63,11 @@ SearchPopup::SearchPopup()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SearchPopup::setChannelFilters(FilterSetPtr filters)
|
||||||
|
{
|
||||||
|
this->channelFilters_ = filters;
|
||||||
|
}
|
||||||
|
|
||||||
void SearchPopup::setChannel(const ChannelPtr &channel)
|
void SearchPopup::setChannel(const ChannelPtr &channel)
|
||||||
{
|
{
|
||||||
this->channelName_ = channel->getName();
|
this->channelName_ = channel->getName();
|
||||||
|
@ -76,7 +85,8 @@ void SearchPopup::updateWindowTitle()
|
||||||
void SearchPopup::search()
|
void SearchPopup::search()
|
||||||
{
|
{
|
||||||
this->channelView_->setChannel(filter(this->searchInput_->text(),
|
this->channelView_->setChannel(filter(this->searchInput_->text(),
|
||||||
this->channelName_, this->snapshot_));
|
this->channelName_, this->snapshot_,
|
||||||
|
this->channelFilters_));
|
||||||
}
|
}
|
||||||
|
|
||||||
void SearchPopup::initLayout()
|
void SearchPopup::initLayout()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ForwardDecl.hpp"
|
#include "ForwardDecl.hpp"
|
||||||
|
#include "controllers/filters/FilterSet.hpp"
|
||||||
#include "messages/LimitedQueueSnapshot.hpp"
|
#include "messages/LimitedQueueSnapshot.hpp"
|
||||||
#include "messages/search/MessagePredicate.hpp"
|
#include "messages/search/MessagePredicate.hpp"
|
||||||
#include "widgets/BasePopup.hpp"
|
#include "widgets/BasePopup.hpp"
|
||||||
|
@ -17,6 +18,7 @@ public:
|
||||||
SearchPopup();
|
SearchPopup();
|
||||||
|
|
||||||
virtual void setChannel(const ChannelPtr &channel);
|
virtual void setChannel(const ChannelPtr &channel);
|
||||||
|
virtual void setChannelFilters(FilterSetPtr filters);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void updateWindowTitle();
|
virtual void updateWindowTitle();
|
||||||
|
@ -32,12 +34,14 @@ private:
|
||||||
* @param text the search query -- will be parsed for MessagePredicates
|
* @param text the search query -- will be parsed for MessagePredicates
|
||||||
* @param channelName name of the channel to be returned
|
* @param channelName name of the channel to be returned
|
||||||
* @param snapshot list of messages to filter
|
* @param snapshot list of messages to filter
|
||||||
|
* @param filterSet channel filter to apply
|
||||||
*
|
*
|
||||||
* @return a ChannelPtr with "channelName" and the filtered messages from
|
* @return a ChannelPtr with "channelName" and the filtered messages from
|
||||||
* "snapshot"
|
* "snapshot"
|
||||||
*/
|
*/
|
||||||
static ChannelPtr filter(const QString &text, const QString &channelName,
|
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
|
* @brief Checks the input for tags and registers their corresponding
|
||||||
|
@ -53,6 +57,7 @@ private:
|
||||||
QLineEdit *searchInput_{};
|
QLineEdit *searchInput_{};
|
||||||
ChannelView *channelView_{};
|
ChannelView *channelView_{};
|
||||||
QString channelName_{};
|
QString channelName_{};
|
||||||
|
FilterSetPtr channelFilters_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
111
src/widgets/settingspages/FiltersPage.cpp
Normal file
111
src/widgets/settingspages/FiltersPage.cpp
Normal 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
src/widgets/settingspages/FiltersPage.hpp
Normal file
25
src/widgets/settingspages/FiltersPage.hpp
Normal 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
|
|
@ -21,6 +21,7 @@
|
||||||
#include "widgets/Window.hpp"
|
#include "widgets/Window.hpp"
|
||||||
#include "widgets/dialogs/QualityPopup.hpp"
|
#include "widgets/dialogs/QualityPopup.hpp"
|
||||||
#include "widgets/dialogs/SelectChannelDialog.hpp"
|
#include "widgets/dialogs/SelectChannelDialog.hpp"
|
||||||
|
#include "widgets/dialogs/SelectChannelFiltersDialog.hpp"
|
||||||
#include "widgets/dialogs/TextInputDialog.hpp"
|
#include "widgets/dialogs/TextInputDialog.hpp"
|
||||||
#include "widgets/dialogs/UserInfoPopup.hpp"
|
#include "widgets/dialogs/UserInfoPopup.hpp"
|
||||||
#include "widgets/helper/ChannelView.hpp"
|
#include "widgets/helper/ChannelView.hpp"
|
||||||
|
@ -749,10 +750,33 @@ void Split::copyToClipboard()
|
||||||
crossPlatformCopy(this->view_->getSelectedText());
|
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()
|
void Split::showSearch()
|
||||||
{
|
{
|
||||||
SearchPopup *popup = new SearchPopup();
|
SearchPopup *popup = new SearchPopup();
|
||||||
|
|
||||||
|
popup->setChannelFilters(this->view_->getFilterSet());
|
||||||
popup->setAttribute(Qt::WA_DeleteOnClose);
|
popup->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
popup->setChannel(this->getChannel());
|
popup->setChannel(this->getChannel());
|
||||||
popup->show();
|
popup->show();
|
||||||
|
|
|
@ -53,6 +53,9 @@ public:
|
||||||
ChannelPtr getChannel();
|
ChannelPtr getChannel();
|
||||||
void setChannel(IndirectChannel newChannel);
|
void setChannel(IndirectChannel newChannel);
|
||||||
|
|
||||||
|
void setFilters(const QList<QUuid> ids);
|
||||||
|
const QList<QUuid> getFilters() const;
|
||||||
|
|
||||||
void setModerationMode(bool value);
|
void setModerationMode(bool value);
|
||||||
bool getModerationMode() const;
|
bool getModerationMode() const;
|
||||||
|
|
||||||
|
@ -132,6 +135,7 @@ public slots:
|
||||||
void openInStreamlink();
|
void openInStreamlink();
|
||||||
void openWithCustomScheme();
|
void openWithCustomScheme();
|
||||||
void copyToClipboard();
|
void copyToClipboard();
|
||||||
|
void setFiltersDialog();
|
||||||
void showSearch();
|
void showSearch();
|
||||||
void showViewerList();
|
void showViewerList();
|
||||||
void openSubPage();
|
void openSubPage();
|
||||||
|
|
|
@ -716,6 +716,7 @@ void SplitContainer::applyFromDescriptorRecursively(
|
||||||
auto *split = new Split(this);
|
auto *split = new Split(this);
|
||||||
split->setChannel(WindowManager::decodeChannel(splitNode));
|
split->setChannel(WindowManager::decodeChannel(splitNode));
|
||||||
split->setModerationMode(splitNode.moderationMode_);
|
split->setModerationMode(splitNode.moderationMode_);
|
||||||
|
split->setFilters(splitNode.filters_);
|
||||||
|
|
||||||
this->appendSplit(split);
|
this->appendSplit(split);
|
||||||
}
|
}
|
||||||
|
@ -748,6 +749,7 @@ void SplitContainer::applyFromDescriptorRecursively(
|
||||||
auto *split = new Split(this);
|
auto *split = new Split(this);
|
||||||
split->setChannel(WindowManager::decodeChannel(splitNode));
|
split->setChannel(WindowManager::decodeChannel(splitNode));
|
||||||
split->setModerationMode(splitNode.moderationMode_);
|
split->setModerationMode(splitNode.moderationMode_);
|
||||||
|
split->setFilters(splitNode.filters_);
|
||||||
|
|
||||||
Node *_node = new Node();
|
Node *_node = new Node();
|
||||||
_node->parent_ = node;
|
_node->parent_ = node;
|
||||||
|
|
|
@ -327,6 +327,7 @@ std::unique_ptr<QMenu> SplitHeader::createMainMenu()
|
||||||
QKeySequence("Ctrl+N"));
|
QKeySequence("Ctrl+N"));
|
||||||
menu->addAction("Search", this->split_, &Split::showSearch,
|
menu->addAction("Search", this->split_, &Split::showSearch,
|
||||||
QKeySequence("Ctrl+F"));
|
QKeySequence("Ctrl+F"));
|
||||||
|
menu->addAction("Set filters", this->split_, &Split::setFiltersDialog);
|
||||||
menu->addSeparator();
|
menu->addSeparator();
|
||||||
#ifdef USEWEBENGINE
|
#ifdef USEWEBENGINE
|
||||||
this->dropdownMenu.addAction("Start watching", this, [this] {
|
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);
|
this->titleLabel_->setText(title.isEmpty() ? "<empty>" : title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue