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:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_`.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user