diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c7faf31..277fcd90b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Minor: Added a setting to hide similar messages by any user. (#2716) - Minor: Duplicate spaces now count towards the display message length. (#3002) - Minor: Commands are now backed up. (#3168) +- Minor: Added the ability to open an entire tab as a popup. (#3082) - Minor: Added optional parameter to /usercard command for opening a usercard in a different channel context. (#3172) - Bugfix: Fixed colored usernames sometimes not working. (#3170) - Bugfix: Restored ability to send duplicate `/me` messages. (#3166) diff --git a/src/common/Version.cpp b/src/common/Version.cpp index 67a3bb477..b678fe48a 100644 --- a/src/common/Version.cpp +++ b/src/common/Version.cpp @@ -2,6 +2,8 @@ #include "common/Modes.hpp" +#include + #define UGLYMACROHACK1(s) #s #define FROM_EXTERNAL_DEFINE(s) UGLYMACROHACK1(s) diff --git a/src/common/WindowDescriptors.cpp b/src/common/WindowDescriptors.cpp index 7a82596f3..902de665f 100644 --- a/src/common/WindowDescriptors.cpp +++ b/src/common/WindowDescriptors.cpp @@ -110,6 +110,42 @@ void SplitDescriptor::loadFromJSON(SplitDescriptor &descriptor, descriptor.filters_ = loadFilters(root.value("filters")); } +TabDescriptor TabDescriptor::loadFromJSON(const QJsonObject &tabObj) +{ + TabDescriptor tab; + // Load tab custom title + QJsonValue titleVal = tabObj.value("title"); + if (titleVal.isString()) + { + tab.customTitle_ = titleVal.toString(); + } + + // Load tab selected state + tab.selected_ = tabObj.value("selected").toBool(false); + + // Load tab "highlightsEnabled" state + tab.highlightsEnabled_ = tabObj.value("highlightsEnabled").toBool(true); + + QJsonObject splitRoot = tabObj.value("splits2").toObject(); + + // Load tab splits + if (!splitRoot.isEmpty()) + { + // root type + auto nodeType = splitRoot.value("type").toString(); + if (nodeType == "split") + { + tab.rootNode_ = loadNodes(splitRoot); + } + else if (nodeType == "horizontal" || nodeType == "vertical") + { + tab.rootNode_ = loadNodes(splitRoot); + } + } + + return tab; +} + WindowLayout WindowLayout::loadFromFile(const QString &path) { WindowLayout layout; @@ -117,15 +153,15 @@ WindowLayout WindowLayout::loadFromFile(const QString &path) bool hasSetAMainWindow = false; // "deserialize" - for (const QJsonValue &window_val : loadWindowArray(path)) + for (const QJsonValue &windowVal : loadWindowArray(path)) { - QJsonObject window_obj = window_val.toObject(); + QJsonObject windowObj = windowVal.toObject(); WindowDescriptor window; // Load window type - QString type_val = window_obj.value("type").toString(); - auto type = type_val == "main" ? WindowType::Main : WindowType::Popup; + QString typeVal = windowObj.value("type").toString(); + auto type = typeVal == "main" ? WindowType::Main : WindowType::Popup; if (type == WindowType::Main) { @@ -142,21 +178,21 @@ WindowLayout WindowLayout::loadFromFile(const QString &path) window.type_ = type; // Load window state - if (window_obj.value("state") == "minimized") + if (windowObj.value("state") == "minimized") { window.state_ = WindowDescriptor::State::Minimized; } - else if (window_obj.value("state") == "maximized") + else if (windowObj.value("state") == "maximized") { window.state_ = WindowDescriptor::State::Maximized; } // Load window geometry { - int x = window_obj.value("x").toInt(-1); - int y = window_obj.value("y").toInt(-1); - int width = window_obj.value("width").toInt(-1); - int height = window_obj.value("height").toInt(-1); + int x = windowObj.value("x").toInt(-1); + int y = windowObj.value("y").toInt(-1); + int width = windowObj.value("width").toInt(-1); + int height = windowObj.value("height").toInt(-1); window.geometry_ = QRect(x, y, width, height); } @@ -164,23 +200,10 @@ WindowLayout WindowLayout::loadFromFile(const QString &path) bool hasSetASelectedTab = false; // Load window tabs - QJsonArray tabs = window_obj.value("tabs").toArray(); - for (QJsonValue tab_val : tabs) + QJsonArray tabs = windowObj.value("tabs").toArray(); + for (QJsonValue tabVal : tabs) { - TabDescriptor tab; - - QJsonObject tab_obj = tab_val.toObject(); - - // Load tab custom title - QJsonValue title_val = tab_obj.value("title"); - if (title_val.isString()) - { - tab.customTitle_ = title_val.toString(); - } - - // Load tab selected state - tab.selected_ = tab_obj.value("selected").toBool(false); - + TabDescriptor tab = TabDescriptor::loadFromJSON(tabVal.toObject()); if (tab.selected_) { if (hasSetASelectedTab) @@ -192,34 +215,11 @@ WindowLayout WindowLayout::loadFromFile(const QString &path) } hasSetASelectedTab = true; } - - // Load tab "highlightsEnabled" state - tab.highlightsEnabled_ = - tab_obj.value("highlightsEnabled").toBool(true); - - QJsonObject splitRoot = tab_obj.value("splits2").toObject(); - - // Load tab splits - if (!splitRoot.isEmpty()) - { - // root type - auto nodeType = splitRoot.value("type").toString(); - if (nodeType == "split") - { - tab.rootNode_ = loadNodes(splitRoot); - } - else if (nodeType == "horizontal" || nodeType == "vertical") - { - tab.rootNode_ = - loadNodes(splitRoot); - } - } - window.tabs_.emplace_back(std::move(tab)); } // Load emote popup position - QJsonObject emote_popup_obj = window_obj.value("emotePopup").toObject(); + QJsonObject emote_popup_obj = windowObj.value("emotePopup").toObject(); layout.emotePopupPos_ = QPoint(emote_popup_obj.value("x").toInt(), emote_popup_obj.value("y").toInt()); diff --git a/src/common/WindowDescriptors.hpp b/src/common/WindowDescriptors.hpp index 9c2aa2888..4740bc919 100644 --- a/src/common/WindowDescriptors.hpp +++ b/src/common/WindowDescriptors.hpp @@ -67,6 +67,8 @@ struct ContainerNodeDescriptor { }; struct TabDescriptor { + static TabDescriptor loadFromJSON(const QJsonObject &root); + QString customTitle_; bool selected_{false}; bool highlightsEnabled_{true}; diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index af66ffcad..254b8b767 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -379,20 +379,20 @@ void WindowManager::save() QJsonDocument document; // "serialize" - QJsonArray window_arr; + QJsonArray windowArr; for (Window *window : this->windows_) { - QJsonObject window_obj; + QJsonObject windowObj; // window type switch (window->getType()) { case WindowType::Main: - window_obj.insert("type", "main"); + windowObj.insert("type", "main"); break; case WindowType::Popup: - window_obj.insert("type", "popup"); + windowObj.insert("type", "popup"); break; case WindowType::Attached:; @@ -400,68 +400,48 @@ void WindowManager::save() if (window->isMaximized()) { - window_obj.insert("state", "maximized"); + windowObj.insert("state", "maximized"); } else if (window->isMinimized()) { - window_obj.insert("state", "minimized"); + windowObj.insert("state", "minimized"); } // window geometry auto rect = window->getBounds(); - window_obj.insert("x", rect.x()); - window_obj.insert("y", rect.y()); - window_obj.insert("width", rect.width()); - window_obj.insert("height", rect.height()); + windowObj.insert("x", rect.x()); + windowObj.insert("y", rect.y()); + windowObj.insert("width", rect.width()); + windowObj.insert("height", rect.height()); - QJsonObject emote_popup_obj; - emote_popup_obj.insert("x", this->emotePopupPos_.x()); - emote_popup_obj.insert("y", this->emotePopupPos_.y()); - window_obj.insert("emotePopup", emote_popup_obj); + QJsonObject emotePopupObj; + emotePopupObj.insert("x", this->emotePopupPos_.x()); + emotePopupObj.insert("y", this->emotePopupPos_.y()); + windowObj.insert("emotePopup", emotePopupObj); // window tabs - QJsonArray tabs_arr; + QJsonArray tabsArr; - for (int tab_i = 0; tab_i < window->getNotebook().getPageCount(); - tab_i++) + for (int tabIndex = 0; tabIndex < window->getNotebook().getPageCount(); + tabIndex++) { - QJsonObject tab_obj; + QJsonObject tabObj; SplitContainer *tab = dynamic_cast( - window->getNotebook().getPageAt(tab_i)); + window->getNotebook().getPageAt(tabIndex)); assert(tab != nullptr); - // custom tab title - if (tab->getTab()->hasCustomTitle()) - { - tab_obj.insert("title", tab->getTab()->getCustomTitle()); - } - - // selected - if (window->getNotebook().getSelectedPage() == tab) - { - tab_obj.insert("selected", true); - } - - // highlighting on new messages - tab_obj.insert("highlightsEnabled", - tab->getTab()->hasHighlightsEnabled()); - - // splits - QJsonObject splits; - - this->encodeNodeRecursively(tab->getBaseNode(), splits); - - tab_obj.insert("splits2", splits); - tabs_arr.append(tab_obj); + bool isSelected = window->getNotebook().getSelectedPage() == tab; + WindowManager::encodeTab(tab, isSelected, tabObj); + tabsArr.append(tabObj); } - window_obj.insert("tabs", tabs_arr); - window_arr.append(window_obj); + windowObj.insert("tabs", tabsArr); + windowArr.append(windowObj); } QJsonObject obj; - obj.insert("windows", window_arr); + obj.insert("windows", windowArr); document.setObject(obj); // save file @@ -497,6 +477,32 @@ void WindowManager::queueSave() this->saveTimer->start(10s); } +void WindowManager::encodeTab(SplitContainer *tab, bool isSelected, + QJsonObject &obj) +{ + // custom tab title + if (tab->getTab()->hasCustomTitle()) + { + obj.insert("title", tab->getTab()->getCustomTitle()); + } + + // selected + if (isSelected) + { + obj.insert("selected", true); + } + + // highlighting on new messages + obj.insert("highlightsEnabled", tab->getTab()->hasHighlightsEnabled()); + + // splits + QJsonObject splits; + + WindowManager::encodeNodeRecursively(tab->getBaseNode(), splits); + + obj.insert("splits2", splits); +} + void WindowManager::encodeNodeRecursively(SplitNode *node, QJsonObject &obj) { switch (node->getType()) @@ -506,11 +512,12 @@ void WindowManager::encodeNodeRecursively(SplitNode *node, QJsonObject &obj) obj.insert("moderationMode", node->getSplit()->getModerationMode()); QJsonObject split; - encodeChannel(node->getSplit()->getIndirectChannel(), split); + WindowManager::encodeChannel(node->getSplit()->getIndirectChannel(), + split); obj.insert("data", split); QJsonArray filters; - encodeFilters(node->getSplit(), filters); + WindowManager::encodeFilters(node->getSplit(), filters); obj.insert("filters", filters); } break; @@ -520,14 +527,14 @@ void WindowManager::encodeNodeRecursively(SplitNode *node, QJsonObject &obj) ? "horizontal" : "vertical"); - QJsonArray items_arr; + QJsonArray itemsArr; for (const std::unique_ptr &n : node->getChildren()) { QJsonObject subObj; - this->encodeNodeRecursively(n.get(), subObj); - items_arr.append(subObj); + WindowManager::encodeNodeRecursively(n.get(), subObj); + itemsArr.append(subObj); } - obj.insert("items", items_arr); + obj.insert("items", itemsArr); } break; } diff --git a/src/singletons/WindowManager.hpp b/src/singletons/WindowManager.hpp index 42a6f5d51..87151d204 100644 --- a/src/singletons/WindowManager.hpp +++ b/src/singletons/WindowManager.hpp @@ -30,6 +30,8 @@ public: WindowManager(); ~WindowManager() override; + static void encodeTab(SplitContainer *tab, bool isSelected, + QJsonObject &obj); static void encodeChannel(IndirectChannel channel, QJsonObject &obj); static void encodeFilters(Split *split, QJsonArray &arr); static IndirectChannel decodeChannel(const SplitDescriptor &descriptor); @@ -99,7 +101,8 @@ public: pajlada::Signals::Signal selectSplitContainer; private: - void encodeNodeRecursively(SplitContainer::Node *node, QJsonObject &obj); + static void encodeNodeRecursively(SplitContainer::Node *node, + QJsonObject &obj); // Load window layout from the window-layout.json file WindowLayout loadWindowLayoutFromFile() const; diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index 932e05fd5..dea06c69c 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -338,6 +338,14 @@ void Window::addShortcuts() } }); + createWindowShortcut(this, "CTRL+SHIFT+N", [this] { + if (auto page = dynamic_cast( + this->notebook_->getSelectedPage())) + { + page->popup(); + } + }); + // Zoom in { auto s = new QShortcut(QKeySequence::ZoomIn, this); diff --git a/src/widgets/helper/NotebookTab.cpp b/src/widgets/helper/NotebookTab.cpp index 9fdd41db0..bb53599fc 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -64,6 +64,16 @@ NotebookTab::NotebookTab(Notebook *notebook) this->notebook_->removePage(this->page); }); + this->menu_.addAction( + "Popup Tab", + [=]() { + if (auto container = dynamic_cast(this->page)) + { + container->popup(); + } + }, + QKeySequence("Ctrl+Shift+N")); + highlightNewMessagesAction_ = new QAction("Mark Tab as Unread on New Messages", &this->menu_); highlightNewMessagesAction_->setCheckable(true); diff --git a/src/widgets/settingspages/KeyboardSettingsPage.cpp b/src/widgets/settingspages/KeyboardSettingsPage.cpp index 7cad92a5f..fcdc69a00 100644 --- a/src/widgets/settingspages/KeyboardSettingsPage.cpp +++ b/src/widgets/settingspages/KeyboardSettingsPage.cpp @@ -44,6 +44,8 @@ KeyboardSettingsPage::KeyboardSettingsPage() form->addRow(new QLabel("Ctrl + Shift + T"), new QLabel("Create new tab")); form->addRow(new QLabel("Ctrl + Shift + W"), new QLabel("Close current tab")); + form->addRow(new QLabel("Ctrl + Shift + N"), + new QLabel("Open current tab as a popup")); form->addRow(new QLabel("Ctrl + H"), new QLabel("Hide/Show similar messages (See General->R9K)")); diff --git a/src/widgets/splits/SplitContainer.cpp b/src/widgets/splits/SplitContainer.cpp index 4a04ba199..5e1543c95 100644 --- a/src/widgets/splits/SplitContainer.cpp +++ b/src/widgets/splits/SplitContainer.cpp @@ -9,6 +9,7 @@ #include "util/Helpers.hpp" #include "util/LayoutCreator.hpp" #include "widgets/Notebook.hpp" +#include "widgets/Window.hpp" #include "widgets/helper/ChannelView.hpp" #include "widgets/helper/NotebookTab.hpp" #include "widgets/splits/ClosedSplits.hpp" @@ -761,6 +762,33 @@ void SplitContainer::applyFromDescriptor(const NodeDescriptor &rootNode) this->layout(); } +void SplitContainer::popup() +{ + Window &window = getApp()->windows->createWindow(WindowType::Popup); + auto popupContainer = window.getNotebook().getOrAddSelectedPage(); + + QJsonObject encodedTab; + WindowManager::encodeTab(this, true, encodedTab); + TabDescriptor tab = TabDescriptor::loadFromJSON(encodedTab); + + // custom title + if (!tab.customTitle_.isEmpty()) + { + popupContainer->getTab()->setCustomTitle(tab.customTitle_); + } + + // highlighting on new messages + popupContainer->getTab()->setHighlightsEnabled(tab.highlightsEnabled_); + + // splits + if (tab.rootNode_) + { + popupContainer->applyFromDescriptor(*tab.rootNode_); + } + + window.show(); +} + void SplitContainer::applyFromDescriptorRecursively( const NodeDescriptor &rootNode, Node *node) { diff --git a/src/widgets/splits/SplitContainer.hpp b/src/widgets/splits/SplitContainer.hpp index bb26e43dc..1662f0b68 100644 --- a/src/widgets/splits/SplitContainer.hpp +++ b/src/widgets/splits/SplitContainer.hpp @@ -202,6 +202,8 @@ public: void applyFromDescriptor(const NodeDescriptor &rootNode); + void popup(); + protected: void paintEvent(QPaintEvent *event) override;