Consolidate input completion code in preparation for advanced completion strategies (#4639)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
Daniel Sage
2023-09-24 08:17:17 -04:00
committed by GitHub
parent 06eb30a50a
commit 37009e8e6b
39 changed files with 1358 additions and 621 deletions
+2 -3
View File
@@ -1,7 +1,6 @@
#include "EmotePopup.hpp"
#include "Application.hpp"
#include "common/CompletionModel.hpp"
#include "common/QLogging.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/hotkeys/HotkeyController.hpp"
@@ -16,6 +15,7 @@
#include "singletons/Emotes.hpp"
#include "singletons/Settings.hpp"
#include "singletons/WindowManager.hpp"
#include "util/Helpers.hpp"
#include "widgets/helper/ChannelView.hpp"
#include "widgets/helper/TrimRegExpValidator.hpp"
#include "widgets/Notebook.hpp"
@@ -58,8 +58,7 @@ auto makeEmoteMessage(const EmoteMap &map, const MessageElementFlag &emoteFlag)
std::sort(vec.begin(), vec.end(),
[](const std::pair<EmoteName, EmotePtr> &l,
const std::pair<EmoteName, EmotePtr> &r) {
return CompletionModel::compareStrings(l.first.string,
r.first.string);
return compareEmoteStrings(l.first.string, r.first.string);
});
for (const auto &emote : vec)
{
+6 -9
View File
@@ -1,8 +1,8 @@
#include "widgets/helper/ResizingTextEdit.hpp"
#include "common/Common.hpp"
#include "common/CompletionModel.hpp"
#include "common/QLogging.hpp"
#include "controllers/completion/TabCompletionModel.hpp"
#include "singletons/Settings.hpp"
#include <QMimeData>
@@ -160,16 +160,18 @@ void ResizingTextEdit::keyPressEvent(QKeyEvent *event)
return;
}
// always expected to be TabCompletionModel
auto *completionModel =
static_cast<CompletionModel *>(this->completer_->model());
dynamic_cast<TabCompletionModel *>(this->completer_->model());
assert(completionModel != nullptr);
if (!this->completionInProgress_)
{
// First type pressing tab after modifying a message, we refresh our
// completion model
this->completer_->setModel(completionModel);
completionModel->refresh(currentCompletionPrefix,
this->isFirstWord());
completionModel->updateResults(currentCompletionPrefix,
this->isFirstWord());
this->completionInProgress_ = true;
{
// this blocks cursor movement events from resetting tab completion
@@ -346,9 +348,4 @@ void ResizingTextEdit::insertFromMimeData(const QMimeData *source)
insertPlainText(source->text());
}
QCompleter *ResizingTextEdit::getCompleter() const
{
return this->completer_;
}
} // namespace chatterino
-1
View File
@@ -23,7 +23,6 @@ public:
pajlada::Signals::Signal<const QMimeData *> imagePasted;
void setCompleter(QCompleter *c);
QCompleter *getCompleter() const;
/**
* Resets a completion for this text if one was is progress.
* See `completionInProgress_`.
+48 -167
View File
@@ -1,138 +1,12 @@
#include "InputCompletionPopup.hpp"
#include "widgets/splits/InputCompletionPopup.hpp"
#include "Application.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "messages/Emote.hpp"
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/ffz/FfzEmotes.hpp"
#include "providers/seventv/SeventvEmotes.hpp"
#include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Emotes.hpp"
#include "controllers/completion/sources/UserSource.hpp"
#include "controllers/completion/strategies/ClassicEmoteStrategy.hpp"
#include "controllers/completion/strategies/ClassicUserStrategy.hpp"
#include "singletons/Theme.hpp"
#include "util/LayoutCreator.hpp"
#include "widgets/listview/GenericListView.hpp"
#include "widgets/splits/InputCompletionItem.hpp"
namespace {
using namespace chatterino;
using namespace chatterino::detail;
void addEmotes(std::vector<CompletionEmote> &out, const EmoteMap &map,
const QString &text, const QString &providerName)
{
for (auto &&emote : map)
{
if (emote.first.string.contains(text, Qt::CaseInsensitive))
{
out.push_back(
{emote.second, emote.second->name.string, providerName});
}
}
}
void addEmojis(std::vector<CompletionEmote> &out, const EmojiMap &map,
const QString &text)
{
map.each([&](const QString &, const std::shared_ptr<EmojiData> &emoji) {
for (auto &&shortCode : emoji->shortCodes)
{
if (shortCode.contains(text, Qt::CaseInsensitive))
{
out.push_back({emoji->emote, shortCode, "Emoji"});
}
}
});
}
} // namespace
namespace chatterino::detail {
std::vector<CompletionEmote> buildCompletionEmoteList(const QString &text,
ChannelPtr channel)
{
std::vector<CompletionEmote> emotes;
auto *app = getIApp();
auto *tc = dynamic_cast<TwitchChannel *>(channel.get());
// returns true also for special Twitch channels (/live, /mentions, /whispers, etc.)
if (channel->isTwitchChannel())
{
if (auto user = app->getAccounts()->twitch.getCurrent())
{
// Twitch Emotes available globally
auto emoteData = user->accessEmotes();
addEmotes(emotes, emoteData->emotes, text, "Twitch Emote");
// Twitch Emotes available locally
auto localEmoteData = user->accessLocalEmotes();
if (tc &&
localEmoteData->find(tc->roomId()) != localEmoteData->end())
{
if (const auto *localEmotes = &localEmoteData->at(tc->roomId()))
{
addEmotes(emotes, *localEmotes, text,
"Local Twitch Emotes");
}
}
}
if (tc)
{
// TODO extract "Channel {BetterTTV,7TV,FrankerFaceZ}" text into a #define.
if (auto bttv = tc->bttvEmotes())
{
addEmotes(emotes, *bttv, text, "Channel BetterTTV");
}
if (auto ffz = tc->ffzEmotes())
{
addEmotes(emotes, *ffz, text, "Channel FrankerFaceZ");
}
if (auto seventv = tc->seventvEmotes())
{
addEmotes(emotes, *seventv, text, "Channel 7TV");
}
}
if (auto bttvG = app->getTwitch()->getBttvEmotes().emotes())
{
addEmotes(emotes, *bttvG, text, "Global BetterTTV");
}
if (auto ffzG = app->getTwitch()->getFfzEmotes().emotes())
{
addEmotes(emotes, *ffzG, text, "Global FrankerFaceZ");
}
if (auto seventvG = app->getTwitch()->getSeventvEmotes().globalEmotes())
{
addEmotes(emotes, *seventvG, text, "Global 7TV");
}
}
addEmojis(emotes, app->getEmotes()->getEmojis()->getEmojis(), text);
// if there is an exact match, put that emote first
for (size_t i = 1; i < emotes.size(); i++)
{
auto emoteText = emotes.at(i).displayName;
// test for match or match with colon at start for emotes like ":)"
if (emoteText.compare(text, Qt::CaseInsensitive) == 0 ||
emoteText.compare(":" + text, Qt::CaseInsensitive) == 0)
{
auto emote = emotes[i];
emotes.erase(emotes.begin() + int(i));
emotes.insert(emotes.begin(), emote);
break;
}
}
return emotes;
}
} // namespace chatterino::detail
namespace chatterino {
InputCompletionPopup::InputCompletionPopup(QWidget *parent)
@@ -153,60 +27,66 @@ InputCompletionPopup::InputCompletionPopup(QWidget *parent)
this->redrawTimer_.setInterval(33);
}
void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel)
void InputCompletionPopup::updateCompletion(const QString &text,
CompletionKind kind,
ChannelPtr channel)
{
auto emotes = detail::buildCompletionEmoteList(text, std::move(channel));
this->model_.clear();
int count = 0;
for (auto &&emote : emotes)
if (this->currentKind_ != kind || this->currentChannel_ != channel)
{
this->model_.addItem(std::make_unique<InputCompletionItem>(
emote.emote, emote.displayName + " - " + emote.providerName,
this->callback_));
if (count++ == MAX_ENTRY_COUNT)
{
break;
}
// New completion context
this->beginCompletion(kind, std::move(channel));
}
if (!emotes.empty())
assert(this->model_.hasSource());
this->model_.updateResults(text, MAX_ENTRY_COUNT);
// Move selection to top row
if (this->model_.rowCount() != 0)
{
this->ui_.listView->setCurrentIndex(this->model_.index(0));
}
}
void InputCompletionPopup::updateUsers(const QString &text, ChannelPtr channel)
std::unique_ptr<completion::Source> InputCompletionPopup::getSource() const
{
auto *tc = dynamic_cast<TwitchChannel *>(channel.get());
if (!tc)
assert(this->currentChannel_ != nullptr);
if (!this->currentKind_)
{
return;
return nullptr;
}
auto chatters = tc->accessChatters()->filterByPrefix(text);
this->model_.clear();
if (chatters.empty())
// Currently, strategies are hard coded.
switch (*this->currentKind_)
{
return;
case CompletionKind::Emote:
return std::make_unique<completion::EmoteSource>(
this->currentChannel_.get(),
std::make_unique<completion::ClassicEmoteStrategy>(),
this->callback_);
case CompletionKind::User:
return std::make_unique<completion::UserSource>(
this->currentChannel_.get(),
std::make_unique<completion::ClassicUserStrategy>(),
this->callback_);
default:
return nullptr;
}
}
int count = 0;
for (const auto &name : chatters)
{
this->model_.addItem(std::make_unique<InputCompletionItem>(
nullptr, name, this->callback_));
void InputCompletionPopup::beginCompletion(CompletionKind kind,
ChannelPtr channel)
{
this->currentKind_ = kind;
this->currentChannel_ = std::move(channel);
this->model_.setSource(this->getSource());
}
if (count++ == MAX_ENTRY_COUNT)
{
break;
}
}
this->ui_.listView->setCurrentIndex(this->model_.index(0));
void InputCompletionPopup::endCompletion()
{
this->currentKind_ = std::nullopt;
this->currentChannel_ = nullptr;
this->model_.setSource(nullptr);
}
void InputCompletionPopup::setInputAction(ActionCallback callback)
@@ -227,6 +107,7 @@ void InputCompletionPopup::showEvent(QShowEvent * /*event*/)
void InputCompletionPopup::hideEvent(QHideEvent * /*event*/)
{
this->redrawTimer_.stop();
this->endCompletion();
}
void InputCompletionPopup::themeChangedEvent()
+15 -21
View File
@@ -1,10 +1,13 @@
#pragma once
#include "controllers/completion/CompletionModel.hpp"
#include "controllers/completion/sources/Source.hpp"
#include "widgets/BasePopup.hpp"
#include "widgets/listview/GenericListModel.hpp"
#include "widgets/listview/GenericListView.hpp"
#include <functional>
#include <memory>
#include <optional>
#include <vector>
namespace chatterino {
@@ -12,35 +15,19 @@ namespace chatterino {
class Channel;
using ChannelPtr = std::shared_ptr<Channel>;
struct Emote;
using EmotePtr = std::shared_ptr<const Emote>;
namespace detail {
struct CompletionEmote {
EmotePtr emote;
QString displayName;
QString providerName;
};
std::vector<CompletionEmote> buildCompletionEmoteList(const QString &text,
ChannelPtr channel);
} // namespace detail
class GenericListView;
class InputCompletionPopup : public BasePopup
{
using ActionCallback = std::function<void(const QString &)>;
constexpr static int MAX_ENTRY_COUNT = 200;
constexpr static size_t MAX_ENTRY_COUNT = 200;
public:
InputCompletionPopup(QWidget *parent = nullptr);
void updateEmotes(const QString &text, ChannelPtr channel);
void updateUsers(const QString &text, ChannelPtr channel);
void updateCompletion(const QString &text, CompletionKind kind,
ChannelPtr channel);
void setInputAction(ActionCallback callback);
@@ -54,14 +41,21 @@ protected:
private:
void initLayout();
void beginCompletion(CompletionKind kind, ChannelPtr channel);
void endCompletion();
std::unique_ptr<completion::Source> getSource() const;
struct {
GenericListView *listView;
} ui_{};
GenericListModel model_;
CompletionModel model_;
ActionCallback callback_;
QTimer redrawTimer_;
std::optional<CompletionKind> currentKind_{};
ChannelPtr currentChannel_{};
};
} // namespace chatterino
+7 -14
View File
@@ -51,7 +51,7 @@ SplitInput::SplitInput(QWidget *parent, Split *_chatWidget,
this->initLayout();
auto completer =
new QCompleter(&this->split_->getChannel().get()->completionModel);
new QCompleter(&this->split_->getChannel()->completionModel);
this->ui_.textEdit->setCompleter(completer);
this->signalHolder_.managedConnect(this->split_->channelChanged, [this] {
@@ -746,8 +746,8 @@ void SplitInput::updateCompletionPopup()
{
if (i == 0 || text[i - 1].isSpace())
{
this->showCompletionPopup(text.mid(i, position - i + 1).mid(1),
true);
this->showCompletionPopup(text.mid(i, position - i + 1),
CompletionKind::Emote);
}
else
{
@@ -760,8 +760,8 @@ void SplitInput::updateCompletionPopup()
{
if (i == 0 || text[i - 1].isSpace())
{
this->showCompletionPopup(text.mid(i, position - i + 1).mid(1),
false);
this->showCompletionPopup(text.mid(i, position - i + 1),
CompletionKind::User);
}
else
{
@@ -774,7 +774,7 @@ void SplitInput::updateCompletionPopup()
this->hideCompletionPopup();
}
void SplitInput::showCompletionPopup(const QString &text, bool emoteCompletion)
void SplitInput::showCompletionPopup(const QString &text, CompletionKind kind)
{
if (this->inputCompletionPopup_.isNull())
{
@@ -792,14 +792,7 @@ void SplitInput::showCompletionPopup(const QString &text, bool emoteCompletion)
auto *popup = this->inputCompletionPopup_.data();
assert(popup);
if (emoteCompletion)
{
popup->updateEmotes(text, this->split_->getChannel());
}
else
{
popup->updateUsers(text, this->split_->getChannel());
}
popup->updateCompletion(text, kind, this->split_->getChannel());
auto pos = this->mapToGlobal(QPoint{0, 0}) - QPoint(0, popup->height()) +
QPoint((this->width() - popup->width()) / 2, 0);
+2 -1
View File
@@ -22,6 +22,7 @@ class EffectLabel;
class MessageThread;
class ResizingTextEdit;
class ChannelView;
enum class CompletionKind;
class SplitInput : public BaseWidget
{
@@ -99,7 +100,7 @@ protected:
void onTextChanged();
void updateEmoteButton();
void updateCompletionPopup();
void showCompletionPopup(const QString &text, bool emoteCompletion);
void showCompletionPopup(const QString &text, CompletionKind kind);
void hideCompletionPopup();
void insertCompletionText(const QString &input_) const;
void openEmotePopup();