diff --git a/.clang-tidy b/.clang-tidy index 170ad019a41..ae07280120b 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/src/common/Channel.cpp b/src/common/Channel.cpp index 872ba246209..7141dbe7efc 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -92,8 +92,16 @@ void Channel::addMessage(MessagePtr message, auto *irc = dynamic_cast(this); if (irc != nullptr) { - channelPlatform = QString("irc-%1").arg( - irc->server()->userFriendlyIdentifier()); + auto *ircServer = irc->server(); + if (ircServer != nullptr) + { + channelPlatform = QString("irc-%1").arg( + irc->server()->userFriendlyIdentifier()); + } + else + { + channelPlatform = "irc-unknown"; + } } } else if (this->isTwitchChannel()) diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index 6adac6a76f4..730a92adfbb 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 @@ class Channel : public std::enable_shared_from_this TwitchAutomod, TwitchEnd, Irc, - Misc + Misc, }; explicit Channel(const QString &name, Type type); @@ -151,3 +152,32 @@ class IndirectChannel }; } // 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 7fdff5495cf..0350a1d7499 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -87,6 +87,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 +107,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 +178,31 @@ void Notebook::removePage(QWidget *page) this->performLayout(true); } +void Notebook::duplicatePage(QWidget *page) +{ + auto *item = this->findItem(page); + assert(item != nullptr); + + auto *container = dynamic_cast(item->page); + if (!container) + { + return; + } + + auto descriptor = container->buildDescriptor(); + + auto *newContainer = new SplitContainer(this); + newContainer->applyFromDescriptor(descriptor); + + int newTabPosition = this->indexOf(page) + 1; + auto *tab = this->addPageAt( + newContainer, newTabPosition, + item->tab->hasCustomTitle() ? item->tab->getCustomTitle() : "", false); + tab->setHighlightState(item->tab->highlightState()); + + newContainer->setTab(tab); +} + void Notebook::removeCurrentPage() { if (this->selectedPage_ != nullptr) diff --git a/src/widgets/Notebook.hpp b/src/widgets/Notebook.hpp index ac0162c4283..df4b116d617 100644 --- a/src/widgets/Notebook.hpp +++ b/src/widgets/Notebook.hpp @@ -42,7 +42,16 @@ class Notebook : public BaseWidget 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 f7cb2b3a0f3..70f956791f9 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -98,6 +98,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 2a274c6cc82..4a5c3680535 100644 --- a/src/widgets/splits/SplitContainer.cpp +++ b/src/widgets/splits/SplitContainer.cpp @@ -5,6 +5,8 @@ #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" @@ -762,6 +764,11 @@ SplitContainer::Node *SplitContainer::getBaseNode() return &this->baseNode_; } +NodeDescriptor SplitContainer::buildDescriptor() +{ + return this->buildDescriptorRecursively(&this->baseNode_); +} + void SplitContainer::applyFromDescriptor(const NodeDescriptor &rootNode) { assert(this->baseNode_.type_ == Node::Type::EmptyRoot); @@ -799,6 +806,47 @@ void SplitContainer::popup() window.show(); } +NodeDescriptor SplitContainer::buildDescriptorRecursively(Node *currentNode) +{ + if (currentNode->children_.empty()) + { + const auto channelType = currentNode->split_->getChannel()->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 (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 +897,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 9022085dad8..310b5998e2b 100644 --- a/src/widgets/splits/SplitContainer.hpp +++ b/src/widgets/splits/SplitContainer.hpp @@ -220,6 +220,7 @@ class SplitContainer final : public BaseWidget void hideResizeHandles(); void resetMouseStatus(); + NodeDescriptor buildDescriptor(); void applyFromDescriptor(const NodeDescriptor &rootNode); void popup(); @@ -237,6 +238,7 @@ class SplitContainer final : public BaseWidget void resizeEvent(QResizeEvent *event) override; private: + NodeDescriptor buildDescriptorRecursively(Node *currentNode); void applyFromDescriptorRecursively(const NodeDescriptor &rootNode, Node *baseNode);