diff --git a/CHANGELOG.md b/CHANGELOG.md index e581f3cd4..52002e0cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index 603f257b2..d9a1ccc23 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -16,7 +16,10 @@ #include "widgets/Scrollbar.hpp" #include "widgets/helper/ChannelView.hpp" +#include #include +#include +#include #include 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( + value->emote, + MessageElementFlags{MessageElementFlag::AlwaysShow, + MessageElementFlag::EmojiAll}) + ->setLink(Link(Link::Type::InsertText, + ":" + value->shortCodes[0] + ":")); + }); + + return builder.release(); + } void addEmoteSets( std::vector> 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()->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 { + 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(this->channel_.get()); - auto twitchChannel = dynamic_cast(_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::Type::None); auto globalChannel = std::make_shared("", 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::Type::None); + + auto twitchEmoteSets = + getApp()->accounts->twitch.getCurrent()->accessEmotes()->emoteSets; + std::vector> twitchGlobalEmotes{}; + + for (const auto &set : twitchEmoteSets) + { + auto setCopy = std::make_shared(*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 &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( - 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 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) diff --git a/src/widgets/dialogs/EmotePopup.hpp b/src/widgets/dialogs/EmotePopup.hpp index 8ed416bb0..22e18373c 100644 --- a/src/widgets/dialogs/EmotePopup.hpp +++ b/src/widgets/dialogs/EmotePopup.hpp @@ -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 +#include + 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 emotes); void addShortcuts() override; };