From 4b9ebb972342e05fa727fdb814bf6d2c0b871aae Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Mon, 27 Nov 2023 21:58:51 +0100 Subject: [PATCH 01/10] Make emote completion a lot smarter --- .../strategies/ClassicEmoteStrategy.cpp | 207 +++++++++++++----- 1 file changed, 157 insertions(+), 50 deletions(-) diff --git a/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp b/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp index 1a427483a7f..1970f72e9c0 100644 --- a/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp +++ b/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp @@ -1,52 +1,168 @@ #include "controllers/completion/strategies/ClassicEmoteStrategy.hpp" +#include "common/QLogging.hpp" +#include "controllers/completion/sources/EmoteSource.hpp" #include "singletons/Settings.hpp" #include "util/Helpers.hpp" -namespace chatterino::completion { +#include -void ClassicEmoteStrategy::apply(const std::vector &items, - std::vector &output, - const QString &query) const -{ - QString normalizedQuery = query; - if (normalizedQuery.startsWith(':')) - { - normalizedQuery = normalizedQuery.mid(1); - } +#include - // First pass: filter by contains match - for (const auto &item : items) +namespace chatterino::completion { +namespace { + /** + * @brief This function calculates the "cost" of the changes that need to + * be done to the query to make it the value. + * + * By default an emote with more differences in character casing from the + * query will get a higher cost, each additional letter also increases cost. + * + * @param prioritizeUpper If set, then differences in casing don't matter, but + * instead the more lowercase letters an emote contains, the higher cost it + * will get. Additional letters also increase the cost in this mode. + * + * @return How different the emote is from query. Values in the range [-10, + * \infty]. Negative cost means exact match. + */ + int costOfEmote(const QString &query, const QString &emote, + bool prioritizeUpper) { - if (item.searchName.contains(normalizedQuery, Qt::CaseInsensitive)) + int score = 0; + + if (prioritizeUpper) { - output.push_back(item); + // We are in case 3, push 'more uppercase' emotes to the top + for (const auto i : emote) + { + score += int(!i.isUpper()); + } } - } + else + { + // Push more matching emotes to the top + int len = std::min(emote.size(), query.size()); + for (int i = 0; i < len; i++) + { + // Different casing gets a higher cost score + score += query.at(i).isUpper() ^ emote.at(i).isUpper(); + } + } + // No case differences, put this at the top + if (score == 0) + { + score = -10; + } + + auto diff = emote.size() - query.size(); + if (diff > 0) + { + // Case changes are way less changes to the user compared to adding characters + score += diff * 100; + } + return score; + }; - // Second pass: if there is an exact match, put that emote first - for (size_t i = 1; i < output.size(); i++) + // This contains the brains of emote tab completion. Updates output to sorted completions. + // Ensure that the query string is already normalized, that is doesn't have a leading ':' + // matchingFunction is used for testing if the emote should be included in the search. + void completeEmotes( + const std::vector &items, std::vector &output, + const QString &query, + const std::function + &matchingFunction) { - auto emoteText = output.at(i).searchName; + // Given these emotes: pajaW, PAJAW + // There are a few cases of input: + // 1. "pajaw" expect {pajaW, PAJAW} - no uppercase characters, do regular case insensitive search + // 2. "PA" expect {PAJAW} - uppercase characters, case sensitive search gives results + // 3. "Pajaw" expect {PAJAW, pajaW} - case sensitive search doesn't give results, need to use sorting + // 4. "NOTHING" expect {} - no results + // 5. "nothing" expect {} - same as 4 but first search is case insensitive + + // Check if the query contains any uppercase characters + // This tells us if we're in case 1 or 5 vs all others + bool haveUpper = + std::any_of(query.begin(), query.end(), [](const QChar &c) { + return c.isUpper(); + }); - // test for match or match with colon at start for emotes like ":)" - if (emoteText.compare(normalizedQuery, Qt::CaseInsensitive) == 0 || - emoteText.compare(":" + normalizedQuery, Qt::CaseInsensitive) == 0) + // First search, for case 1 it will be case insensitive, + // for cases 2, 3 and 4 it will be case sensitive + for (const auto &item : items) { - auto emote = output[i]; - output.erase(output.begin() + int(i)); - output.insert(output.begin(), emote); - break; + if (matchingFunction( + item, query, + haveUpper ? Qt::CaseSensitive : Qt::CaseInsensitive)) + { + output.push_back(item); + } } + + // if case 3: then true; false otherwise + bool prioritizeUpper = false; + + // No results from search + if (output.empty()) + { + if (!haveUpper) + { + // Optimisation: First search was case insensitive, but we found nothing + // There is nothing to be found: case 5. + return; + } + // Case sensitive search from case 2 found nothing, therefore we can + // only be in case 3 or 4. + + prioritizeUpper = true; + // Run the search again but this time without case sensitivity + for (const auto &item : items) + { + if (matchingFunction(item, query, Qt::CaseInsensitive)) + { + output.push_back(item); + } + } + if (output.empty()) + { + // The second search found nothing, so don't even try to sort: case 4 + return; + } + } + + std::sort( + output.begin(), output.end(), + [query, prioritizeUpper](const EmoteItem &a, + const EmoteItem &b) -> bool { + auto costA = costOfEmote(query, a.searchName, prioritizeUpper); + auto costB = costOfEmote(query, b.searchName, prioritizeUpper); + if (costA == costB) + { + // Case difference and length came up tied for (a, b), break the tie + return QString::compare(a.searchName, b.searchName, + Qt::CaseInsensitive) < 0; + } + + return costA < costB; + }); } -} +} // namespace -struct CompletionEmoteOrder { - bool operator()(const EmoteItem &a, const EmoteItem &b) const +void ClassicEmoteStrategy::apply(const std::vector &items, + std::vector &output, + const QString &query) const +{ + QString normalizedQuery = query; + if (normalizedQuery.startsWith(':')) { - return compareEmoteStrings(a.searchName, b.searchName); + normalizedQuery = normalizedQuery.mid(1); } -}; + completeEmotes(items, output, normalizedQuery, + [](const EmoteItem &left, const QString &right, + Qt::CaseSensitivity caseHandling) { + return left.searchName.contains(right, caseHandling); + }); +} void ClassicTabEmoteStrategy::apply(const std::vector &items, std::vector &output, @@ -60,26 +176,17 @@ void ClassicTabEmoteStrategy::apply(const std::vector &items, // tab completion with : prefix should do emojis only emojiOnly = true; } - - std::set emotes; - - for (const auto &item : items) - { - if (emojiOnly ^ item.isEmoji) - { - continue; - } - - if (startsWithOrContains(item.searchName, normalizedQuery, - Qt::CaseInsensitive, - getSettings()->prefixOnlyEmoteCompletion)) - { - emotes.insert(item); - } - } - - output.reserve(emotes.size()); - output.assign(emotes.begin(), emotes.end()); + completeEmotes(items, output, normalizedQuery, + [emojiOnly](const EmoteItem &left, const QString &right, + Qt::CaseSensitivity caseHandling) -> bool { + if (emojiOnly ^ left.isEmoji) + { + return false; + } + return startsWithOrContains( + left.searchName, right, caseHandling, + getSettings()->prefixOnlyEmoteCompletion); + }); } } // namespace chatterino::completion From 598f5108e455b8d2dbeb303613294284e22ee1f6 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Mon, 27 Nov 2023 22:26:55 +0100 Subject: [PATCH 02/10] i hate q.*\.h --- src/controllers/completion/strategies/ClassicEmoteStrategy.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp b/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp index 1970f72e9c0..c74e4f1885b 100644 --- a/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp +++ b/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp @@ -5,7 +5,7 @@ #include "singletons/Settings.hpp" #include "util/Helpers.hpp" -#include +#include #include From d2b26a2385b34cfd79a47bb8a19a8bc8ab0c66be Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Mon, 27 Nov 2023 22:37:24 +0100 Subject: [PATCH 03/10] Rename ClassicEmoteStrategy to SmartEmoteStrategy --- src/CMakeLists.txt | 4 ++-- src/controllers/completion/TabCompletionModel.cpp | 5 ++--- ...sicEmoteStrategy.cpp => SmartEmoteStrategy.cpp} | 14 +++++++------- ...sicEmoteStrategy.hpp => SmartEmoteStrategy.hpp} | 4 ++-- src/widgets/splits/InputCompletionPopup.cpp | 4 ++-- 5 files changed, 15 insertions(+), 16 deletions(-) rename src/controllers/completion/strategies/{ClassicEmoteStrategy.cpp => SmartEmoteStrategy.cpp} (93%) rename src/controllers/completion/strategies/{ClassicEmoteStrategy.hpp => SmartEmoteStrategy.hpp} (81%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9b75d7e9022..b69b1eb46c5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -124,8 +124,8 @@ set(SOURCE_FILES controllers/completion/sources/UserSource.cpp controllers/completion/sources/UserSource.hpp controllers/completion/strategies/Strategy.hpp - controllers/completion/strategies/ClassicEmoteStrategy.cpp - controllers/completion/strategies/ClassicEmoteStrategy.hpp + controllers/completion/strategies/SmartEmoteStrategy.cpp + controllers/completion/strategies/SmartEmoteStrategy.hpp controllers/completion/strategies/ClassicUserStrategy.cpp controllers/completion/strategies/ClassicUserStrategy.hpp controllers/completion/strategies/CommandStrategy.cpp diff --git a/src/controllers/completion/TabCompletionModel.cpp b/src/controllers/completion/TabCompletionModel.cpp index 159e89f3d33..a2be57962e1 100644 --- a/src/controllers/completion/TabCompletionModel.cpp +++ b/src/controllers/completion/TabCompletionModel.cpp @@ -5,9 +5,9 @@ #include "controllers/completion/sources/EmoteSource.hpp" #include "controllers/completion/sources/UnifiedSource.hpp" #include "controllers/completion/sources/UserSource.hpp" -#include "controllers/completion/strategies/ClassicEmoteStrategy.hpp" #include "controllers/completion/strategies/ClassicUserStrategy.hpp" #include "controllers/completion/strategies/CommandStrategy.hpp" +#include "controllers/completion/strategies/SmartEmoteStrategy.hpp" #include "singletons/Settings.hpp" namespace chatterino { @@ -124,8 +124,7 @@ std::unique_ptr TabCompletionModel::buildSource( std::unique_ptr TabCompletionModel::buildEmoteSource() const { return std::make_unique( - &this->channel_, - std::make_unique()); + &this->channel_, std::make_unique()); } std::unique_ptr TabCompletionModel::buildUserSource( diff --git a/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp b/src/controllers/completion/strategies/SmartEmoteStrategy.cpp similarity index 93% rename from src/controllers/completion/strategies/ClassicEmoteStrategy.cpp rename to src/controllers/completion/strategies/SmartEmoteStrategy.cpp index c74e4f1885b..2923108a667 100644 --- a/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp +++ b/src/controllers/completion/strategies/SmartEmoteStrategy.cpp @@ -1,4 +1,4 @@ -#include "controllers/completion/strategies/ClassicEmoteStrategy.hpp" +#include "controllers/completion/strategies/SmartEmoteStrategy.hpp" #include "common/QLogging.hpp" #include "controllers/completion/sources/EmoteSource.hpp" @@ -148,9 +148,9 @@ namespace { } } // namespace -void ClassicEmoteStrategy::apply(const std::vector &items, - std::vector &output, - const QString &query) const +void SmartEmoteStrategy::apply(const std::vector &items, + std::vector &output, + const QString &query) const { QString normalizedQuery = query; if (normalizedQuery.startsWith(':')) @@ -164,9 +164,9 @@ void ClassicEmoteStrategy::apply(const std::vector &items, }); } -void ClassicTabEmoteStrategy::apply(const std::vector &items, - std::vector &output, - const QString &query) const +void SmartTabEmoteStrategy::apply(const std::vector &items, + std::vector &output, + const QString &query) const { bool emojiOnly = false; QString normalizedQuery = query; diff --git a/src/controllers/completion/strategies/ClassicEmoteStrategy.hpp b/src/controllers/completion/strategies/SmartEmoteStrategy.hpp similarity index 81% rename from src/controllers/completion/strategies/ClassicEmoteStrategy.hpp rename to src/controllers/completion/strategies/SmartEmoteStrategy.hpp index d231c8ac142..365e106b033 100644 --- a/src/controllers/completion/strategies/ClassicEmoteStrategy.hpp +++ b/src/controllers/completion/strategies/SmartEmoteStrategy.hpp @@ -5,14 +5,14 @@ namespace chatterino::completion { -class ClassicEmoteStrategy : public Strategy +class SmartEmoteStrategy : public Strategy { void apply(const std::vector &items, std::vector &output, const QString &query) const override; }; -class ClassicTabEmoteStrategy : public Strategy +class SmartTabEmoteStrategy : public Strategy { void apply(const std::vector &items, std::vector &output, diff --git a/src/widgets/splits/InputCompletionPopup.cpp b/src/widgets/splits/InputCompletionPopup.cpp index 2828551ea0b..bd26ff1e6ff 100644 --- a/src/widgets/splits/InputCompletionPopup.cpp +++ b/src/widgets/splits/InputCompletionPopup.cpp @@ -1,8 +1,8 @@ #include "widgets/splits/InputCompletionPopup.hpp" #include "controllers/completion/sources/UserSource.hpp" -#include "controllers/completion/strategies/ClassicEmoteStrategy.hpp" #include "controllers/completion/strategies/ClassicUserStrategy.hpp" +#include "controllers/completion/strategies/SmartEmoteStrategy.hpp" #include "singletons/Theme.hpp" #include "util/LayoutCreator.hpp" #include "widgets/splits/InputCompletionItem.hpp" @@ -62,7 +62,7 @@ std::unique_ptr InputCompletionPopup::getSource() const case CompletionKind::Emote: return std::make_unique( this->currentChannel_.get(), - std::make_unique(), + std::make_unique(), this->callback_); case CompletionKind::User: return std::make_unique( From 152d564cab05ffb384a1ecdce02f989ab2f95394 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Mon, 27 Nov 2023 23:15:18 +0100 Subject: [PATCH 04/10] Restore ClassicEmoteStrategy --- src/CMakeLists.txt | 8 +- .../strategies/ClassicEmoteStrategy.cpp | 85 +++++++++++++++++++ .../strategies/ClassicEmoteStrategy.hpp | 22 +++++ 3 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 src/controllers/completion/strategies/ClassicEmoteStrategy.cpp create mode 100644 src/controllers/completion/strategies/ClassicEmoteStrategy.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b69b1eb46c5..7aeccfc089e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -123,13 +123,15 @@ set(SOURCE_FILES controllers/completion/sources/UnifiedSource.hpp controllers/completion/sources/UserSource.cpp controllers/completion/sources/UserSource.hpp - controllers/completion/strategies/Strategy.hpp - controllers/completion/strategies/SmartEmoteStrategy.cpp - controllers/completion/strategies/SmartEmoteStrategy.hpp + controllers/completion/strategies/ClassicEmoteStrategy.hpp + controllers/completion/strategies/ClassicEmoteStrategy.hpp controllers/completion/strategies/ClassicUserStrategy.cpp controllers/completion/strategies/ClassicUserStrategy.hpp controllers/completion/strategies/CommandStrategy.cpp controllers/completion/strategies/CommandStrategy.hpp + controllers/completion/strategies/SmartEmoteStrategy.cpp + controllers/completion/strategies/SmartEmoteStrategy.cpp + controllers/completion/strategies/Strategy.hpp controllers/completion/TabCompletionModel.cpp controllers/completion/TabCompletionModel.hpp diff --git a/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp b/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp new file mode 100644 index 00000000000..1a427483a7f --- /dev/null +++ b/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp @@ -0,0 +1,85 @@ +#include "controllers/completion/strategies/ClassicEmoteStrategy.hpp" + +#include "singletons/Settings.hpp" +#include "util/Helpers.hpp" + +namespace chatterino::completion { + +void ClassicEmoteStrategy::apply(const std::vector &items, + std::vector &output, + const QString &query) const +{ + QString normalizedQuery = query; + if (normalizedQuery.startsWith(':')) + { + normalizedQuery = normalizedQuery.mid(1); + } + + // First pass: filter by contains match + for (const auto &item : items) + { + if (item.searchName.contains(normalizedQuery, Qt::CaseInsensitive)) + { + output.push_back(item); + } + } + + // Second pass: if there is an exact match, put that emote first + for (size_t i = 1; i < output.size(); i++) + { + auto emoteText = output.at(i).searchName; + + // test for match or match with colon at start for emotes like ":)" + if (emoteText.compare(normalizedQuery, Qt::CaseInsensitive) == 0 || + emoteText.compare(":" + normalizedQuery, Qt::CaseInsensitive) == 0) + { + auto emote = output[i]; + output.erase(output.begin() + int(i)); + output.insert(output.begin(), emote); + break; + } + } +} + +struct CompletionEmoteOrder { + bool operator()(const EmoteItem &a, const EmoteItem &b) const + { + return compareEmoteStrings(a.searchName, b.searchName); + } +}; + +void ClassicTabEmoteStrategy::apply(const std::vector &items, + std::vector &output, + const QString &query) const +{ + bool emojiOnly = false; + QString normalizedQuery = query; + if (normalizedQuery.startsWith(':')) + { + normalizedQuery = normalizedQuery.mid(1); + // tab completion with : prefix should do emojis only + emojiOnly = true; + } + + std::set emotes; + + for (const auto &item : items) + { + if (emojiOnly ^ item.isEmoji) + { + continue; + } + + if (startsWithOrContains(item.searchName, normalizedQuery, + Qt::CaseInsensitive, + getSettings()->prefixOnlyEmoteCompletion)) + { + emotes.insert(item); + } + } + + output.reserve(emotes.size()); + output.assign(emotes.begin(), emotes.end()); +} + +} // namespace chatterino::completion diff --git a/src/controllers/completion/strategies/ClassicEmoteStrategy.hpp b/src/controllers/completion/strategies/ClassicEmoteStrategy.hpp new file mode 100644 index 00000000000..d231c8ac142 --- /dev/null +++ b/src/controllers/completion/strategies/ClassicEmoteStrategy.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "controllers/completion/sources/EmoteSource.hpp" +#include "controllers/completion/strategies/Strategy.hpp" + +namespace chatterino::completion { + +class ClassicEmoteStrategy : public Strategy +{ + void apply(const std::vector &items, + std::vector &output, + const QString &query) const override; +}; + +class ClassicTabEmoteStrategy : public Strategy +{ + void apply(const std::vector &items, + std::vector &output, + const QString &query) const override; +}; + +} // namespace chatterino::completion From 7829d2e92d651bdf1ae264f43cc5bad9d3a88e02 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Mon, 27 Nov 2023 23:24:48 +0100 Subject: [PATCH 05/10] Allow the user to pick between old and new strategies --- src/CMakeLists.txt | 2 +- src/controllers/completion/TabCompletionModel.cpp | 11 ++++++++++- src/singletons/Settings.hpp | 2 ++ src/widgets/settingspages/GeneralPage.cpp | 2 ++ src/widgets/splits/InputCompletionPopup.cpp | 11 ++++++++++- 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7aeccfc089e..86c24c15b40 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -123,7 +123,7 @@ set(SOURCE_FILES controllers/completion/sources/UnifiedSource.hpp controllers/completion/sources/UserSource.cpp controllers/completion/sources/UserSource.hpp - controllers/completion/strategies/ClassicEmoteStrategy.hpp + controllers/completion/strategies/ClassicEmoteStrategy.cpp controllers/completion/strategies/ClassicEmoteStrategy.hpp controllers/completion/strategies/ClassicUserStrategy.cpp controllers/completion/strategies/ClassicUserStrategy.hpp diff --git a/src/controllers/completion/TabCompletionModel.cpp b/src/controllers/completion/TabCompletionModel.cpp index a2be57962e1..74e82652900 100644 --- a/src/controllers/completion/TabCompletionModel.cpp +++ b/src/controllers/completion/TabCompletionModel.cpp @@ -5,6 +5,7 @@ #include "controllers/completion/sources/EmoteSource.hpp" #include "controllers/completion/sources/UnifiedSource.hpp" #include "controllers/completion/sources/UserSource.hpp" +#include "controllers/completion/strategies/ClassicEmoteStrategy.hpp" #include "controllers/completion/strategies/ClassicUserStrategy.hpp" #include "controllers/completion/strategies/CommandStrategy.hpp" #include "controllers/completion/strategies/SmartEmoteStrategy.hpp" @@ -123,8 +124,16 @@ std::unique_ptr TabCompletionModel::buildSource( std::unique_ptr TabCompletionModel::buildEmoteSource() const { + if (getSettings()->useSmartEmoteCompletion) + { + return std::make_unique( + &this->channel_, + std::make_unique()); + } + return std::make_unique( - &this->channel_, std::make_unique()); + &this->channel_, + std::make_unique()); } std::unique_ptr TabCompletionModel::buildUserSource( diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 40d7be51e8e..ad9dadf7533 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -218,6 +218,8 @@ class Settings "/behaviour/autocompletion/emoteCompletionWithColon", true}; BoolSetting showUsernameCompletionMenu = { "/behaviour/autocompletion/showUsernameCompletionMenu", true}; + BoolSetting useSmartEmoteCompletion = { + "/behaviour/autocompletion/useSmartEmoteCompletion", false}; FloatSetting pauseOnHoverDuration = {"/behaviour/pauseOnHoverDuration", 0}; EnumSetting pauseChatModifier = { diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 48ecdb486fd..91761396115 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -484,6 +484,8 @@ void GeneralPage::initLayout(GeneralPageView &layout) "cvMask and 7TV's RainTime, will appear as normal emotes."); layout.addCheckbox("Enable emote auto-completion by typing :", s.emoteCompletionWithColon); + layout.addCheckbox("Use smarter tab completion.", + s.useSmartEmoteCompletion); layout.addDropdown( "Size", {"0.5x", "0.75x", "Default", "1.25x", "1.5x", "2x"}, s.emoteScale, diff --git a/src/widgets/splits/InputCompletionPopup.cpp b/src/widgets/splits/InputCompletionPopup.cpp index bd26ff1e6ff..c103774db82 100644 --- a/src/widgets/splits/InputCompletionPopup.cpp +++ b/src/widgets/splits/InputCompletionPopup.cpp @@ -1,8 +1,10 @@ #include "widgets/splits/InputCompletionPopup.hpp" #include "controllers/completion/sources/UserSource.hpp" +#include "controllers/completion/strategies/ClassicEmoteStrategy.hpp" #include "controllers/completion/strategies/ClassicUserStrategy.hpp" #include "controllers/completion/strategies/SmartEmoteStrategy.hpp" +#include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "util/LayoutCreator.hpp" #include "widgets/splits/InputCompletionItem.hpp" @@ -60,9 +62,16 @@ std::unique_ptr InputCompletionPopup::getSource() const switch (*this->currentKind_) { case CompletionKind::Emote: + if (getSettings()->useSmartEmoteCompletion) + { + return std::make_unique( + this->currentChannel_.get(), + std::make_unique(), + this->callback_); + } return std::make_unique( this->currentChannel_.get(), - std::make_unique(), + std::make_unique(), this->callback_); case CompletionKind::User: return std::make_unique( From 7da7d027310bb443a9c8c9b2a8c892fcc3c4bdde Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Tue, 28 Nov 2023 00:38:20 +0100 Subject: [PATCH 06/10] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57fd2f21619..845d117b446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Minor: Add menu actions to reply directly to a message or the original thread root. (#4923) - Minor: The `/reply` command now replies to the latest message of the user. (#4919) - Minor: All sound capabilities can now be disabled by setting your "Sound backend" setting to "Null" and restarting Chatterino. (#4978) +- Minor: Add an option to use new experimental smarter emote completion. (#4987) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) - Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834) From bf60f7d97f0cc14f37fa4ffa54adafe0209b6d21 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Tue, 28 Nov 2023 00:39:08 +0100 Subject: [PATCH 07/10] Make UI say that this feature is experimental --- src/widgets/settingspages/GeneralPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 91761396115..d2b5c990892 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -484,7 +484,7 @@ void GeneralPage::initLayout(GeneralPageView &layout) "cvMask and 7TV's RainTime, will appear as normal emotes."); layout.addCheckbox("Enable emote auto-completion by typing :", s.emoteCompletionWithColon); - layout.addCheckbox("Use smarter tab completion.", + layout.addCheckbox("Use experimental smarter emote completion.", s.useSmartEmoteCompletion); layout.addDropdown( "Size", {"0.5x", "0.75x", "Default", "1.25x", "1.5x", "2x"}, From 958e6ea6f21d1a6d3f388b3fd339a26ca932f6c0 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Tue, 28 Nov 2023 00:57:35 +0100 Subject: [PATCH 08/10] Remove inaccurate part of comment --- src/controllers/completion/strategies/SmartEmoteStrategy.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/completion/strategies/SmartEmoteStrategy.cpp b/src/controllers/completion/strategies/SmartEmoteStrategy.cpp index 2923108a667..03d467447df 100644 --- a/src/controllers/completion/strategies/SmartEmoteStrategy.cpp +++ b/src/controllers/completion/strategies/SmartEmoteStrategy.cpp @@ -23,7 +23,7 @@ namespace { * will get. Additional letters also increase the cost in this mode. * * @return How different the emote is from query. Values in the range [-10, - * \infty]. Negative cost means exact match. + * \infty]. */ int costOfEmote(const QString &query, const QString &emote, bool prioritizeUpper) From a810474eca3953a21d1f629f083a7a4f4bf989f1 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Tue, 28 Nov 2023 01:46:28 +0100 Subject: [PATCH 09/10] Make the SmartEmoteStrategy account for emotes starting with :, they shouldn't have additional cost --- .../strategies/SmartEmoteStrategy.cpp | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/controllers/completion/strategies/SmartEmoteStrategy.cpp b/src/controllers/completion/strategies/SmartEmoteStrategy.cpp index 03d467447df..aa6e43127ef 100644 --- a/src/controllers/completion/strategies/SmartEmoteStrategy.cpp +++ b/src/controllers/completion/strategies/SmartEmoteStrategy.cpp @@ -68,7 +68,7 @@ namespace { // matchingFunction is used for testing if the emote should be included in the search. void completeEmotes( const std::vector &items, std::vector &output, - const QString &query, + const QString &query, bool ignoreColonForCost, const std::function &matchingFunction) { @@ -130,21 +130,31 @@ namespace { } } - std::sort( - output.begin(), output.end(), - [query, prioritizeUpper](const EmoteItem &a, - const EmoteItem &b) -> bool { - auto costA = costOfEmote(query, a.searchName, prioritizeUpper); - auto costB = costOfEmote(query, b.searchName, prioritizeUpper); - if (costA == costB) - { - // Case difference and length came up tied for (a, b), break the tie - return QString::compare(a.searchName, b.searchName, - Qt::CaseInsensitive) < 0; - } - - return costA < costB; - }); + std::sort(output.begin(), output.end(), + [query, prioritizeUpper, ignoreColonForCost]( + const EmoteItem &a, const EmoteItem &b) -> bool { + auto tempA = a.searchName; + auto tempB = b.searchName; + if (ignoreColonForCost && tempA.startsWith(":")) + { + tempA = tempA.mid(1); + } + if (ignoreColonForCost && tempB.startsWith(":")) + { + tempB = tempB.mid(1); + } + + auto costA = costOfEmote(query, tempA, prioritizeUpper); + auto costB = costOfEmote(query, tempB, prioritizeUpper); + if (costA == costB) + { + // Case difference and length came up tied for (a, b), break the tie + return QString::compare(tempA, tempB, + Qt::CaseInsensitive) < 0; + } + + return costA < costB; + }); } } // namespace @@ -153,11 +163,13 @@ void SmartEmoteStrategy::apply(const std::vector &items, const QString &query) const { QString normalizedQuery = query; + bool ignoreColonForCost = false; if (normalizedQuery.startsWith(':')) { normalizedQuery = normalizedQuery.mid(1); + ignoreColonForCost = true; } - completeEmotes(items, output, normalizedQuery, + completeEmotes(items, output, normalizedQuery, ignoreColonForCost, [](const EmoteItem &left, const QString &right, Qt::CaseSensitivity caseHandling) { return left.searchName.contains(right, caseHandling); @@ -176,7 +188,7 @@ void SmartTabEmoteStrategy::apply(const std::vector &items, // tab completion with : prefix should do emojis only emojiOnly = true; } - completeEmotes(items, output, normalizedQuery, + completeEmotes(items, output, normalizedQuery, false, [emojiOnly](const EmoteItem &left, const QString &right, Qt::CaseSensitivity caseHandling) -> bool { if (emojiOnly ^ left.isEmoji) From 523580959c6321532e9b2e3c79ed7f15ed87476d Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Tue, 28 Nov 2023 10:46:28 +0100 Subject: [PATCH 10/10] Change setting path to lie under `/experiments/useSmartEmoteCompletion` This setting is not expected to stay in its current form after we've tried it out. --- src/singletons/Settings.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index ad9dadf7533..8e650de873d 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -219,7 +219,9 @@ class Settings BoolSetting showUsernameCompletionMenu = { "/behaviour/autocompletion/showUsernameCompletionMenu", true}; BoolSetting useSmartEmoteCompletion = { - "/behaviour/autocompletion/useSmartEmoteCompletion", false}; + "/experiments/useSmartEmoteCompletion", + false, + }; FloatSetting pauseOnHoverDuration = {"/behaviour/pauseOnHoverDuration", 0}; EnumSetting pauseChatModifier = {