mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Make emote completion a lot smarter (#4987)
This commit is contained in:
parent
a240797b68
commit
6d02bb7304
|
@ -12,6 +12,7 @@
|
||||||
- Minor: Add menu actions to reply directly to a message or the original thread root. (#4923)
|
- 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: 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: 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 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: 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)
|
- 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)
|
||||||
|
|
|
@ -123,13 +123,15 @@ set(SOURCE_FILES
|
||||||
controllers/completion/sources/UnifiedSource.hpp
|
controllers/completion/sources/UnifiedSource.hpp
|
||||||
controllers/completion/sources/UserSource.cpp
|
controllers/completion/sources/UserSource.cpp
|
||||||
controllers/completion/sources/UserSource.hpp
|
controllers/completion/sources/UserSource.hpp
|
||||||
controllers/completion/strategies/Strategy.hpp
|
|
||||||
controllers/completion/strategies/ClassicEmoteStrategy.cpp
|
controllers/completion/strategies/ClassicEmoteStrategy.cpp
|
||||||
controllers/completion/strategies/ClassicEmoteStrategy.hpp
|
controllers/completion/strategies/ClassicEmoteStrategy.hpp
|
||||||
controllers/completion/strategies/ClassicUserStrategy.cpp
|
controllers/completion/strategies/ClassicUserStrategy.cpp
|
||||||
controllers/completion/strategies/ClassicUserStrategy.hpp
|
controllers/completion/strategies/ClassicUserStrategy.hpp
|
||||||
controllers/completion/strategies/CommandStrategy.cpp
|
controllers/completion/strategies/CommandStrategy.cpp
|
||||||
controllers/completion/strategies/CommandStrategy.hpp
|
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.cpp
|
||||||
controllers/completion/TabCompletionModel.hpp
|
controllers/completion/TabCompletionModel.hpp
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include "controllers/completion/strategies/ClassicEmoteStrategy.hpp"
|
#include "controllers/completion/strategies/ClassicEmoteStrategy.hpp"
|
||||||
#include "controllers/completion/strategies/ClassicUserStrategy.hpp"
|
#include "controllers/completion/strategies/ClassicUserStrategy.hpp"
|
||||||
#include "controllers/completion/strategies/CommandStrategy.hpp"
|
#include "controllers/completion/strategies/CommandStrategy.hpp"
|
||||||
|
#include "controllers/completion/strategies/SmartEmoteStrategy.hpp"
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
@ -123,6 +124,13 @@ std::unique_ptr<completion::Source> TabCompletionModel::buildSource(
|
||||||
|
|
||||||
std::unique_ptr<completion::Source> TabCompletionModel::buildEmoteSource() const
|
std::unique_ptr<completion::Source> TabCompletionModel::buildEmoteSource() const
|
||||||
{
|
{
|
||||||
|
if (getSettings()->useSmartEmoteCompletion)
|
||||||
|
{
|
||||||
|
return std::make_unique<completion::EmoteSource>(
|
||||||
|
&this->channel_,
|
||||||
|
std::make_unique<completion::SmartTabEmoteStrategy>());
|
||||||
|
}
|
||||||
|
|
||||||
return std::make_unique<completion::EmoteSource>(
|
return std::make_unique<completion::EmoteSource>(
|
||||||
&this->channel_,
|
&this->channel_,
|
||||||
std::make_unique<completion::ClassicTabEmoteStrategy>());
|
std::make_unique<completion::ClassicTabEmoteStrategy>());
|
||||||
|
|
204
src/controllers/completion/strategies/SmartEmoteStrategy.cpp
Normal file
204
src/controllers/completion/strategies/SmartEmoteStrategy.cpp
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
#include "controllers/completion/strategies/SmartEmoteStrategy.hpp"
|
||||||
|
|
||||||
|
#include "common/QLogging.hpp"
|
||||||
|
#include "controllers/completion/sources/EmoteSource.hpp"
|
||||||
|
#include "singletons/Settings.hpp"
|
||||||
|
#include "util/Helpers.hpp"
|
||||||
|
|
||||||
|
#include <Qt>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
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].
|
||||||
|
*/
|
||||||
|
int costOfEmote(const QString &query, const QString &emote,
|
||||||
|
bool prioritizeUpper)
|
||||||
|
{
|
||||||
|
int score = 0;
|
||||||
|
|
||||||
|
if (prioritizeUpper)
|
||||||
|
{
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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<EmoteItem> &items, std::vector<EmoteItem> &output,
|
||||||
|
const QString &query, bool ignoreColonForCost,
|
||||||
|
const std::function<bool(EmoteItem, QString, Qt::CaseSensitivity)>
|
||||||
|
&matchingFunction)
|
||||||
|
{
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
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, 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
|
||||||
|
|
||||||
|
void SmartEmoteStrategy::apply(const std::vector<EmoteItem> &items,
|
||||||
|
std::vector<EmoteItem> &output,
|
||||||
|
const QString &query) const
|
||||||
|
{
|
||||||
|
QString normalizedQuery = query;
|
||||||
|
bool ignoreColonForCost = false;
|
||||||
|
if (normalizedQuery.startsWith(':'))
|
||||||
|
{
|
||||||
|
normalizedQuery = normalizedQuery.mid(1);
|
||||||
|
ignoreColonForCost = true;
|
||||||
|
}
|
||||||
|
completeEmotes(items, output, normalizedQuery, ignoreColonForCost,
|
||||||
|
[](const EmoteItem &left, const QString &right,
|
||||||
|
Qt::CaseSensitivity caseHandling) {
|
||||||
|
return left.searchName.contains(right, caseHandling);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SmartTabEmoteStrategy::apply(const std::vector<EmoteItem> &items,
|
||||||
|
std::vector<EmoteItem> &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;
|
||||||
|
}
|
||||||
|
completeEmotes(items, output, normalizedQuery, false,
|
||||||
|
[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
|
22
src/controllers/completion/strategies/SmartEmoteStrategy.hpp
Normal file
22
src/controllers/completion/strategies/SmartEmoteStrategy.hpp
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "controllers/completion/sources/EmoteSource.hpp"
|
||||||
|
#include "controllers/completion/strategies/Strategy.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::completion {
|
||||||
|
|
||||||
|
class SmartEmoteStrategy : public Strategy<EmoteItem>
|
||||||
|
{
|
||||||
|
void apply(const std::vector<EmoteItem> &items,
|
||||||
|
std::vector<EmoteItem> &output,
|
||||||
|
const QString &query) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SmartTabEmoteStrategy : public Strategy<EmoteItem>
|
||||||
|
{
|
||||||
|
void apply(const std::vector<EmoteItem> &items,
|
||||||
|
std::vector<EmoteItem> &output,
|
||||||
|
const QString &query) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino::completion
|
|
@ -218,6 +218,10 @@ public:
|
||||||
"/behaviour/autocompletion/emoteCompletionWithColon", true};
|
"/behaviour/autocompletion/emoteCompletionWithColon", true};
|
||||||
BoolSetting showUsernameCompletionMenu = {
|
BoolSetting showUsernameCompletionMenu = {
|
||||||
"/behaviour/autocompletion/showUsernameCompletionMenu", true};
|
"/behaviour/autocompletion/showUsernameCompletionMenu", true};
|
||||||
|
BoolSetting useSmartEmoteCompletion = {
|
||||||
|
"/experiments/useSmartEmoteCompletion",
|
||||||
|
false,
|
||||||
|
};
|
||||||
|
|
||||||
FloatSetting pauseOnHoverDuration = {"/behaviour/pauseOnHoverDuration", 0};
|
FloatSetting pauseOnHoverDuration = {"/behaviour/pauseOnHoverDuration", 0};
|
||||||
EnumSetting<Qt::KeyboardModifier> pauseChatModifier = {
|
EnumSetting<Qt::KeyboardModifier> pauseChatModifier = {
|
||||||
|
|
|
@ -484,6 +484,8 @@ void GeneralPage::initLayout(GeneralPageView &layout)
|
||||||
"cvMask and 7TV's RainTime, will appear as normal emotes.");
|
"cvMask and 7TV's RainTime, will appear as normal emotes.");
|
||||||
layout.addCheckbox("Enable emote auto-completion by typing :",
|
layout.addCheckbox("Enable emote auto-completion by typing :",
|
||||||
s.emoteCompletionWithColon);
|
s.emoteCompletionWithColon);
|
||||||
|
layout.addCheckbox("Use experimental smarter emote completion.",
|
||||||
|
s.useSmartEmoteCompletion);
|
||||||
layout.addDropdown<float>(
|
layout.addDropdown<float>(
|
||||||
"Size", {"0.5x", "0.75x", "Default", "1.25x", "1.5x", "2x"},
|
"Size", {"0.5x", "0.75x", "Default", "1.25x", "1.5x", "2x"},
|
||||||
s.emoteScale,
|
s.emoteScale,
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
#include "controllers/completion/sources/UserSource.hpp"
|
#include "controllers/completion/sources/UserSource.hpp"
|
||||||
#include "controllers/completion/strategies/ClassicEmoteStrategy.hpp"
|
#include "controllers/completion/strategies/ClassicEmoteStrategy.hpp"
|
||||||
#include "controllers/completion/strategies/ClassicUserStrategy.hpp"
|
#include "controllers/completion/strategies/ClassicUserStrategy.hpp"
|
||||||
|
#include "controllers/completion/strategies/SmartEmoteStrategy.hpp"
|
||||||
|
#include "singletons/Settings.hpp"
|
||||||
#include "singletons/Theme.hpp"
|
#include "singletons/Theme.hpp"
|
||||||
#include "util/LayoutCreator.hpp"
|
#include "util/LayoutCreator.hpp"
|
||||||
#include "widgets/splits/InputCompletionItem.hpp"
|
#include "widgets/splits/InputCompletionItem.hpp"
|
||||||
|
@ -60,6 +62,13 @@ std::unique_ptr<completion::Source> InputCompletionPopup::getSource() const
|
||||||
switch (*this->currentKind_)
|
switch (*this->currentKind_)
|
||||||
{
|
{
|
||||||
case CompletionKind::Emote:
|
case CompletionKind::Emote:
|
||||||
|
if (getSettings()->useSmartEmoteCompletion)
|
||||||
|
{
|
||||||
|
return std::make_unique<completion::EmoteSource>(
|
||||||
|
this->currentChannel_.get(),
|
||||||
|
std::make_unique<completion::SmartEmoteStrategy>(),
|
||||||
|
this->callback_);
|
||||||
|
}
|
||||||
return std::make_unique<completion::EmoteSource>(
|
return std::make_unique<completion::EmoteSource>(
|
||||||
this->currentChannel_.get(),
|
this->currentChannel_.get(),
|
||||||
std::make_unique<completion::ClassicEmoteStrategy>(),
|
std::make_unique<completion::ClassicEmoteStrategy>(),
|
||||||
|
|
Loading…
Reference in a new issue