diff --git a/CHANGELOG.md b/CHANGELOG.md index 97722f477..5e620c0b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Minor: Added ability to negate search options by prefixing it with an exclamation mark (e.g. `!badge:mod` to search for messages where the author does not have the moderator badge). (#4207) - Minor: Search window input will automatically use currently selected text if present. (#4178) - Minor: Cleared up highlight sound settings (#4194) - Bugfix: Fixed highlight sounds not reloading on change properly. (#4194) diff --git a/src/messages/search/AuthorPredicate.cpp b/src/messages/search/AuthorPredicate.cpp index 77e02b296..c84b5d2e3 100644 --- a/src/messages/search/AuthorPredicate.cpp +++ b/src/messages/search/AuthorPredicate.cpp @@ -4,20 +4,18 @@ namespace chatterino { -AuthorPredicate::AuthorPredicate(const QStringList &authors) - : authors_() +AuthorPredicate::AuthorPredicate(const QString &authors, bool negate) + : MessagePredicate(negate) + , authors_() { // Check if any comma-seperated values were passed and transform those - for (const auto &entry : authors) + for (const auto &author : authors.split(',', Qt::SkipEmptyParts)) { - for (const auto &author : entry.split(',', Qt::SkipEmptyParts)) - { - this->authors_ << author; - } + this->authors_ << author; } } -bool AuthorPredicate::appliesTo(const Message &message) +bool AuthorPredicate::appliesToImpl(const Message &message) { return authors_.contains(message.displayName, Qt::CaseInsensitive) || authors_.contains(message.loginName, Qt::CaseInsensitive); diff --git a/src/messages/search/AuthorPredicate.hpp b/src/messages/search/AuthorPredicate.hpp index 8b3bbced2..2fb25c985 100644 --- a/src/messages/search/AuthorPredicate.hpp +++ b/src/messages/search/AuthorPredicate.hpp @@ -16,10 +16,12 @@ public: /** * @brief Create an AuthorPredicate with a list of users to search for. * - * @param authors a list of user names that a message should be sent from + * @param authors one or more comma-separated user names that a message should be sent from + * @param negate when set, excludes list of user names from results */ - AuthorPredicate(const QStringList &authors); + AuthorPredicate(const QString &authors, bool negate); +protected: /** * @brief Checks whether the message is authored by any of the users passed * in the constructor. @@ -28,7 +30,7 @@ public: * @return true if the message was authored by one of the specified users, * false otherwise */ - bool appliesTo(const Message &message); + bool appliesToImpl(const Message &message) override; private: /// Holds the user names that will be searched for diff --git a/src/messages/search/BadgePredicate.cpp b/src/messages/search/BadgePredicate.cpp index 308624658..555a2b36c 100644 --- a/src/messages/search/BadgePredicate.cpp +++ b/src/messages/search/BadgePredicate.cpp @@ -4,35 +4,33 @@ namespace chatterino { -BadgePredicate::BadgePredicate(const QStringList &badges) +BadgePredicate::BadgePredicate(const QString &badges, bool negate) + : MessagePredicate(negate) { // Check if any comma-seperated values were passed and transform those - for (const auto &entry : badges) + for (const auto &badge : badges.split(',', Qt::SkipEmptyParts)) { - for (const auto &badge : entry.split(',', Qt::SkipEmptyParts)) + // convert short form name of certain badges to formal name + if (badge.compare("mod", Qt::CaseInsensitive) == 0) { - // convert short form name of certain badges to formal name - if (badge.compare("mod", Qt::CaseInsensitive) == 0) - { - this->badges_ << "moderator"; - } - else if (badge.compare("sub", Qt::CaseInsensitive) == 0) - { - this->badges_ << "subscriber"; - } - else if (badge.compare("prime", Qt::CaseInsensitive) == 0) - { - this->badges_ << "premium"; - } - else - { - this->badges_ << badge; - } + this->badges_ << "moderator"; + } + else if (badge.compare("sub", Qt::CaseInsensitive) == 0) + { + this->badges_ << "subscriber"; + } + else if (badge.compare("prime", Qt::CaseInsensitive) == 0) + { + this->badges_ << "premium"; + } + else + { + this->badges_ << badge; } } } -bool BadgePredicate::appliesTo(const Message &message) +bool BadgePredicate::appliesToImpl(const Message &message) { for (const Badge &badge : message.badges) { diff --git a/src/messages/search/BadgePredicate.hpp b/src/messages/search/BadgePredicate.hpp index f4e990eec..510f6e057 100644 --- a/src/messages/search/BadgePredicate.hpp +++ b/src/messages/search/BadgePredicate.hpp @@ -16,10 +16,12 @@ public: /** * @brief Create an BadgePredicate with a list of badges to search for. * - * @param badges a list of badges that a message should contain + * @param badges one or more comma-separated badges that a message should contain + * @param negate when set, excludes list of badges from results */ - BadgePredicate(const QStringList &badges); + BadgePredicate(const QString &badges, bool negate); +protected: /** * @brief Checks whether the message contains any of the badges passed * in the constructor. @@ -28,7 +30,7 @@ public: * @return true if the message contains a badge listed in the specified badges, * false otherwise */ - bool appliesTo(const Message &message) override; + bool appliesToImpl(const Message &message) override; private: /// Holds the badges that will be searched for diff --git a/src/messages/search/ChannelPredicate.cpp b/src/messages/search/ChannelPredicate.cpp index 798c2df52..2f7f3f3fd 100644 --- a/src/messages/search/ChannelPredicate.cpp +++ b/src/messages/search/ChannelPredicate.cpp @@ -4,20 +4,18 @@ namespace chatterino { -ChannelPredicate::ChannelPredicate(const QStringList &channels) - : channels_() +ChannelPredicate::ChannelPredicate(const QString &channels, bool negate) + : MessagePredicate(negate) + , channels_() { // Check if any comma-seperated values were passed and transform those - for (const auto &entry : channels) + for (const auto &channel : channels.split(',', Qt::SkipEmptyParts)) { - for (const auto &channel : entry.split(',', Qt::SkipEmptyParts)) - { - this->channels_ << channel; - } + this->channels_ << channel; } } -bool ChannelPredicate::appliesTo(const Message &message) +bool ChannelPredicate::appliesToImpl(const Message &message) { return channels_.contains(message.channelName, Qt::CaseInsensitive); } diff --git a/src/messages/search/ChannelPredicate.hpp b/src/messages/search/ChannelPredicate.hpp index 08521942a..a9f189089 100644 --- a/src/messages/search/ChannelPredicate.hpp +++ b/src/messages/search/ChannelPredicate.hpp @@ -16,10 +16,12 @@ public: /** * @brief Create a ChannelPredicate with a list of channels to search for. * - * @param channels a list of channel names that a message should be sent in + * @param channels one or more comma-separated channel names that a message should be sent in + * @param negate when set, excludes list of channel names from results */ - ChannelPredicate(const QStringList &channels); + ChannelPredicate(const QString &channels, bool negate); +protected: /** * @brief Checks whether the message was sent in any of the channels passed * in the constructor. @@ -28,7 +30,7 @@ public: * @return true if the message was sent in one of the specified channels, * false otherwise */ - bool appliesTo(const Message &message); + bool appliesToImpl(const Message &message) override; private: /// Holds the channel names that will be searched for diff --git a/src/messages/search/LinkPredicate.cpp b/src/messages/search/LinkPredicate.cpp index 45860a236..a592e4015 100644 --- a/src/messages/search/LinkPredicate.cpp +++ b/src/messages/search/LinkPredicate.cpp @@ -5,11 +5,12 @@ namespace chatterino { -LinkPredicate::LinkPredicate() +LinkPredicate::LinkPredicate(bool negate) + : MessagePredicate(negate) { } -bool LinkPredicate::appliesTo(const Message &message) +bool LinkPredicate::appliesToImpl(const Message &message) { for (const auto &word : message.messageText.split(' ', Qt::SkipEmptyParts)) { diff --git a/src/messages/search/LinkPredicate.hpp b/src/messages/search/LinkPredicate.hpp index 4d73920b5..c6419402f 100644 --- a/src/messages/search/LinkPredicate.hpp +++ b/src/messages/search/LinkPredicate.hpp @@ -12,15 +12,21 @@ namespace chatterino { class LinkPredicate : public MessagePredicate { public: - LinkPredicate(); + /** + * @brief Create an LinkPredicate + * + * @param negate when set, excludes messages containing links from results + */ + LinkPredicate(bool negate); +protected: /** * @brief Checks whether the message contains a link. * * @param message the message to check * @return true if the message contains a link, false otherwise */ - bool appliesTo(const Message &message); + bool appliesToImpl(const Message &message) override; }; } // namespace chatterino diff --git a/src/messages/search/MessageFlagsPredicate.cpp b/src/messages/search/MessageFlagsPredicate.cpp index 84601a183..9bcee9843 100644 --- a/src/messages/search/MessageFlagsPredicate.cpp +++ b/src/messages/search/MessageFlagsPredicate.cpp @@ -4,8 +4,9 @@ namespace chatterino { -MessageFlagsPredicate::MessageFlagsPredicate(const QString &flags) - : flags_() +MessageFlagsPredicate::MessageFlagsPredicate(const QString &flags, bool negate) + : MessagePredicate(negate) + , flags_() { // Check if any comma-seperated values were passed and transform those for (const auto &flag : flags.split(',', Qt::SkipEmptyParts)) @@ -54,13 +55,15 @@ MessageFlagsPredicate::MessageFlagsPredicate(const QString &flags) } } -bool MessageFlagsPredicate::appliesTo(const Message &message) +bool MessageFlagsPredicate::appliesToImpl(const Message &message) { // Exclude timeout messages from system flag when timeout flag isn't present if (this->flags_.has(MessageFlag::System) && !this->flags_.has(MessageFlag::Timeout)) + { return message.flags.hasAny(flags_) && !message.flags.has(MessageFlag::Timeout); + } return message.flags.hasAny(flags_); } diff --git a/src/messages/search/MessageFlagsPredicate.hpp b/src/messages/search/MessageFlagsPredicate.hpp index 2c3896546..a74d72ae2 100644 --- a/src/messages/search/MessageFlagsPredicate.hpp +++ b/src/messages/search/MessageFlagsPredicate.hpp @@ -27,9 +27,11 @@ public: * "system" is used for the "System" flag. * * @param flags a string comma seperated list of names for the flags a message should have + * @param negate when set, excludes messages containg selected flags from results */ - MessageFlagsPredicate(const QString &flags); + MessageFlagsPredicate(const QString &flags, bool negate); +protected: /** * @brief Checks whether the message has any of the flags passed * in the constructor. @@ -38,7 +40,7 @@ public: * @return true if the message has at least one of the specified flags, * false otherwise */ - bool appliesTo(const Message &message); + bool appliesToImpl(const Message &message) override; private: /// Holds the flags that will be searched for diff --git a/src/messages/search/MessagePredicate.hpp b/src/messages/search/MessagePredicate.hpp index 6deb2de52..79806bd62 100644 --- a/src/messages/search/MessagePredicate.hpp +++ b/src/messages/search/MessagePredicate.hpp @@ -11,7 +11,7 @@ namespace chatterino { * * Message predicates define certain features a message can satisfy. * Features are represented by classes derived from this abstract class. - * A derived class must override `appliesTo` in order to test for the desired + * A derived class must override `appliesToImpl` in order to test for the desired * feature. */ class MessagePredicate @@ -19,15 +19,43 @@ class MessagePredicate public: virtual ~MessagePredicate() = default; + /** + * @brief Checks whether this predicate applies to the passed message + * + * Calls the derived classes `appliedTo` implementation, and respects the `isNegated_` flag + * it's set. + * + * @param message the message to check for this predicate + * @return true if this predicate applies, false otherwise + **/ + bool appliesTo(const Message &message) + { + auto result = this->appliesToImpl(message); + if (this->isNegated_) + { + return !result; + } + return result; + } + +protected: + explicit MessagePredicate(bool negate) + : isNegated_(negate) + { + } + /** * @brief Checks whether this predicate applies to the passed message. * - * Implementations of `appliesTo` should never change the message's content + * Implementations of `appliesToImpl` should never change the message's content * in order to be compatible with other MessagePredicates. * * @param message the message to check for this predicate * @return true if this predicate applies, false otherwise */ - virtual bool appliesTo(const Message &message) = 0; + virtual bool appliesToImpl(const Message &message) = 0; + +private: + const bool isNegated_ = false; }; } // namespace chatterino diff --git a/src/messages/search/RegexPredicate.cpp b/src/messages/search/RegexPredicate.cpp index b16dcf4d1..04a22f624 100644 --- a/src/messages/search/RegexPredicate.cpp +++ b/src/messages/search/RegexPredicate.cpp @@ -2,12 +2,13 @@ namespace chatterino { -RegexPredicate::RegexPredicate(const QString ®ex) - : regex_(regex, QRegularExpression::CaseInsensitiveOption) +RegexPredicate::RegexPredicate(const QString ®ex, bool negate) + : MessagePredicate(negate) + , regex_(regex, QRegularExpression::CaseInsensitiveOption) { } -bool RegexPredicate::appliesTo(const Message &message) +bool RegexPredicate::appliesToImpl(const Message &message) { if (!regex_.isValid()) { @@ -19,4 +20,4 @@ bool RegexPredicate::appliesTo(const Message &message) return match.hasMatch(); } -} // namespace chatterino \ No newline at end of file +} // namespace chatterino diff --git a/src/messages/search/RegexPredicate.hpp b/src/messages/search/RegexPredicate.hpp index fcf5d262e..594b985f0 100644 --- a/src/messages/search/RegexPredicate.hpp +++ b/src/messages/search/RegexPredicate.hpp @@ -20,9 +20,11 @@ public: * The message is being matched case-insensitively. * * @param regex the regex to match the message against + * @param negate when set, excludes messages matching the regex from results */ - RegexPredicate(const QString ®ex); + RegexPredicate(const QString ®ex, bool negate); +protected: /** * @brief Checks whether the message matches the regex passed in the * constructor @@ -32,11 +34,11 @@ public: * @param message the message to check * @return true if the message matches the regex, false otherwise */ - bool appliesTo(const Message &message); + bool appliesToImpl(const Message &message) override; private: /// Holds the regular expression to match the message against QRegularExpression regex_; }; -} // namespace chatterino \ No newline at end of file +} // namespace chatterino diff --git a/src/messages/search/SubstringPredicate.cpp b/src/messages/search/SubstringPredicate.cpp index 752ed9338..736cb2cc1 100644 --- a/src/messages/search/SubstringPredicate.cpp +++ b/src/messages/search/SubstringPredicate.cpp @@ -3,11 +3,12 @@ namespace chatterino { SubstringPredicate::SubstringPredicate(const QString &search) - : search_(search) + : MessagePredicate(false) + , search_(search) { } -bool SubstringPredicate::appliesTo(const Message &message) +bool SubstringPredicate::appliesToImpl(const Message &message) { return message.searchText.contains(this->search_, Qt::CaseInsensitive); } diff --git a/src/messages/search/SubstringPredicate.hpp b/src/messages/search/SubstringPredicate.hpp index 31f43102e..e6b99f11b 100644 --- a/src/messages/search/SubstringPredicate.hpp +++ b/src/messages/search/SubstringPredicate.hpp @@ -22,6 +22,7 @@ public: */ SubstringPredicate(const QString &search); +protected: /** * @brief Checks whether the message contains the substring passed in the * constructor. @@ -31,7 +32,7 @@ public: * @param message the message to check * @return true if the message contains the substring, false otherwise */ - bool appliesTo(const Message &message); + bool appliesToImpl(const Message &message) override; private: /// Holds the substring to search for in a message's `messageText` diff --git a/src/messages/search/SubtierPredicate.cpp b/src/messages/search/SubtierPredicate.cpp index 8c4ce3f13..81db09f28 100644 --- a/src/messages/search/SubtierPredicate.cpp +++ b/src/messages/search/SubtierPredicate.cpp @@ -4,19 +4,17 @@ namespace chatterino { -SubtierPredicate::SubtierPredicate(const QStringList &subtiers) +SubtierPredicate::SubtierPredicate(const QString &subtiers, bool negate) + : MessagePredicate(negate) { // Check if any comma-seperated values were passed and transform those - for (const auto &entry : subtiers) + for (const auto &subtier : subtiers.split(',', Qt::SkipEmptyParts)) { - for (const auto &subtier : entry.split(',', Qt::SkipEmptyParts)) - { - this->subtiers_ << subtier; - } + this->subtiers_ << subtier; } } -bool SubtierPredicate::appliesTo(const Message &message) +bool SubtierPredicate::appliesToImpl(const Message &message) { for (const Badge &badge : message.badges) { diff --git a/src/messages/search/SubtierPredicate.hpp b/src/messages/search/SubtierPredicate.hpp index 87bcfe10d..bb5caba2a 100644 --- a/src/messages/search/SubtierPredicate.hpp +++ b/src/messages/search/SubtierPredicate.hpp @@ -16,10 +16,12 @@ public: /** * @brief Create an SubtierPredicate with a list of subtiers to search for. * - * @param subtiers a list of subtiers that a message should contain + * @param subtiers one or more comma-separated subtiers that the message should contain + * @param negate when set, excludes messages containing selected subtiers from results */ - SubtierPredicate(const QStringList &subtiers); + SubtierPredicate(const QString &subtiers, bool negate); +protected: /** * @brief Checks whether the message contains any of the subtiers passed * in the constructor. @@ -28,7 +30,7 @@ public: * @return true if the message contains a subtier listed in the specified subtiers, * false otherwise */ - bool appliesTo(const Message &message) override; + bool appliesToImpl(const Message &message) override; private: /// Holds the subtiers that will be searched for diff --git a/src/widgets/helper/SearchPopup.cpp b/src/widgets/helper/SearchPopup.cpp index 990e6759c..a7524abf4 100644 --- a/src/widgets/helper/SearchPopup.cpp +++ b/src/widgets/helper/SearchPopup.cpp @@ -302,55 +302,57 @@ std::vector> SearchPopup::parsePredicates( // It also ignores whitespaces in values when being surrounded by quotation // marks, to enable inputs like this => regex:"kappa 123" static QRegularExpression predicateRegex( - R"lit((?:(?\w+):(?".+?"|[^\s]+))|[^\s]+?(?=$|\s))lit"); + R"lit((?[!\-])?(?:(?\w+):(?".+?"|[^\s]+))|[^\s]+?(?=$|\s))lit"); static QRegularExpression trimQuotationMarksRegex(R"(^"|"$)"); QRegularExpressionMatchIterator it = predicateRegex.globalMatch(input); std::vector> predicates; - QStringList authors; - QStringList channels; - QStringList badges; - QStringList subtiers; while (it.hasNext()) { QRegularExpressionMatch match = it.next(); QString name = match.captured("name"); - + bool isNegated = !match.captured("negation").isEmpty(); QString value = match.captured("value"); value.remove(trimQuotationMarksRegex); // match predicates + if (name == "from") { - authors.append(value); + predicates.push_back( + std::make_unique(value, isNegated)); } else if (name == "badge") { - badges.append(value); + predicates.push_back( + std::make_unique(value, isNegated)); } else if (name == "subtier") { - subtiers.append(value); + predicates.push_back( + std::make_unique(value, isNegated)); } else if (name == "has" && value == "link") { - predicates.push_back(std::make_unique()); + predicates.push_back(std::make_unique(isNegated)); } else if (name == "in") { - channels.append(value); + predicates.push_back( + std::make_unique(value, isNegated)); } else if (name == "is") { predicates.push_back( - std::make_unique(value)); + std::make_unique(value, isNegated)); } else if (name == "regex") { - predicates.push_back(std::make_unique(value)); + predicates.push_back( + std::make_unique(value, isNegated)); } else { @@ -359,26 +361,6 @@ std::vector> SearchPopup::parsePredicates( } } - if (!authors.empty()) - { - predicates.push_back(std::make_unique(authors)); - } - - if (!channels.empty()) - { - predicates.push_back(std::make_unique(channels)); - } - - if (!badges.empty()) - { - predicates.push_back(std::make_unique(badges)); - } - - if (!subtiers.empty()) - { - predicates.push_back(std::make_unique(subtiers)); - } - return predicates; }