diff --git a/.clang-tidy b/.clang-tidy index 170ad019a..ae0728012 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -22,6 +22,7 @@ Checks: "-*, -readability-magic-numbers, -performance-noexcept-move-constructor, -misc-non-private-member-variables-in-classes, + -misc-no-recursion, -cppcoreguidelines-non-private-member-variables-in-classes, -modernize-use-nodiscard, -modernize-use-trailing-return-type, diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f12f82a4..f546e8a2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Minor: Add option to customise Moderation buttons with images. (#5369) - Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) - Minor: Added `flags.action` filter variable, allowing you to filter on `/me` messages. (#5397) +- Minor: Added the ability to duplicate tabs. (#5277) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) - Bugfix: Fixed a crash that could occur when logging was enabled in IRC servers that were removed. (#5419) - Dev: Use Qt's high DPI scaling. (#4868, #5400) diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index 601142b4a..106d6cabc 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -4,6 +4,7 @@ #include "controllers/completion/TabCompletionModel.hpp" #include "messages/LimitedQueue.hpp" +#include #include #include #include @@ -45,7 +46,7 @@ public: TwitchAutomod, TwitchEnd, Irc, - Misc + Misc, }; explicit Channel(const QString &name, Type type); @@ -151,3 +152,32 @@ private: }; } // namespace chatterino + +template <> +constexpr magic_enum::customize::customize_t + magic_enum::customize::enum_name( + chatterino::Channel::Type value) noexcept +{ + using Type = chatterino::Channel::Type; + switch (value) + { + case Type::Twitch: + return "twitch"; + case Type::TwitchWhispers: + return "whispers"; + case Type::TwitchWatching: + return "watching"; + case Type::TwitchMentions: + return "mentions"; + case Type::TwitchLive: + return "live"; + case Type::TwitchAutomod: + return "automod"; + case Type::Irc: + return "irc"; + case Type::Misc: + return "misc"; + default: + return default_tag; + } +} diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index e924329d2..a3871eee9 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -29,6 +29,8 @@ #include #include +#include + namespace chatterino { Notebook::Notebook(QWidget *parent) @@ -87,6 +89,12 @@ Notebook::Notebook(QWidget *parent) } NotebookTab *Notebook::addPage(QWidget *page, QString title, bool select) +{ + return this->addPageAt(page, -1, std::move(title), select); +} + +NotebookTab *Notebook::addPageAt(QWidget *page, int position, QString title, + bool select) { // Queue up save because: Tab added getIApp()->getWindows()->queueSave(); @@ -101,7 +109,14 @@ NotebookTab *Notebook::addPage(QWidget *page, QString title, bool select) item.page = page; item.tab = tab; - this->items_.append(item); + if (position == -1) + { + this->items_.push_back(item); + } + else + { + this->items_.insert(position, item); + } page->hide(); page->setParent(this); @@ -165,6 +180,48 @@ void Notebook::removePage(QWidget *page) this->performLayout(true); } +void Notebook::duplicatePage(QWidget *page) +{ + auto *item = this->findItem(page); + assert(item != nullptr); + if (item == nullptr) + { + return; + } + + auto *container = dynamic_cast(item->page); + if (!container) + { + return; + } + + auto *newContainer = new SplitContainer(this); + if (!container->getSplits().empty()) + { + auto descriptor = container->buildDescriptor(); + newContainer->applyFromDescriptor(descriptor); + } + + const auto tabPosition = this->indexOf(page); + auto newTabPosition = -1; + if (tabPosition != -1) + { + newTabPosition = tabPosition + 1; + } + auto newTabHighlightState = item->tab->highlightState(); + QString newTabTitle = ""; + if (item->tab->hasCustomTitle()) + { + newTabTitle = item->tab->getCustomTitle(); + } + + auto *tab = + this->addPageAt(newContainer, newTabPosition, newTabTitle, false); + tab->setHighlightState(newTabHighlightState); + + newContainer->setTab(tab); +} + void Notebook::removeCurrentPage() { if (this->selectedPage_ != nullptr) diff --git a/src/widgets/Notebook.hpp b/src/widgets/Notebook.hpp index 2829bf4fd..c024998d8 100644 --- a/src/widgets/Notebook.hpp +++ b/src/widgets/Notebook.hpp @@ -42,7 +42,16 @@ public: NotebookTab *addPage(QWidget *page, QString title = QString(), bool select = false); + + /** + * @brief Adds a page to the Notebook at a given position. + * + * @param position if set to -1, adds the page to the end + **/ + NotebookTab *addPageAt(QWidget *page, int position, + QString title = QString(), bool select = false); void removePage(QWidget *page); + void duplicatePage(QWidget *page); void removeCurrentPage(); /** diff --git a/src/widgets/helper/NotebookTab.cpp b/src/widgets/helper/NotebookTab.cpp index cf9141fb7..1084abca7 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -99,6 +99,10 @@ NotebookTab::NotebookTab(Notebook *notebook) getIApp()->getHotkeys()->getDisplaySequence(HotkeyCategory::Window, "popup", {{"window"}})); + this->menu_.addAction("Duplicate Tab", [this]() { + this->notebook_->duplicatePage(this->page); + }); + highlightNewMessagesAction_ = new QAction("Mark Tab as Unread on New Messages", &this->menu_); highlightNewMessagesAction_->setCheckable(true); diff --git a/src/widgets/splits/SplitContainer.cpp b/src/widgets/splits/SplitContainer.cpp index 2a274c6cc..bee31cd11 100644 --- a/src/widgets/splits/SplitContainer.cpp +++ b/src/widgets/splits/SplitContainer.cpp @@ -5,9 +5,12 @@ #include "common/QLogging.hpp" #include "common/WindowDescriptors.hpp" #include "debug/AssertInGuiThread.hpp" +#include "providers/irc/IrcChannel2.hpp" +#include "providers/irc/IrcServer.hpp" #include "singletons/Fonts.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" +#include "util/QMagicEnum.hpp" #include "widgets/helper/ChannelView.hpp" #include "widgets/helper/NotebookTab.hpp" #include "widgets/Notebook.hpp" @@ -762,6 +765,11 @@ SplitContainer::Node *SplitContainer::getBaseNode() return &this->baseNode_; } +NodeDescriptor SplitContainer::buildDescriptor() const +{ + return this->buildDescriptorRecursively(&this->baseNode_); +} + void SplitContainer::applyFromDescriptor(const NodeDescriptor &rootNode) { assert(this->baseNode_.type_ == Node::Type::EmptyRoot); @@ -799,6 +807,49 @@ void SplitContainer::popup() window.show(); } +NodeDescriptor SplitContainer::buildDescriptorRecursively( + const Node *currentNode) const +{ + if (currentNode->children_.empty()) + { + const auto channelType = + currentNode->split_->getIndirectChannel().getType(); + + SplitNodeDescriptor result; + result.type_ = qmagicenum::enumNameString(channelType); + + switch (channelType) + { + case Channel::Type::Irc: { + if (auto *ircChannel = dynamic_cast( + currentNode->split_->getChannel().get())) + { + if (ircChannel->server()) + { + result.server_ = ircChannel->server()->id(); + } + } + } + break; + } + + result.channelName_ = currentNode->split_->getChannel()->getName(); + result.filters_ = currentNode->split_->getFilters(); + return result; + } + + ContainerNodeDescriptor descriptor; + for (const auto &child : currentNode->children_) + { + descriptor.vertical_ = + currentNode->type_ == Node::Type::VerticalContainer; + descriptor.items_.push_back( + this->buildDescriptorRecursively(child.get())); + } + + return descriptor; +} + void SplitContainer::applyFromDescriptorRecursively( const NodeDescriptor &rootNode, Node *baseNode) { @@ -849,9 +900,9 @@ void SplitContainer::applyFromDescriptorRecursively( } const auto &splitNode = *inner; auto *split = new Split(this); + split->setFilters(splitNode.filters_); split->setChannel(WindowManager::decodeChannel(splitNode)); split->setModerationMode(splitNode.moderationMode_); - split->setFilters(splitNode.filters_); auto *node = new Node(); node->parent_ = baseNode; diff --git a/src/widgets/splits/SplitContainer.hpp b/src/widgets/splits/SplitContainer.hpp index 9022085da..9e4a6cc75 100644 --- a/src/widgets/splits/SplitContainer.hpp +++ b/src/widgets/splits/SplitContainer.hpp @@ -220,6 +220,7 @@ public: void hideResizeHandles(); void resetMouseStatus(); + NodeDescriptor buildDescriptor() const; void applyFromDescriptor(const NodeDescriptor &rootNode); void popup(); @@ -237,6 +238,7 @@ protected: void resizeEvent(QResizeEvent *event) override; private: + NodeDescriptor buildDescriptorRecursively(const Node *currentNode) const; void applyFromDescriptorRecursively(const NodeDescriptor &rootNode, Node *baseNode);