From 1e5dd7363b3efabde172ba07685e52aed71bee19 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 26 Jul 2026 15:24:37 +0200 Subject: [PATCH] feat: Set window roles for session restoration (#7058) This sets the window roles for our windows similar to what I described in #6655. All roles are prefixed with `chatterino.` to avoid collision in the case where we're embedded. - Main Window: `chatterino.main` - Popups: `chatterino.popup.{id}` - Emote popup: `chatterino.emote-popup` - Settings: `chatterino.settings` - Overlay: `chatterino.overlay` Right now, you need to pass `-session {session-id}_0` when starting, but I hope future Qt versions add a way to set the session ID after the `QGuiApplication` is created. Note that because of this, restoration won't work out of the box. That's expected. You can see the session status in the logs of `qt.qpa.wayland`. Tested with KDE 6.7 and Qt 6.12-beta1. Closes #6655. Reviewed-by: pajlada Reviewed-by: Mm2PL --- src/common/WindowDescriptors.cpp | 9 ++- src/common/WindowDescriptors.hpp | 1 + src/singletons/WindowManager.cpp | 102 +++++++++++++++++++------ src/singletons/WindowManager.hpp | 17 ++++- src/widgets/OverlayWindow.cpp | 1 + src/widgets/Window.cpp | 10 +++ src/widgets/Window.hpp | 5 ++ src/widgets/dialogs/EmotePopup.cpp | 3 + src/widgets/dialogs/SettingsDialog.cpp | 3 + src/widgets/dialogs/UserInfoPopup.cpp | 2 +- src/widgets/splits/Split.cpp | 4 +- src/widgets/splits/SplitContainer.cpp | 3 +- 12 files changed, 132 insertions(+), 28 deletions(-) diff --git a/src/common/WindowDescriptors.cpp b/src/common/WindowDescriptors.cpp index 2aaab34a5..b039e7f14 100644 --- a/src/common/WindowDescriptors.cpp +++ b/src/common/WindowDescriptors.cpp @@ -202,7 +202,7 @@ WindowLayout WindowLayout::loadFromFile(const QString &path) // "deserialize" for (const auto windowVal : loadWindowArray(path)) { - QJsonObject windowObj = windowVal.toObject(); + const QJsonObject windowObj = windowVal.toObject(); WindowDescriptor window; @@ -244,6 +244,13 @@ WindowLayout WindowLayout::loadFromFile(const QString &path) window.geometry_ = QRect(x, y, width, height); } + // Load popup ID + auto idVal = windowObj["popupID"]; + if (idVal.isDouble()) + { + window.popupID = idVal.toInt(1); + } + bool hasSetASelectedTab = false; // Load window tabs diff --git a/src/common/WindowDescriptors.hpp b/src/common/WindowDescriptors.hpp index bb25d5117..6c102de7b 100644 --- a/src/common/WindowDescriptors.hpp +++ b/src/common/WindowDescriptors.hpp @@ -106,6 +106,7 @@ struct WindowDescriptor { State state_ = State::None; QRect geometry_; + std::optional popupID; std::vector tabs_; }; diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index f63f27a22..59be3f41b 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -322,18 +322,16 @@ Window *WindowManager::getLastSelectedWindow() const return this->selectedWindow_; } -Window &WindowManager::createWindow(WindowType type, bool show, QWidget *parent) +Window &WindowManager::createWindow(WindowType type, + const CreateWindowArgs &args) { assertInGuiThread(); - auto *const realParent = [this, type, parent]() -> QWidget * { - (void)this; - (void)type; - - if (parent) + auto *const realParent = [&]() -> QWidget * { + if (args.parent) { // If a parent is explicitly specified, we use that immediately. - return parent; + return args.parent; } // FIXME: On Windows, parenting popup windows causes unwanted behavior (see @@ -355,9 +353,28 @@ Window &WindowManager::createWindow(WindowType type, bool show, QWidget *parent) }(); auto *window = new Window(type, realParent); + assert(!window->testAttribute(Qt::WA_WState_Created)); + switch (type) + { + case WindowType::Main: { + window->setWindowRole(u"chatterino.main"_s); + } + break; + case WindowType::Popup: { + size_t popupID = this->takePopupID(args.popupID); + window->setWindowRole(u"chatterino.popup." % + QString::number(popupID)); + window->setPopupID(popupID); + qCDebug(chatterinoWindowmanager) + << "Creating popup with ID" << popupID; + } + break; + case WindowType::Attached: + break; // No window role for you. + } this->windows_.push_back(window); - if (show) + if (args.parent) { window->show(); } @@ -366,17 +383,15 @@ Window &WindowManager::createWindow(WindowType type, bool show, QWidget *parent) { window->setAttribute(Qt::WA_DeleteOnClose); - QObject::connect(window, &QWidget::destroyed, this, [this, window] { - for (auto it = this->windows_.begin(); it != this->windows_.end(); - it++) - { - if (*it == window) - { - this->windows_.erase(it); - break; - } - } - }); + auto popupID = window->popupID(); + QObject::connect(window, &QWidget::destroyed, this, + [this, window, popupID] { + std::erase(this->windows_, window); + if (popupID) + { + this->closePopup(*popupID); + } + }); } return *window; @@ -384,7 +399,9 @@ Window &WindowManager::createWindow(WindowType type, bool show, QWidget *parent) Window &WindowManager::openInPopup(ChannelPtr channel) { - auto &popup = this->createWindow(WindowType::Popup, true); + auto &popup = this->createWindow(WindowType::Popup, { + .show = true, + }); auto *split = popup.getNotebook().getOrAddSelectedPage()->appendNewSplit(false); split->setChannel(channel); @@ -459,7 +476,7 @@ void WindowManager::initialize() // No main window has been created from loading, create an empty one if (this->mainWindow_ == nullptr) { - this->mainWindow_ = &this->createWindow(WindowType::Main); + this->mainWindow_ = &this->createWindow(WindowType::Main, {}); this->mainWindow_->getNotebook().addPage(true); // TODO: don't create main window if it's a frameless embed @@ -524,6 +541,12 @@ void WindowManager::save() windowObj.insert("width", rect.width()); windowObj.insert("height", rect.height()); + auto popupID = window->popupID(); + if (popupID) + { + windowObj.insert("popupID", static_cast(*popupID)); + } + windowObj["emotePopup"] = QJsonObject{ {"x", this->emotePopupBounds_.x()}, {"y", this->emotePopupBounds_.y()}, @@ -783,7 +806,9 @@ void WindowManager::applyWindowLayout(const WindowLayout &layout) { auto type = windowData.type_; - Window &window = this->createWindow(type, false); + Window &window = this->createWindow(type, { + .show = false, + }); if (type == WindowType::Main) { @@ -881,6 +906,39 @@ void WindowManager::applyWindowLayout(const WindowLayout &layout) break; } } + + // We might've opened a few popups, so make sure the next ID is unused. + this->refreshNextPopupID(); +} + +size_t WindowManager::takePopupID(std::optional preferred) +{ + size_t id = this->nextPopupID; + if (preferred && !this->usedPopupIDs.contains(*preferred)) + { + id = *preferred; + } + assert(!this->usedPopupIDs.contains(id)); + this->usedPopupIDs.insert(id); + this->refreshNextPopupID(); + return id; +} + +void WindowManager::closePopup(size_t id) +{ + // The user closed a popup. Remember this ID, so the popup will get this ID. + this->nextPopupID = id; + this->usedPopupIDs.remove(id); +} + +void WindowManager::refreshNextPopupID() +{ + size_t selected = 1; + while (this->usedPopupIDs.contains(selected)) + { + selected += 1; + } + this->nextPopupID = selected; } } // namespace chatterino diff --git a/src/singletons/WindowManager.hpp b/src/singletons/WindowManager.hpp index e9ff24236..ceebb731d 100644 --- a/src/singletons/WindowManager.hpp +++ b/src/singletons/WindowManager.hpp @@ -102,8 +102,13 @@ public: // - If the window was unfocused since being selected, this function will still return it. Window *getLastSelectedWindow() const; - Window &createWindow(WindowType type, bool show = true, - QWidget *parent = nullptr); + struct CreateWindowArgs { + bool show = true; + QWidget *parent = nullptr; + std::optional popupID; + }; + + Window &createWindow(WindowType type, const CreateWindowArgs &args); // Use this method if you want to open a "new" channel in a popup. If you want to popup an // existing Split or SplitContainer, consider using Split::popup() or SplitContainer::popup(). @@ -176,6 +181,10 @@ private: // Apply a window layout for this window manager. void applyWindowLayout(const WindowLayout &layout); + size_t takePopupID(std::optional preferred); + void closePopup(size_t id); + void refreshNextPopupID(); + // Contains the full path to the window layout file, e.g. /home/pajlada/.local/share/Chatterino/Settings/window-layout.json const QString windowLayoutFilePath; @@ -187,6 +196,10 @@ private: std::vector windows_; + /// ID to be used for the next popup. + size_t nextPopupID = 1; + QSet usedPopupIDs; + std::unique_ptr framelessEmbedWindow_; Window *mainWindow_{}; Window *selectedWindow_{}; diff --git a/src/widgets/OverlayWindow.cpp b/src/widgets/OverlayWindow.cpp index 8e7196863..af4e2d90b 100644 --- a/src/widgets/OverlayWindow.cpp +++ b/src/widgets/OverlayWindow.cpp @@ -111,6 +111,7 @@ OverlayWindow::OverlayWindow(IndirectChannel channel, { this->setAttribute(Qt::WA_DeleteOnClose); this->setWindowTitle(u"Chatterino - Overlay"_s); + this->setWindowRole(u"chatterino.overlay"_s); // QGridLayout is (ab)used to stack widgets and position them auto *grid = new QGridLayout(this); diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index 298c74bf2..e0edc152d 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -122,6 +122,16 @@ SplitNotebook &Window::getNotebook() return *this->notebook_; } +void Window::setPopupID(size_t id) +{ + this->popupID_ = id; +} + +std::optional Window::popupID() const +{ + return this->popupID_; +} + bool Window::event(QEvent *event) { switch (event->type()) diff --git a/src/widgets/Window.hpp b/src/widgets/Window.hpp index 957ae87e3..7b5d00cd7 100644 --- a/src/widgets/Window.hpp +++ b/src/widgets/Window.hpp @@ -34,6 +34,9 @@ public: WindowType getType(); SplitNotebook &getNotebook(); + void setPopupID(size_t id); + std::optional popupID() const; + pajlada::Signals::NoArgSignal closed; protected: @@ -63,6 +66,8 @@ private: PixmapButton *streamerModeTitlebarIcon_ = nullptr; void updateStreamerModeIcon(); + std::optional popupID_; + friend class Notebook; }; diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index f44865742..b6c040998 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -41,6 +41,8 @@ #include #include +using namespace Qt::Literals; + namespace { using namespace chatterino; @@ -395,6 +397,7 @@ EmotePopup::EmotePopup(QWidget *parent) , notebook_(new Notebook(this)) { // this->setStayInScreenRect(true); + this->setWindowRole(u"chatterino.emote-popup"_s); auto bounds = getApp()->getWindows()->emotePopupBounds(); if (bounds.size().isEmpty()) { diff --git a/src/widgets/dialogs/SettingsDialog.cpp b/src/widgets/dialogs/SettingsDialog.cpp index d20ff4650..3624bfac1 100644 --- a/src/widgets/dialogs/SettingsDialog.cpp +++ b/src/widgets/dialogs/SettingsDialog.cpp @@ -30,6 +30,8 @@ #include #include +using namespace Qt::Literals; + namespace chatterino { SettingsDialog::SettingsDialog(QWidget *parent) @@ -45,6 +47,7 @@ SettingsDialog::SettingsDialog(QWidget *parent) { this->setObjectName("SettingsDialog"); this->setWindowTitle("Chatterino Settings"); + this->setWindowRole(u"chatterino.settings"_s); // Disable the ? button in the titlebar until we decide to use it this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index 034e27daa..6f22a8d40 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -320,7 +320,7 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, Split *split) [loginName] { auto *app = getApp(); auto &window = app->getWindows()->createWindow( - WindowType::Popup, true); + WindowType::Popup, {}); auto *split = window.getNotebook() .getOrAddSelectedPage() ->appendNewSplit(false); diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 8f899a6ed..6e4521a54 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -56,6 +56,8 @@ #include +using namespace Qt::Literals; + namespace chatterino { namespace { void showTutorialVideo(QWidget *parent, const QString &source, @@ -1111,7 +1113,7 @@ void Split::explainSplitting() void Split::popup() { auto *app = getApp(); - Window &window = app->getWindows()->createWindow(WindowType::Popup); + Window &window = app->getWindows()->createWindow(WindowType::Popup, {}); auto *split = new Split(window.getNotebook().getOrAddSelectedPage()); diff --git a/src/widgets/splits/SplitContainer.cpp b/src/widgets/splits/SplitContainer.cpp index 43642ad87..2f2cf3fb3 100644 --- a/src/widgets/splits/SplitContainer.cpp +++ b/src/widgets/splits/SplitContainer.cpp @@ -816,7 +816,8 @@ void SplitContainer::applyFromDescriptor(const NodeDescriptor &rootNode) void SplitContainer::popup() { - Window &window = getApp()->getWindows()->createWindow(WindowType::Popup); + Window &window = + getApp()->getWindows()->createWindow(WindowType::Popup, {}); auto *popupContainer = window.getNotebook().getOrAddSelectedPage(); QJsonObject encodedTab;