From db8047ea7b0ec4deba86722c484e524637b7f6fe Mon Sep 17 00:00:00 2001 From: hemirt <1310440+hemirt@users.noreply.github.com> Date: Sat, 2 Nov 2024 14:21:56 +0100 Subject: [PATCH] highlight tabs only on unviewed messages (#5649) --- CHANGELOG.md | 1 + src/widgets/Notebook.cpp | 4 +- src/widgets/helper/ChannelView.cpp | 27 ++++ src/widgets/helper/ChannelView.hpp | 13 +- src/widgets/helper/NotebookTab.cpp | 210 +++++++++++++++++++++++++- src/widgets/helper/NotebookTab.hpp | 28 +++- src/widgets/splits/SplitContainer.cpp | 12 +- 7 files changed, 286 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc3924f39..95466ae93 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 a5fa62582..22cb629c4 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -1064,6 +1064,8 @@ void ChannelView::setChannel(const ChannelPtr &underlyingChannel) this->underlyingChannel_ = underlyingChannel; + this->updateID(); + this->performLayout(); this->queueUpdate(); @@ -1082,6 +1084,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 @@ -3243,4 +3247,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 40b40444e..3c8f078f4 100644 --- a/src/widgets/helper/ChannelView.hpp +++ b/src/widgets/helper/ChannelView.hpp @@ -212,6 +212,14 @@ public: Scrollbar *scrollbar(); + using ChannelViewID = std::size_t; + /// + /// \brief Get the ID of this ChannelView + /// + /// The ID is made of the underlying channel's name + /// combined with the filter set IDs + ChannelViewID getID() const; + pajlada::Signals::Signal mouseDown; pajlada::Signals::NoArgSignal selectionChanged; pajlada::Signals::Signal tabHighlightRequested; @@ -315,6 +323,9 @@ private: void showReplyThreadPopup(const MessagePtr &message); bool canReplyToMessages() const; + void updateID(); + ChannelViewID id_{}; + bool layoutQueued_ = false; bool bufferInvalidationQueued_ = false; @@ -376,7 +387,7 @@ private: FilterSetPtr channelFilters_; // Returns true if message should be included - bool shouldIncludeMessage(const MessagePtr &m) const; + bool shouldIncludeMessage(const MessagePtr &message) 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..563084d39 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,134 @@ bool NotebookTab::isSelected() const return this->selected_; } +void NotebookTab::removeHighlightStateChangeSources( + const HighlightSources &toRemove) +{ + for (const auto &[source, _] : toRemove) + { + this->removeHighlightSource(source); + } +} + +void NotebookTab::removeHighlightSource( + const ChannelView::ChannelViewID &source) +{ + this->highlightSources_.erase(source); +} + +void NotebookTab::newHighlightSourceAdded(const ChannelView &channelViewSource) +{ + auto channelViewId = channelViewSource.getID(); + this->removeHighlightSource(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->removeHighlightSource(channelViewId); + tab->updateHighlightStateDueSourcesChange(); + } + } + } + } +} + +void NotebookTab::updateHighlightStateDueSourcesChange() +{ + if (std::ranges::any_of(this->highlightSources_, [](const auto &keyval) { + return keyval.second == HighlightState::Highlighted; + })) + { + assert(this->highlightState_ == HighlightState::Highlighted); + return; + } + + if (std::ranges::any_of(this->highlightSources_, [](const auto &keyval) { + return keyval.second == HighlightState::NewMessage; + })) + { + 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_.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 +485,22 @@ 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_.empty()); + assert(this->highlightState_ == HighlightState::None); return; } + this->highlightSources_.clear(); + if (!this->highlightEnabled_ && newHighlightStyle == HighlightState::NewMessage) { @@ -381,9 +517,79 @@ void NotebookTab::setHighlightState(HighlightState newHighlightStyle) this->update(); } -HighlightState NotebookTab::highlightState() const +void NotebookTab::updateHighlightState(HighlightState newHighlightStyle, + const ChannelView &channelViewSource) { - return this->highlightState_; + if (this->isSelected()) + { + assert(this->highlightSources_.empty()); + assert(this->highlightState_ == HighlightState::None); + return; + } + + if (!this->shouldMessageHighlight(channelViewSource)) + { + return; + } + + if (!this->highlightEnabled_ && + newHighlightStyle == HighlightState::NewMessage) + { + return; + } + + // message is highlighting unvisible tab + + auto channelViewId = channelViewSource.getID(); + + switch (newHighlightStyle) + { + case HighlightState::Highlighted: + // override lower states + this->highlightSources_.insert_or_assign(channelViewId, + newHighlightStyle); + case HighlightState::NewMessage: { + // only insert if no state already there to avoid overriding + if (!this->highlightSources_.contains(channelViewId)) + { + this->highlightSources_.emplace(channelViewId, + newHighlightStyle); + } + 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 +{ + auto *visibleSplitContainer = + dynamic_cast(this->notebook_->getSelectedPage()); + if (visibleSplitContainer != nullptr) + { + const auto &visibleSplits = visibleSplitContainer->getSplits(); + for (const auto &visibleSplit : visibleSplits) + { + if (channelViewSource.getID() == + visibleSplit->getChannelView().getID()) + { + 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..ae3bfbc2f 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,24 @@ 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); + 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 +121,16 @@ private: int normalTabWidthForHeight(int height) const; + bool shouldMessageHighlight(const ChannelView &channelViewSource) const; + + using HighlightSources = + std::unordered_map; + HighlightSources highlightSources_; + + void removeHighlightStateChangeSources(const HighlightSources &toRemove); + void removeHighlightSource(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..430098f0e 100644 --- a/src/widgets/splits/SplitContainer.cpp +++ b/src/widgets/splits/SplitContainer.cpp @@ -214,13 +214,21 @@ void SplitContainer::addSplit(Split *split) auto &&conns = this->connectionsPerSplit_[split]; conns.managedConnect(split->getChannelView().tabHighlightRequested, - [this](HighlightState state) { + [this, split](HighlightState state) { if (this->tab_ != nullptr) { - this->tab_->setHighlightState(state); + this->tab_->updateHighlightState( + state, split->getChannelView()); } }); + conns.managedConnect(split->channelChanged, [this, split] { + if (this->tab_ != nullptr) + { + this->tab_->newHighlightSourceAdded(split->getChannelView()); + } + }); + conns.managedConnect(split->getChannelView().liveStatusChanged, [this]() { this->refreshTabLiveStatus(); });