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: 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)
|
||||
|
|
|
@ -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/ClassicEmoteStrategy.cpp
|
||||
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
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#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 {
|
||||
|
@ -123,6 +124,13 @@ std::unique_ptr<completion::Source> TabCompletionModel::buildSource(
|
|||
|
||||
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>(
|
||||
&this->channel_,
|
||||
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};
|
||||
BoolSetting showUsernameCompletionMenu = {
|
||||
"/behaviour/autocompletion/showUsernameCompletionMenu", true};
|
||||
BoolSetting useSmartEmoteCompletion = {
|
||||
"/experiments/useSmartEmoteCompletion",
|
||||
false,
|
||||
};
|
||||
|
||||
FloatSetting pauseOnHoverDuration = {"/behaviour/pauseOnHoverDuration", 0};
|
||||
EnumSetting<Qt::KeyboardModifier> pauseChatModifier = {
|
||||
|
|
|
@ -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 experimental smarter emote completion.",
|
||||
s.useSmartEmoteCompletion);
|
||||
layout.addDropdown<float>(
|
||||
"Size", {"0.5x", "0.75x", "Default", "1.25x", "1.5x", "2x"},
|
||||
s.emoteScale,
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#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,6 +62,13 @@ std::unique_ptr<completion::Source> InputCompletionPopup::getSource() const
|
|||
switch (*this->currentKind_)
|
||||
{
|
||||
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>(
|
||||
this->currentChannel_.get(),
|
||||
std::make_unique<completion::ClassicEmoteStrategy>(),
|
||||
|
|
Loading…
Reference in a new issue