Add search to emote popup (#3404)

Co-authored-by: Paweł <zneix@zneix.eu>
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
Adam Davies 2022-01-02 08:59:16 -06:00 committed by GitHub
parent 2b9e2bd1b0
commit 8e5468c316
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 202 additions and 40 deletions

View file

@ -42,6 +42,7 @@
- Minor: Added autocompletion for default Twitch commands starting with the dot (e.g. `.mods` which does the same as `/mods`). (#3144)
- Minor: Sorted usernames in `Users joined/parted` messages alphabetically. (#3421)
- Minor: Mod list, VIP list, and Users joined/parted messages are now searchable. (#3426)
- Minor: Add search to emote popup. (#3404)
- Minor: Messages can now be highlighted by subscriber or founder badges. (#3445)
- Bugfix: Fix Split Input hotkeys not being available when input is hidden (#3362)
- Bugfix: Fixed colored usernames sometimes not working. (#3170)

View file

@ -16,7 +16,10 @@
#include "widgets/Scrollbar.hpp"
#include "widgets/helper/ChannelView.hpp"
#include <QAbstractButton>
#include <QHBoxLayout>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include <QTabWidget>
namespace chatterino {
@ -62,6 +65,24 @@ namespace {
return builder.release();
}
auto makeEmojiMessage(EmojiMap &emojiMap)
{
MessageBuilder builder;
builder->flags.set(MessageFlag::Centered);
builder->flags.set(MessageFlag::DisableCompactEmotes);
emojiMap.each([&builder](const auto &key, const auto &value) {
builder
.emplace<EmoteElement>(
value->emote,
MessageElementFlags{MessageElementFlag::AlwaysShow,
MessageElementFlag::EmojiAll})
->setLink(Link(Link::Type::InsertText,
":" + value->shortCodes[0] + ":"));
});
return builder.release();
}
void addEmoteSets(
std::vector<std::shared_ptr<TwitchAccount::EmoteSet>> sets,
Channel &globalChannel, Channel &subChannel, QString currentChannelName)
@ -126,6 +147,12 @@ namespace {
}
}
}
void addEmotes(Channel &channel, const EmoteMap &map, const QString &title,
const MessageElementFlag &emoteFlag)
{
channel.addMessage(makeTitleMessage(title));
channel.addMessage(makeEmoteMessage(map, emoteFlag));
};
} // namespace
EmotePopup::EmotePopup(QWidget *parent)
@ -137,40 +164,66 @@ EmotePopup::EmotePopup(QWidget *parent)
auto layout = new QVBoxLayout(this);
this->getLayoutContainer()->setLayout(layout);
this->notebook_ = new Notebook(this);
layout->addWidget(this->notebook_);
layout->setMargin(0);
QRegularExpression searchRegex("\\S*");
searchRegex.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
QValidator *searchValidator = new QRegularExpressionValidator(searchRegex);
this->search_ = new QLineEdit();
this->search_->setPlaceholderText("Search all emotes...");
this->search_->setValidator(searchValidator);
this->search_->setClearButtonEnabled(true);
this->search_->findChild<QAbstractButton *>()->setIcon(
QPixmap(":/buttons/clearSearch.png"));
layout->addWidget(this->search_);
QObject::connect(this->search_, &QLineEdit::textChanged, this,
&EmotePopup::filterEmotes);
auto clicked = [this](const Link &link) {
this->linkClicked.invoke(link);
};
auto makeView = [&](QString tabTitle) {
auto makeView = [&](QString tabTitle, bool addToNotebook = true) {
auto view = new ChannelView();
view->setOverrideFlags(MessageElementFlags{
MessageElementFlag::Default, MessageElementFlag::AlwaysShow,
MessageElementFlag::EmoteImages});
view->setEnableScrollingToBottom(false);
this->notebook_->addPage(view, tabTitle);
view->linkClicked.connect(clicked);
if (addToNotebook)
{
this->notebook_->addPage(view, tabTitle);
}
return view;
};
this->searchView_ = makeView("", false);
this->searchView_->hide();
layout->addWidget(this->searchView_);
this->notebook_ = new Notebook(this);
layout->addWidget(this->notebook_);
layout->setMargin(0);
this->subEmotesView_ = makeView("Subs");
this->channelEmotesView_ = makeView("Channel");
this->globalEmotesView_ = makeView("Global");
this->viewEmojis_ = makeView("Emojis");
this->loadEmojis();
this->loadEmojis(*this->viewEmojis_, getApp()->emotes->emojis.emojis);
this->addShortcuts();
this->signalHolder_.managedConnect(getApp()->hotkeys->onItemsUpdated,
[this]() {
this->clearShortcuts();
this->addShortcuts();
});
this->search_->setFocus();
}
void EmotePopup::addShortcuts()
{
HotkeyController::HotkeyMap actions{
@ -252,29 +305,31 @@ void EmotePopup::addShortcuts()
{"reject", nullptr},
{"accept", nullptr},
{"search", nullptr},
{"search",
[this](std::vector<QString>) -> QString {
this->search_->setFocus();
this->search_->selectAll();
return "";
}},
};
this->shortcuts_ = getApp()->hotkeys->shortcutsForCategory(
HotkeyCategory::PopupWindow, actions, this);
}
void EmotePopup::loadChannel(ChannelPtr _channel)
void EmotePopup::loadChannel(ChannelPtr channel)
{
BenchmarkGuard guard("loadChannel");
this->setWindowTitle("Emotes in #" + _channel->getName());
this->channel_ = channel;
this->twitchChannel_ = dynamic_cast<TwitchChannel *>(this->channel_.get());
auto twitchChannel = dynamic_cast<TwitchChannel *>(_channel.get());
if (twitchChannel == nullptr)
this->setWindowTitle("Emotes in #" + this->channel_->getName());
if (this->twitchChannel_ == nullptr)
{
return;
auto addEmotes = [&](Channel &channel, const EmoteMap &map,
const QString &title,
const MessageElementFlag &emoteFlag) {
channel.addMessage(makeTitleMessage(title));
channel.addMessage(makeEmoteMessage(map, emoteFlag));
};
}
auto subChannel = std::make_shared<Channel>("", Channel::Type::None);
auto globalChannel = std::make_shared<Channel>("", Channel::Type::None);
@ -283,7 +338,7 @@ void EmotePopup::loadChannel(ChannelPtr _channel)
// twitch
addEmoteSets(
getApp()->accounts->twitch.getCurrent()->accessEmotes()->emoteSets,
*globalChannel, *subChannel, _channel->getName());
*globalChannel, *subChannel, this->channel_->getName());
// global
addEmotes(*globalChannel, *getApp()->twitch2->getBttvEmotes().emotes(),
@ -292,10 +347,10 @@ void EmotePopup::loadChannel(ChannelPtr _channel)
"FrankerFaceZ", MessageElementFlag::FfzEmote);
// channel
addEmotes(*channelChannel, *twitchChannel->bttvEmotes(), "BetterTTV",
addEmotes(*channelChannel, *this->twitchChannel_->bttvEmotes(), "BetterTTV",
MessageElementFlag::BttvEmote);
addEmotes(*channelChannel, *twitchChannel->ffzEmotes(), "FrankerFaceZ",
MessageElementFlag::FfzEmote);
addEmotes(*channelChannel, *this->twitchChannel_->ffzEmotes(),
"FrankerFaceZ", MessageElementFlag::FfzEmote);
this->globalEmotesView_->setChannel(globalChannel);
this->subEmotesView_->setChannel(subChannel);
@ -313,29 +368,117 @@ void EmotePopup::loadChannel(ChannelPtr _channel)
}
}
void EmotePopup::loadEmojis()
void EmotePopup::loadEmojis(ChannelView &view, EmojiMap &emojiMap)
{
auto &emojis = getApp()->emotes->emojis.emojis;
ChannelPtr emojiChannel(new Channel("", Channel::Type::None));
emojiChannel->addMessage(makeEmojiMessage(emojiMap));
view.setChannel(emojiChannel);
}
void EmotePopup::loadEmojis(Channel &channel, EmojiMap &emojiMap,
const QString &title)
{
channel.addMessage(makeTitleMessage(title));
channel.addMessage(makeEmojiMessage(emojiMap));
}
void EmotePopup::filterEmotes(const QString &searchText)
{
if (searchText.length() == 0)
{
this->notebook_->show();
this->searchView_->hide();
return;
}
auto searchChannel = std::make_shared<Channel>("", Channel::Type::None);
auto twitchEmoteSets =
getApp()->accounts->twitch.getCurrent()->accessEmotes()->emoteSets;
std::vector<std::shared_ptr<TwitchAccount::EmoteSet>> twitchGlobalEmotes{};
for (const auto &set : twitchEmoteSets)
{
auto setCopy = std::make_shared<TwitchAccount::EmoteSet>(*set);
auto setIt =
std::remove_if(setCopy->emotes.begin(), setCopy->emotes.end(),
[searchText](auto &emote) {
return !emote.name.string.contains(
searchText, Qt::CaseInsensitive);
});
setCopy->emotes.resize(std::distance(setCopy->emotes.begin(), setIt));
if (setCopy->emotes.size() > 0)
twitchGlobalEmotes.push_back(setCopy);
}
auto bttvGlobalEmotes = this->filterEmoteMap(
searchText, getApp()->twitch2->getBttvEmotes().emotes());
auto ffzGlobalEmotes = this->filterEmoteMap(
searchText, getApp()->twitch2->getFfzEmotes().emotes());
auto bttvChannelEmotes =
this->filterEmoteMap(searchText, this->twitchChannel_->bttvEmotes());
auto ffzChannelEmotes =
this->filterEmoteMap(searchText, this->twitchChannel_->ffzEmotes());
EmojiMap filteredEmojis{};
int emojiCount = 0;
getApp()->emotes->emojis.emojis.each(
[&, searchText](const auto &name, std::shared_ptr<EmojiData> &emoji) {
if (emoji->shortCodes[0].contains(searchText, Qt::CaseInsensitive))
{
filteredEmojis.insert(name, emoji);
emojiCount++;
}
});
// twitch
addEmoteSets(twitchGlobalEmotes, *searchChannel, *searchChannel,
this->channel_->getName());
// global
if (bttvGlobalEmotes->size() > 0)
addEmotes(*searchChannel, *bttvGlobalEmotes, "BetterTTV (Global)",
MessageElementFlag::BttvEmote);
if (ffzGlobalEmotes->size() > 0)
addEmotes(*searchChannel, *ffzGlobalEmotes, "FrankerFaceZ (Global)",
MessageElementFlag::FfzEmote);
// channel
if (bttvChannelEmotes->size() > 0)
addEmotes(*searchChannel, *bttvChannelEmotes, "BetterTTV (Channel)",
MessageElementFlag::BttvEmote);
if (ffzChannelEmotes->size() > 0)
addEmotes(*searchChannel, *ffzChannelEmotes, "FrankerFaceZ (Channel)",
MessageElementFlag::FfzEmote);
// emojis
MessageBuilder builder;
builder->flags.set(MessageFlag::Centered);
builder->flags.set(MessageFlag::DisableCompactEmotes);
if (emojiCount > 0)
this->loadEmojis(*searchChannel, filteredEmojis, "Emojis");
emojis.each([&builder](const auto &key, const auto &value) {
builder
.emplace<EmoteElement>(
value->emote,
MessageElementFlags{MessageElementFlag::AlwaysShow,
MessageElementFlag::EmojiAll})
->setLink(
Link(Link::Type::InsertText, ":" + value->shortCodes[0] + ":"));
});
emojiChannel->addMessage(builder.release());
this->searchView_->setChannel(searchChannel);
this->viewEmojis_->setChannel(emojiChannel);
this->notebook_->hide();
this->searchView_->show();
}
EmoteMap *EmotePopup::filterEmoteMap(const QString &text,
std::shared_ptr<const EmoteMap> emotes)
{
auto filteredMap = new EmoteMap();
for (const auto &emote : *emotes)
{
if (emote.first.string.contains(text, Qt::CaseInsensitive))
{
filteredMap->insert(emote);
}
}
return filteredMap;
}
void EmotePopup::closeEvent(QCloseEvent *event)

View file

@ -1,10 +1,14 @@
#pragma once
#include "providers/emoji/Emojis.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "widgets/BasePopup.hpp"
#include "widgets/Notebook.hpp"
#include <pajlada/signals/signal.hpp>
#include <QLineEdit>
namespace chatterino {
struct Link;
@ -18,7 +22,6 @@ public:
EmotePopup(QWidget *parent = nullptr);
void loadChannel(ChannelPtr channel);
void loadEmojis();
virtual void closeEvent(QCloseEvent *event) override;
@ -29,8 +32,23 @@ private:
ChannelView *channelEmotesView_{};
ChannelView *subEmotesView_{};
ChannelView *viewEmojis_{};
/**
* @brief Visible only when the user has specified a search query into the `search_` input.
* Otherwise the `notebook_` and all other views are visible.
*/
ChannelView *searchView_{};
ChannelPtr channel_;
TwitchChannel *twitchChannel_{};
QLineEdit *search_;
Notebook *notebook_;
void loadEmojis(ChannelView &view, EmojiMap &emojiMap);
void loadEmojis(Channel &channel, EmojiMap &emojiMap, const QString &title);
void filterEmotes(const QString &text);
EmoteMap *filterEmoteMap(const QString &text,
std::shared_ptr<const EmoteMap> emotes);
void addShortcuts() override;
};