diff --git a/CHANGELOG.md b/CHANGELOG.md index 2992cd350..5833dc927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ - Minor: Indicate when subscriptions and resubscriptions are for multiple months. (#5642) - Minor: Proxy URL information is now included in the `/debug-env` command. (#5648) - Minor: Make raid entry message usernames clickable. (#5651) +- Minor: Tabs unhighlight when their content is read in other tabs. (#5649) - Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426, #5612) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed restricted users usernames not being clickable. (#5405) 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/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 50f6d1acc..eb33cef3b 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -1063,6 +1063,8 @@ void ChannelView::setChannel(const ChannelPtr &underlyingChannel) this->underlyingChannel_ = underlyingChannel; + this->updateID(); + this->performLayout(); this->queueUpdate(); @@ -1081,6 +1083,8 @@ void ChannelView::setChannel(const ChannelPtr &underlyingChannel) void ChannelView::setFilters(const QList &ids) { this->channelFilters_ = std::make_shared(ids); + + this->updateID(); } QList ChannelView::getFilterIds() const @@ -1190,11 +1194,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); } } @@ -3240,4 +3246,27 @@ void ChannelView::pendingLinkInfoStateChanged() this->tooltipWidget_->applyLastBoundsCheck(); } +void ChannelView::updateID() +{ + if (!this->underlyingChannel_) + { + // cannot update + return; + } + + std::size_t seed = 0; + auto first = qHash(this->underlyingChannel_->getName()); + auto second = qHash(this->getFilterIds()); + + boost::hash_combine(seed, first); + boost::hash_combine(seed, second); + + this->id_ = seed; +} + +ChannelView::ChannelViewID ChannelView::getID() const +{ + return this->id_; +} + } // namespace chatterino diff --git a/src/widgets/helper/ChannelView.hpp b/src/widgets/helper/ChannelView.hpp index 704210712..74fd0aa39 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(); @@ -212,9 +215,13 @@ public: Scrollbar *scrollbar(); + using ChannelViewID = std::size_t; + ChannelViewID getID() const; + 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 @@ -314,6 +321,9 @@ private: void showReplyThreadPopup(const MessagePtr &message); bool canReplyToMessages() const; + void updateID(); + ChannelViewID id_{}; + bool layoutQueued_ = false; bool bufferInvalidationQueued_ = false; @@ -374,9 +384,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..23d7d90bc 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 @@ -302,10 +305,144 @@ bool NotebookTab::isSelected() const return this->selected_; } +void NotebookTab::removeNewMessageSource( + const ChannelView::ChannelViewID &source) +{ + this->highlightSources_.newMessageSource.erase(source); +} + +void NotebookTab::removeHighlightedSource( + const ChannelView::ChannelViewID &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 channelViewId = channelViewSource.getID(); + this->removeHighlightedSource(channelViewId); + this->removeNewMessageSource(channelViewId); + 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(channelViewId); + tab->removeNewMessageSource(channelViewId); + 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 +495,23 @@ 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) { @@ -381,9 +528,87 @@ void NotebookTab::setHighlightState(HighlightState newHighlightStyle) this->update(); } -HighlightState NotebookTab::highlightState() const +void NotebookTab::updateHighlightState(HighlightState newHighlightStyle, + const ChannelView &channelViewSource, + const MessagePtr &message) { - return this->highlightState_; + 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; + } + + if (!this->highlightEnabled_ && + newHighlightStyle == HighlightState::NewMessage) + { + return; + } + + // message is highlighting unvisible tab + + auto channelViewId = channelViewSource.getID(); + + switch (newHighlightStyle) + { + case HighlightState::Highlighted: { + if (!this->highlightSources_.highlightedSource.contains( + channelViewId)) + { + this->highlightSources_.highlightedSource.insert(channelViewId); + } + break; + } + case HighlightState::NewMessage: { + if (!this->highlightSources_.newMessageSource.contains( + channelViewId)) + { + this->highlightSources_.newMessageSource.insert(channelViewId); + } + break; + } + case HighlightState::None: + break; + } + + if (this->highlightState_ == newHighlightStyle || + this->highlightState_ == HighlightState::Highlighted) + { + return; + } + + this->highlightState_ = newHighlightStyle; + this->update(); +} + +bool NotebookTab::shouldMessageHighlight(const ChannelView &channelViewSource, + const MessagePtr &message) const +{ + 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..2d3865daa 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,25 @@ private: int normalTabWidthForHeight(int height) const; + bool shouldMessageHighlight(const ChannelView &channelViewSource, + const MessagePtr &message) const; + + struct HighlightSources { + 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 ChannelView::ChannelViewID &source); + void removeHighlightedSource(const ChannelView::ChannelViewID &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();