diff --git a/CHANGELOG.md b/CHANGELOG.md index 24e0fb4fb..720014b34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Major: Added multi-channel searching to search dialog via keyboard shortcut. [Ctrl+Shift+F by default] (#3694) - Minor: Added `is:first-msg` search option. (#3700) - Minor: Added quotation marks in the permitted/blocked Automod messages for clarity. (#3654) - Minor: Adjust large stream thumbnail to 16:9 (#3655) diff --git a/src/controllers/hotkeys/ActionNames.hpp b/src/controllers/hotkeys/ActionNames.hpp index 5470d7dc4..f326c24c8 100644 --- a/src/controllers/hotkeys/ActionNames.hpp +++ b/src/controllers/hotkeys/ActionNames.hpp @@ -103,7 +103,8 @@ inline const std::map actionNames{ 0, 1, }}, - {"showSearch", ActionDefinition{"Search"}}, + {"showSearch", ActionDefinition{"Search current channel"}}, + {"showGlobalSearch", ActionDefinition{"Search all channels"}}, {"startWatching", ActionDefinition{"Start watching"}}, {"debug", ActionDefinition{"Show debug popup"}}, }}, diff --git a/src/controllers/hotkeys/HotkeyController.cpp b/src/controllers/hotkeys/HotkeyController.cpp index 6e6d32f9c..7557fa67a 100644 --- a/src/controllers/hotkeys/HotkeyController.cpp +++ b/src/controllers/hotkeys/HotkeyController.cpp @@ -339,6 +339,9 @@ void HotkeyController::addDefaults(std::set &addedHotkeys) this->tryAddDefault(addedHotkeys, HotkeyCategory::Split, QKeySequence("Ctrl+F"), "showSearch", std::vector(), "show search"); + this->tryAddDefault(addedHotkeys, HotkeyCategory::Split, + QKeySequence("Ctrl+Shift+F"), "showGlobalSearch", + std::vector(), "show global search"); this->tryAddDefault(addedHotkeys, HotkeyCategory::Split, QKeySequence("Ctrl+F5"), "reconnect", std::vector(), "reconnect"); diff --git a/src/widgets/helper/ChannelView.hpp b/src/widgets/helper/ChannelView.hpp index 994f1e762..6588fc6cc 100644 --- a/src/widgets/helper/ChannelView.hpp +++ b/src/widgets/helper/ChannelView.hpp @@ -81,6 +81,8 @@ public: void pause(PauseReason reason, boost::optional msecs = boost::none); void unpause(PauseReason reason); + MessageElementFlags getFlags() const; + ChannelPtr channel(); void setChannel(ChannelPtr channel_); @@ -158,7 +160,6 @@ private: void drawMessages(QPainter &painter); void setSelection(const SelectionItem &start, const SelectionItem &end); - MessageElementFlags getFlags() const; void selectWholeMessage(MessageLayout *layout, int &messageIndex); void getWordBounds(MessageLayout *layout, const MessageLayoutElement *element, diff --git a/src/widgets/helper/SearchPopup.cpp b/src/widgets/helper/SearchPopup.cpp index 731ebe0ce..b5e80e616 100644 --- a/src/widgets/helper/SearchPopup.cpp +++ b/src/widgets/helper/SearchPopup.cpp @@ -3,11 +3,9 @@ #include #include #include -#include #include "common/Channel.hpp" #include "controllers/hotkeys/HotkeyController.hpp" -#include "messages/Message.hpp" #include "messages/search/AuthorPredicate.hpp" #include "messages/search/ChannelPredicate.hpp" #include "messages/search/LinkPredicate.hpp" @@ -19,8 +17,7 @@ namespace chatterino { ChannelPtr SearchPopup::filter(const QString &text, const QString &channelName, - const LimitedQueueSnapshot &snapshot, - FilterSetPtr filterSet) + const LimitedQueueSnapshot &snapshot) { ChannelPtr channel(new Channel(channelName, Channel::Type::None)); @@ -44,9 +41,6 @@ ChannelPtr SearchPopup::filter(const QString &text, const QString &channelName, } } - if (accept && filterSet) - accept = filterSet->filter(message, channel); - // If all predicates match, add the message to the channel if (accept) channel->addMessage(message); @@ -67,13 +61,13 @@ void SearchPopup::addShortcuts() { HotkeyController::HotkeyMap actions{ {"search", - [this](std::vector) -> QString { + [this](const std::vector &) -> QString { this->searchInput_->setFocus(); this->searchInput_->selectAll(); return ""; }}, {"delete", - [this](std::vector) -> QString { + [this](const std::vector &) -> QString { this->close(); return ""; }}, @@ -88,17 +82,25 @@ void SearchPopup::addShortcuts() HotkeyCategory::PopupWindow, actions, this); } -void SearchPopup::setChannelFilters(FilterSetPtr filters) +void SearchPopup::addChannel(ChannelView &channel) { - this->channelFilters_ = std::move(filters); -} + if (this->searchChannels_.empty()) + { + this->channelView_->setSourceChannel(channel.channel()); + this->channelName_ = channel.channel()->getName(); + } + else if (this->searchChannels_.size() == 1) + { + this->channelView_->setSourceChannel( + std::make_shared("multichannel", Channel::Type::None)); -void SearchPopup::setChannel(const ChannelPtr &channel) -{ - this->channelView_->setSourceChannel(channel); - this->channelName_ = channel->getName(); - this->snapshot_ = channel->getMessageSnapshot(); - this->search(); + auto flags = this->channelView_->getFlags(); + flags.set(MessageElementFlag::ChannelName); + flags.unset(MessageElementFlag::ModeratorTools); + this->channelView_->setOverrideFlags(flags); + } + + this->searchChannels_.append(std::ref(channel)); this->updateWindowTitle(); } @@ -115,6 +117,10 @@ void SearchPopup::updateWindowTitle() { historyName = "mentions"; } + else if (this->searchChannels_.size() > 1) + { + historyName = "multiple channels'"; + } else if (this->channelName_.isEmpty()) { historyName = "'s"; @@ -126,24 +132,90 @@ void SearchPopup::updateWindowTitle() this->setWindowTitle("Searching in " + historyName + " history"); } +void SearchPopup::showEvent(QShowEvent *) +{ + this->search(); +} + void SearchPopup::search() { + if (this->snapshot_.size() == 0) + { + this->snapshot_ = this->buildSnapshot(); + } + this->channelView_->setChannel(filter(this->searchInput_->text(), - this->channelName_, this->snapshot_, - this->channelFilters_)); + this->channelName_, this->snapshot_)); +} + +LimitedQueueSnapshot SearchPopup::buildSnapshot() +{ + // no point in filtering/sorting if it's a single channel search + if (this->searchChannels_.length() == 1) + { + const auto channelPtr = this->searchChannels_.at(0); + return channelPtr.get().channel()->getMessageSnapshot(); + } + + auto combinedSnapshot = std::vector>{}; + for (auto &channel : this->searchChannels_) + { + ChannelView &sharedView = channel.get(); + + const FilterSetPtr filterSet = sharedView.getFilterSet(); + const LimitedQueueSnapshot &snapshot = + sharedView.channel()->getMessageSnapshot(); + + // TODO: implement iterator on LimitedQueueSnapshot? + for (auto i = 0; i < snapshot.size(); ++i) + { + const MessagePtr &message = snapshot[i]; + if (filterSet && !filterSet->filter(message, sharedView.channel())) + { + continue; + } + + combinedSnapshot.push_back(message); + } + } + + // remove any duplicate messages from splits containing the same channel + std::sort(combinedSnapshot.begin(), combinedSnapshot.end(), + [](MessagePtr &a, MessagePtr &b) { + return a->id > b->id; + }); + + auto uniqueIterator = + std::unique(combinedSnapshot.begin(), combinedSnapshot.end(), + [](MessagePtr &a, MessagePtr &b) { + return a->id == b->id; + }); + + combinedSnapshot.erase(uniqueIterator, combinedSnapshot.end()); + + // resort by time for presentation + std::sort(combinedSnapshot.begin(), combinedSnapshot.end(), + [](MessagePtr &a, MessagePtr &b) { + return a->serverReceivedTime < b->serverReceivedTime; + }); + + auto queue = LimitedQueue(combinedSnapshot.size()); + queue.pushFront(combinedSnapshot); + + return queue.getSnapshot(); } void SearchPopup::initLayout() { // VBOX { - QVBoxLayout *layout1 = new QVBoxLayout(this); + auto *layout1 = new QVBoxLayout(this); layout1->setMargin(0); layout1->setSpacing(0); // HBOX { - QHBoxLayout *layout2 = new QHBoxLayout(this); + auto *layout2 = new QHBoxLayout(this); layout2->setMargin(8); layout2->setSpacing(8); diff --git a/src/widgets/helper/SearchPopup.hpp b/src/widgets/helper/SearchPopup.hpp index cf9499ec2..61c0f9719 100644 --- a/src/widgets/helper/SearchPopup.hpp +++ b/src/widgets/helper/SearchPopup.hpp @@ -17,16 +17,17 @@ class SearchPopup : public BasePopup public: SearchPopup(QWidget *parent); - virtual void setChannel(const ChannelPtr &channel); - virtual void setChannelFilters(FilterSetPtr filters); + virtual void addChannel(ChannelView &channel); protected: virtual void updateWindowTitle(); + void showEvent(QShowEvent *event) override; private: void initLayout(); void search(); void addShortcuts() override; + LimitedQueueSnapshot buildSnapshot(); /** * @brief Only retains those message from a list of messages that satisfy a @@ -41,8 +42,7 @@ private: * "snapshot" */ static ChannelPtr filter(const QString &text, const QString &channelName, - const LimitedQueueSnapshot &snapshot, - FilterSetPtr filterSet); + const LimitedQueueSnapshot &snapshot); /** * @brief Checks the input for tags and registers their corresponding @@ -58,7 +58,7 @@ private: QLineEdit *searchInput_{}; ChannelView *channelView_{}; QString channelName_{}; - FilterSetPtr channelFilters_; + QList> searchChannels_; }; } // namespace chatterino diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 8ee377f41..ac9923a15 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -273,7 +273,12 @@ void Split::addShortcuts() }}, {"showSearch", [this](std::vector) -> QString { - this->showSearch(); + this->showSearch(true); + return ""; + }}, + {"showGlobalSearch", + [this](std::vector) -> QString { + this->showSearch(false); return ""; }}, {"reconnect", @@ -1155,13 +1160,29 @@ const QList Split::getFilters() const return this->view_->getFilterIds(); } -void Split::showSearch() +void Split::showSearch(bool singleChannel) { - SearchPopup *popup = new SearchPopup(this); - - popup->setChannelFilters(this->view_->getFilterSet()); + auto *popup = new SearchPopup(this); popup->setAttribute(Qt::WA_DeleteOnClose); - popup->setChannel(this->getChannel()); + + if (singleChannel) + { + popup->addChannel(this->getChannelView()); + popup->show(); + return; + } + + // Pass every ChannelView for every Split across the app to the search popup + auto ¬ebook = getApp()->windows->getMainWindow().getNotebook(); + for (int i = 0; i < notebook.getPageCount(); ++i) + { + auto container = dynamic_cast(notebook.getPageAt(i)); + for (auto split : container->getSplits()) + { + popup->addChannel(split->getChannelView()); + } + } + popup->show(); } diff --git a/src/widgets/splits/Split.hpp b/src/widgets/splits/Split.hpp index 3f49d26fa..7a1eb350e 100644 --- a/src/widgets/splits/Split.hpp +++ b/src/widgets/splits/Split.hpp @@ -171,7 +171,7 @@ public slots: void copyToClipboard(); void startWatching(); void setFiltersDialog(); - void showSearch(); + void showSearch(bool singleChannel); void showViewerList(); void openSubPage(); void reloadChannelAndSubscriberEmotes();