diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index 2a01020fc..53c6dfcfa 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -204,7 +204,7 @@ void Notebook::duplicatePage(QWidget *page) { newTabPosition = tabPosition + 1; } - auto newTabHighlightState = item->tab->highlightState(); + QString newTabTitle = ""; if (item->tab->hasCustomTitle()) { @@ -213,7 +213,7 @@ void Notebook::duplicatePage(QWidget *page) auto *tab = this->addPageAt(newContainer, newTabPosition, newTabTitle, false); - tab->setHighlightState(newTabHighlightState); + tab->copyHighlightStateAndSourcesFrom(item->tab); newContainer->setTab(tab); } diff --git a/src/widgets/Notebook.hpp b/src/widgets/Notebook.hpp index ac6c4dad7..23888ae61 100644 --- a/src/widgets/Notebook.hpp +++ b/src/widgets/Notebook.hpp @@ -2,6 +2,7 @@ #include "widgets/BaseWidget.hpp" +#include #include #include #include diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 066085030..54d5dded5 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -1190,11 +1190,13 @@ void ChannelView::messageAppended(MessagePtr &message, (this->channel_->getType() == Channel::Type::TwitchAutomod && getSettings()->enableAutomodHighlight)) { - this->tabHighlightRequested.invoke(HighlightState::Highlighted); + this->tabHighlightRequested.invoke(HighlightState::Highlighted, + message); } else { - this->tabHighlightRequested.invoke(HighlightState::NewMessage); + this->tabHighlightRequested.invoke(HighlightState::NewMessage, + message); } } diff --git a/src/widgets/helper/ChannelView.hpp b/src/widgets/helper/ChannelView.hpp index 704210712..52d08803a 100644 --- a/src/widgets/helper/ChannelView.hpp +++ b/src/widgets/helper/ChannelView.hpp @@ -179,6 +179,9 @@ public: LimitedQueueSnapshot &getMessagesSnapshot(); + // Returns true if message should be included + bool shouldIncludeMessage(const MessagePtr &m) const; + void queueLayout(); void invalidateBuffers(); @@ -214,7 +217,8 @@ public: pajlada::Signals::Signal mouseDown; pajlada::Signals::NoArgSignal selectionChanged; - pajlada::Signals::Signal tabHighlightRequested; + pajlada::Signals::Signal + tabHighlightRequested; pajlada::Signals::NoArgSignal liveStatusChanged; pajlada::Signals::Signal linkClicked; pajlada::Signals::Signal @@ -374,9 +378,6 @@ private: FilterSetPtr channelFilters_; - // Returns true if message should be included - bool shouldIncludeMessage(const MessagePtr &m) const; - // Returns whether the scrollbar should have highlights bool showScrollbarHighlights() const; diff --git a/src/widgets/helper/NotebookTab.cpp b/src/widgets/helper/NotebookTab.cpp index be2b371a3..ca14d384b 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -1,6 +1,7 @@ #include "widgets/helper/NotebookTab.hpp" #include "Application.hpp" +#include "common/Channel.hpp" #include "common/Common.hpp" #include "controllers/hotkeys/HotkeyCategory.hpp" #include "controllers/hotkeys/HotkeyController.hpp" @@ -12,9 +13,11 @@ #include "widgets/dialogs/SettingsDialog.hpp" #include "widgets/Notebook.hpp" #include "widgets/splits/DraggedSplit.hpp" +#include "widgets/splits/Split.hpp" #include "widgets/splits/SplitContainer.hpp" #include +#include #include #include #include @@ -53,6 +56,27 @@ namespace { } } // namespace +std::size_t NotebookTab::HighlightSources::ChannelViewProxyHash::operator()( + const ChannelViewProxy &cp) const noexcept +{ + std::size_t seed = 0; + auto first = qHash(cp.channelView->underlyingChannel()->getName()); + auto second = qHash(cp.channelView->getFilterIds()); + + boost::hash_combine(seed, first); + boost::hash_combine(seed, second); + + return seed; +} + +bool NotebookTab::HighlightSources::ChannelViewProxyEqual::operator()( + const ChannelViewProxy &lp, const ChannelViewProxy &rp) const +{ + return lp.channelView->underlyingChannel() == + rp.channelView->underlyingChannel() && + lp.channelView->getFilterIds() == rp.channelView->getFilterIds(); +} + NotebookTab::NotebookTab(Notebook *notebook) : Button(notebook) , positionChangedAnimation_(this, "pos") @@ -302,10 +326,146 @@ bool NotebookTab::isSelected() const return this->selected_; } +void NotebookTab::removeNewMessageSource( + const HighlightSources::ChannelViewProxy &source) +{ + this->highlightSources_.newMessageSource.erase(source); +} + +void NotebookTab::removeHighlightedSource( + const HighlightSources::ChannelViewProxy &source) +{ + this->highlightSources_.highlightedSource.erase(source); +} + +void NotebookTab::removeHighlightStateChangeSources( + const HighlightSources &toRemove) +{ + for (const auto &source : toRemove.newMessageSource) + { + this->removeNewMessageSource(source); + } + + for (const auto &source : toRemove.highlightedSource) + { + this->removeHighlightedSource(source); + } +} + +void NotebookTab::newHighlightSourceAdded(const ChannelView &channelViewSource) +{ + auto channelViewProxy = + HighlightSources::ChannelViewProxy{&channelViewSource}; + auto sourceChannel = channelViewSource.underlyingChannel(); + this->removeHighlightedSource(channelViewProxy); + this->removeNewMessageSource(channelViewProxy); + this->updateHighlightStateDueSourcesChange(); + + auto *splitNotebook = dynamic_cast(this->notebook_); + if (splitNotebook) + { + for (int i = 0; i < splitNotebook->getPageCount(); ++i) + { + auto *splitContainer = + dynamic_cast(splitNotebook->getPageAt(i)); + if (splitContainer) + { + auto *tab = splitContainer->getTab(); + if (tab && tab != this) + { + tab->removeHighlightedSource(channelViewProxy); + tab->removeNewMessageSource(channelViewProxy); + tab->updateHighlightStateDueSourcesChange(); + } + } + } + } +} + +void NotebookTab::updateHighlightStateDueSourcesChange() +{ + if (!this->highlightSources_.highlightedSource.empty()) + { + assert(this->highlightState_ == HighlightState::Highlighted); + return; + } + + if (!this->highlightSources_.newMessageSource.empty()) + { + if (this->highlightState_ != HighlightState::NewMessage) + { + this->highlightState_ = HighlightState::NewMessage; + this->update(); + } + } + else + { + if (this->highlightState_ != HighlightState::None) + { + this->highlightState_ = HighlightState::None; + this->update(); + } + } + + assert(this->highlightState_ != HighlightState::Highlighted); +} + +void NotebookTab::copyHighlightStateAndSourcesFrom(const NotebookTab *sourceTab) +{ + if (this->isSelected()) + { + assert(this->highlightSources_.highlightedSource.empty()); + assert(this->highlightSources_.newMessageSource.empty()); + assert(this->highlightState_ == HighlightState::None); + return; + } + + this->highlightSources_ = sourceTab->highlightSources_; + + if (!this->highlightEnabled_ && + sourceTab->highlightState_ == HighlightState::NewMessage) + { + return; + } + + if (this->highlightState_ == sourceTab->highlightState_ || + this->highlightState_ == HighlightState::Highlighted) + { + return; + } + + this->highlightState_ = sourceTab->highlightState_; + this->update(); +} + void NotebookTab::setSelected(bool value) { this->selected_ = value; + if (value) + { + auto *splitNotebook = dynamic_cast(this->notebook_); + if (splitNotebook) + { + for (int i = 0; i < splitNotebook->getPageCount(); ++i) + { + auto *splitContainer = + dynamic_cast(splitNotebook->getPageAt(i)); + if (splitContainer) + { + auto *tab = splitContainer->getTab(); + if (tab && tab != this) + { + tab->removeHighlightStateChangeSources( + this->highlightSources_); + tab->updateHighlightStateDueSourcesChange(); + } + } + } + } + } + + this->highlightSources_.clear(); this->highlightState_ = HighlightState::None; this->update(); @@ -358,13 +518,91 @@ bool NotebookTab::isLive() const return this->isLive_; } +HighlightState NotebookTab::highlightState() const +{ + return this->highlightState_; +} + void NotebookTab::setHighlightState(HighlightState newHighlightStyle) { if (this->isSelected()) { + assert(this->highlightSources_.highlightedSource.empty()); + assert(this->highlightSources_.newMessageSource.empty()); + assert(this->highlightState_ == HighlightState::None); return; } + this->highlightSources_.clear(); + + if (!this->highlightEnabled_ && + newHighlightStyle == HighlightState::NewMessage) + { + return; + } + + if (this->highlightState_ == newHighlightStyle || + this->highlightState_ == HighlightState::Highlighted) + { + return; + } + + this->highlightState_ = newHighlightStyle; + this->update(); +} + +void NotebookTab::updateHighlightState(HighlightState newHighlightStyle, + const ChannelView &channelViewSource, + const MessagePtr &message) +{ + if (this->isSelected()) + { + assert(this->highlightSources_.highlightedSource.empty()); + assert(this->highlightSources_.newMessageSource.empty()); + assert(this->highlightState_ == HighlightState::None); + return; + } + + if (!this->shouldMessageHighlight(channelViewSource, message)) + { + return; + } + + // message is highlighting unvisible tab + + auto underlyingChannel = channelViewSource.underlyingChannel(); + auto newFilters = channelViewSource.getFilterIds(); + auto channelViewProxy = + HighlightSources::ChannelViewProxy{&channelViewSource}; + + // the unvisible tab should unhighlight other tabs iff + // the other tab's filters are more generic therefore + // the other tab's filter set is subset of the unvisible tab + + switch (newHighlightStyle) + { + case HighlightState::Highlighted: { + if (!this->highlightSources_.highlightedSource.contains( + channelViewProxy)) + { + this->highlightSources_.highlightedSource.insert( + channelViewProxy); + } + break; + } + case HighlightState::NewMessage: { + if (!this->highlightSources_.newMessageSource.contains( + channelViewProxy)) + { + this->highlightSources_.newMessageSource.insert( + channelViewProxy); + } + break; + } + case HighlightState::None: + break; + } + if (!this->highlightEnabled_ && newHighlightStyle == HighlightState::NewMessage) { @@ -381,9 +619,28 @@ void NotebookTab::setHighlightState(HighlightState newHighlightStyle) this->update(); } -HighlightState NotebookTab::highlightState() const +bool NotebookTab::shouldMessageHighlight(const ChannelView &channelViewSource, + const MessagePtr &message) const { - return this->highlightState_; + auto *visibleSplitContainer = + dynamic_cast(this->notebook_->getSelectedPage()); + if (visibleSplitContainer != nullptr) + { + const auto &visibleSplits = visibleSplitContainer->getSplits(); + for (const auto &visibleSplit : visibleSplits) + { + if (channelViewSource.underlyingChannel() == + visibleSplit->getChannel() && + visibleSplit->getChannelView().shouldIncludeMessage(message) && + channelViewSource.shouldIncludeMessage(message) && + channelViewSource.getFilterIds().empty()) + { + return false; + } + } + } + + return true; } void NotebookTab::setHighlightsEnabled(const bool &newVal) diff --git a/src/widgets/helper/NotebookTab.hpp b/src/widgets/helper/NotebookTab.hpp index 6ae7802d0..ec0ad0ad9 100644 --- a/src/widgets/helper/NotebookTab.hpp +++ b/src/widgets/helper/NotebookTab.hpp @@ -2,6 +2,7 @@ #include "common/Common.hpp" #include "widgets/helper/Button.hpp" +#include "widgets/helper/ChannelView.hpp" #include "widgets/Notebook.hpp" #include @@ -59,11 +60,25 @@ public: **/ bool isLive() const; + /** + * @brief Sets the highlight state of this tab clearing highlight sources + * + * Obeys the HighlightsEnabled setting and highlight states hierarchy + */ void setHighlightState(HighlightState style); - HighlightState highlightState() const; - + /** + * @brief Updates the highlight state and highlight sources of this tab + * + * Obeys the HighlightsEnabled setting and the highlight state hierarchy and tracks the highlight state update sources + */ + void updateHighlightState(HighlightState style, + const ChannelView &channelViewSource, + const MessagePtr &message); + void copyHighlightStateAndSourcesFrom(const NotebookTab *sourceTab); void setHighlightsEnabled(const bool &newVal); + void newHighlightSourceAdded(const ChannelView &channelViewSource); bool hasHighlightsEnabled() const; + HighlightState highlightState() const; void moveAnimated(QPoint targetPos, bool animated = true); @@ -107,6 +122,46 @@ private: int normalTabWidthForHeight(int height) const; + bool shouldMessageHighlight(const ChannelView &channelViewSource, + const MessagePtr &message) const; + + struct HighlightSources { + struct ChannelViewProxy { + const ChannelView *channelView; + }; + + struct ChannelViewProxyHash { + using is_transparent = void; + std::size_t operator()(const ChannelViewProxy &cp) const noexcept; + }; + + struct ChannelViewProxyEqual { + bool operator()(const ChannelViewProxy &l, + const ChannelViewProxy &r) const; + }; + + std::unordered_set + newMessageSource; + std::unordered_set + highlightedSource; + + void clear() + { + this->newMessageSource.clear(); + this->highlightedSource.clear(); + } + + } highlightSources_; + + void removeHighlightStateChangeSources(const HighlightSources &toRemove); + void removeNewMessageSource( + const HighlightSources::ChannelViewProxy &source); + void removeHighlightedSource( + const HighlightSources::ChannelViewProxy &source); + void updateHighlightStateDueSourcesChange(); + QPropertyAnimation positionChangedAnimation_; QPoint positionAnimationDesiredPoint_; diff --git a/src/widgets/splits/SplitContainer.cpp b/src/widgets/splits/SplitContainer.cpp index 1cca478f0..e0ef7d4ea 100644 --- a/src/widgets/splits/SplitContainer.cpp +++ b/src/widgets/splits/SplitContainer.cpp @@ -213,13 +213,22 @@ void SplitContainer::addSplit(Split *split) auto &&conns = this->connectionsPerSplit_[split]; - conns.managedConnect(split->getChannelView().tabHighlightRequested, - [this](HighlightState state) { - if (this->tab_ != nullptr) - { - this->tab_->setHighlightState(state); - } - }); + conns.managedConnect( + split->getChannelView().tabHighlightRequested, + [this, split](HighlightState state, const MessagePtr &message) { + if (this->tab_ != nullptr) + { + this->tab_->updateHighlightState(state, split->getChannelView(), + message); + } + }); + + conns.managedConnect(split->channelChanged, [this, split] { + if (this->tab_ != nullptr) + { + this->tab_->newHighlightSourceAdded(split->getChannelView()); + } + }); conns.managedConnect(split->getChannelView().liveStatusChanged, [this]() { this->refreshTabLiveStatus();