From aaff22f4b0239529f738770fa3c4388438f219fc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 3 Apr 2026 21:28:09 -0700 Subject: [PATCH 1/7] io/fileview: write values into correct JsonObjects in deserialize Property writes were being done on the JsonAdapter and not the child JsonObject, resulting in the data of children being set on the adapter's props, and occasional crashes. --- changelog/next.md | 1 + src/io/jsonadapter.cpp | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 8b22d07..3059cc9 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -66,6 +66,7 @@ set shell id. - Worked around Qt bug causing crashes when plugging and unplugging monitors. - Fixed HyprlandFocusGrab crashing if windows were destroyed after being passed to it. - Fixed ScreencopyView pixelation when scaled. +- Fixed JsonAdapter crashing and providing bad data on read when using JsonObject. ## Packaging Changes diff --git a/src/io/jsonadapter.cpp b/src/io/jsonadapter.cpp index e80c6f2..68e85ab 100644 --- a/src/io/jsonadapter.cpp +++ b/src/io/jsonadapter.cpp @@ -155,13 +155,13 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM if (prop.metaType() == QMetaType::fromType()) { auto variant = jval.toVariant(); - auto oldValue = prop.read(this).value(); + auto oldValue = prop.read(obj).value(); // Calling prop.write with a new QJSValue will cause a property update // even if content is identical. if (jval.toVariant() != oldValue.toVariant()) { auto jsValue = qmlEngine(this)->fromVariant(jval.toVariant()); - prop.write(this, QVariant::fromValue(jsValue)); + prop.write(obj, QVariant::fromValue(jsValue)); } } else if (QMetaType::canView(prop.metaType(), QMetaType::fromType())) { // FIXME: This doesn't support creating descendants of JsonObject, as QMetaType.metaObject() @@ -196,7 +196,7 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM QMetaType::fromType>() )) { - auto pval = prop.read(this); + auto pval = prop.read(obj); if (pval.canConvert>()) { auto lp = pval.value>(); From ceac3c6cfacfd7afc806de230ec5a239c24a4199 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 3 Apr 2026 21:30:05 -0700 Subject: [PATCH 2/7] io/fileview: use QVariant when QJSValue cast fails in adapter prop read A QVariant(QVariantMap) does not convert implicitly to a QVaraint(QJSValue), causing extra signals to be emitted if the old value was not updated by js (replaced by a QJSValue) before deserializing again. --- changelog/next.md | 1 + src/io/jsonadapter.cpp | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 3059cc9..88c7484 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -67,6 +67,7 @@ set shell id. - Fixed HyprlandFocusGrab crashing if windows were destroyed after being passed to it. - Fixed ScreencopyView pixelation when scaled. - Fixed JsonAdapter crashing and providing bad data on read when using JsonObject. +- Fixed JsonAdapter sending unnecessary property changes for primitive values. ## Packaging Changes diff --git a/src/io/jsonadapter.cpp b/src/io/jsonadapter.cpp index 68e85ab..9ca7060 100644 --- a/src/io/jsonadapter.cpp +++ b/src/io/jsonadapter.cpp @@ -154,13 +154,15 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM auto jval = json.value(prop.name()); if (prop.metaType() == QMetaType::fromType()) { - auto variant = jval.toVariant(); - auto oldValue = prop.read(obj).value(); + auto newVariant = jval.toVariant(); + auto oldValue = prop.read(obj); + auto oldVariant = + oldValue.canConvert() ? oldValue.value().toVariant() : oldValue; // Calling prop.write with a new QJSValue will cause a property update // even if content is identical. - if (jval.toVariant() != oldValue.toVariant()) { - auto jsValue = qmlEngine(this)->fromVariant(jval.toVariant()); + if (newVariant != oldVariant) { + auto jsValue = qmlEngine(this)->fromVariant(newVariant); prop.write(obj, QVariant::fromValue(jsValue)); } } else if (QMetaType::canView(prop.metaType(), QMetaType::fromType())) { From b4e71cb2c0afe640e62ceb7b7fb44c5fdb3546cf Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 26 Nov 2025 10:29:51 -0500 Subject: [PATCH 3/7] core/window: add parentWindow property to FloatingWindow --- CMakeLists.txt | 1 + changelog/next.md | 1 + src/window/floatingwindow.cpp | 85 ++++++++++++++++++++++++- src/window/floatingwindow.hpp | 34 +++++++++- src/window/test/manual/parentwindow.qml | 42 ++++++++++++ 5 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 src/window/test/manual/parentwindow.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 966e8c3..8293f23 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ project(quickshell VERSION "0.2.1" LANGUAGES CXX C) set(UNRELEASED_FEATURES "network.2" "colorquant-imagerect" + "window-parent" ) set(QT_MIN_VERSION "6.6.0") diff --git a/changelog/next.md b/changelog/next.md index 88c7484..1137e9a 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -30,6 +30,7 @@ set shell id. - Added ext-background-effect window blur support. - Added per-corner radius support to Region. - Added ColorQuantizer region selection. +- Added dialog window support to FloatingWindow. ## Other Changes diff --git a/src/window/floatingwindow.cpp b/src/window/floatingwindow.cpp index a0c9fdd..7a46bbf 100644 --- a/src/window/floatingwindow.cpp +++ b/src/window/floatingwindow.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include #include @@ -11,6 +11,27 @@ #include "proxywindow.hpp" #include "windowinterface.hpp" +ProxyFloatingWindow::ProxyFloatingWindow(QObject* parent): ProxyWindowBase(parent) { + this->bTargetVisible.setBinding([this] { + if (!this->bWantsVisible) return false; + auto* parent = this->bParentProxyWindow.value(); + if (!parent) return true; + return parent->bindableBackerVisibility().value(); + }); +} + +void ProxyFloatingWindow::targetVisibleChanged() { + if (this->window && this->bParentProxyWindow) { + auto* bw = this->bParentProxyWindow.value()->backingWindow(); + + if (bw != this->window->transientParent()) { + this->window->setTransientParent(bw); + } + } + + this->ProxyWindowBase::setVisible(this->bTargetVisible); +} + void ProxyFloatingWindow::connectWindow() { this->ProxyWindowBase::connectWindow(); @@ -19,6 +40,25 @@ void ProxyFloatingWindow::connectWindow() { this->window->setMaximumSize(this->bMaximumSize); } +void ProxyFloatingWindow::completeWindow() { + this->ProxyWindowBase::completeWindow(); + + auto* parent = this->bParentProxyWindow.value(); + this->window->setTransientParent(parent ? parent->backingWindow() : nullptr); +} + +void ProxyFloatingWindow::postCompleteWindow() { + this->ProxyWindowBase::setVisible(this->bTargetVisible); +} + +void ProxyFloatingWindow::onParentDestroyed() { + this->mParentWindow = nullptr; + this->bParentProxyWindow = nullptr; + emit this->parentWindowChanged(); +} + +void ProxyFloatingWindow::setVisible(bool visible) { this->bWantsVisible = visible; } + void ProxyFloatingWindow::trySetWidth(qint32 implicitWidth) { if (!this->window->isVisible()) { this->ProxyWindowBase::trySetWidth(implicitWidth); @@ -46,6 +86,42 @@ void ProxyFloatingWindow::onMaximumSizeChanged() { emit this->maximumSizeChanged(); } +QObject* ProxyFloatingWindow::parentWindow() const { return this->mParentWindow; } + +void ProxyFloatingWindow::setParentWindow(QObject* window) { + if (window == this->mParentWindow) return; + + if (this->window && this->window->isVisible()) { + qmlWarning(this) << "parentWindow cannot be changed after the window is visible."; + return; + } + + if (this->bParentProxyWindow) { + QObject::disconnect(this->bParentProxyWindow, nullptr, this, nullptr); + } + + if (this->mParentWindow) { + QObject::disconnect(this->mParentWindow, nullptr, this, nullptr); + } + + this->mParentWindow = nullptr; + this->bParentProxyWindow = nullptr; + + if (auto* proxy = ProxyWindowBase::forObject(window)) { + this->mParentWindow = window; + this->bParentProxyWindow = proxy; + + QObject::connect( + this->mParentWindow, + &QObject::destroyed, + this, + &ProxyFloatingWindow::onParentDestroyed + ); + } + + emit this->parentWindowChanged(); +} + // FloatingWindowInterface FloatingWindowInterface::FloatingWindowInterface(QObject* parent) @@ -57,6 +133,7 @@ FloatingWindowInterface::FloatingWindowInterface(QObject* parent) QObject::connect(this->window, &ProxyFloatingWindow::titleChanged, this, &FloatingWindowInterface::titleChanged); QObject::connect(this->window, &ProxyFloatingWindow::minimumSizeChanged, this, &FloatingWindowInterface::minimumSizeChanged); QObject::connect(this->window, &ProxyFloatingWindow::maximumSizeChanged, this, &FloatingWindowInterface::maximumSizeChanged); + QObject::connect(this->window, &ProxyFloatingWindow::parentWindowChanged, this, &FloatingWindowInterface::parentWindowChanged); QObject::connect(this->window, &ProxyWindowBase::windowConnected, this, &FloatingWindowInterface::onWindowConnected); // clang-format on } @@ -169,3 +246,9 @@ bool FloatingWindowInterface::startSystemResize(Qt::Edges edges) const { if (!qw) return false; return qw->startSystemResize(edges); } + +QObject* FloatingWindowInterface::parentWindow() const { return this->window->parentWindow(); } + +void FloatingWindowInterface::setParentWindow(QObject* window) { + this->window->setParentWindow(window); +} diff --git a/src/window/floatingwindow.hpp b/src/window/floatingwindow.hpp index 06b5b9e..e9e536a 100644 --- a/src/window/floatingwindow.hpp +++ b/src/window/floatingwindow.hpp @@ -16,9 +16,15 @@ class ProxyFloatingWindow: public ProxyWindowBase { Q_OBJECT; public: - explicit ProxyFloatingWindow(QObject* parent = nullptr): ProxyWindowBase(parent) {} + explicit ProxyFloatingWindow(QObject* parent = nullptr); void connectWindow() override; + void completeWindow() override; + void postCompleteWindow() override; + void setVisible(bool visible) override; + + [[nodiscard]] QObject* parentWindow() const; + void setParentWindow(QObject* window); // Setting geometry while the window is visible makes the content item shrink but not the window // which is awful so we disable it for floating windows. @@ -29,11 +35,28 @@ signals: void minimumSizeChanged(); void maximumSizeChanged(); void titleChanged(); + void parentWindowChanged(); + +private slots: + void onParentDestroyed(); private: void onMinimumSizeChanged(); void onMaximumSizeChanged(); void onTitleChanged(); + void targetVisibleChanged(); + + QObject* mParentWindow = nullptr; + + Q_OBJECT_BINDABLE_PROPERTY(ProxyFloatingWindow, ProxyWindowBase*, bParentProxyWindow); + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(ProxyFloatingWindow, bool, bWantsVisible, true); + + Q_OBJECT_BINDABLE_PROPERTY( + ProxyFloatingWindow, + bool, + bTargetVisible, + &ProxyFloatingWindow::targetVisibleChanged + ); public: Q_OBJECT_BINDABLE_PROPERTY( @@ -75,6 +98,11 @@ class FloatingWindowInterface: public WindowInterface { Q_PROPERTY(bool maximized READ isMaximized WRITE setMaximized NOTIFY maximizedChanged); /// Whether the window is currently fullscreen. Q_PROPERTY(bool fullscreen READ isFullscreen WRITE setFullscreen NOTIFY fullscreenChanged); + /// The parent window of this window. Setting this makes the window a child of the parent, + /// which affects window stacking behavior. + /// + /// > [!NOTE] This property cannot be changed after the window is visible. + Q_PROPERTY(QObject* parentWindow READ parentWindow WRITE setParentWindow NOTIFY parentWindowChanged); // clang-format on QML_NAMED_ELEMENT(FloatingWindow); @@ -101,6 +129,9 @@ public: /// Start a system resize operation. Must be called during a pointer press/drag. Q_INVOKABLE [[nodiscard]] bool startSystemResize(Qt::Edges edges) const; + [[nodiscard]] QObject* parentWindow() const; + void setParentWindow(QObject* window); + signals: void minimumSizeChanged(); void maximumSizeChanged(); @@ -108,6 +139,7 @@ signals: void minimizedChanged(); void maximizedChanged(); void fullscreenChanged(); + void parentWindowChanged(); private slots: void onWindowConnected(); diff --git a/src/window/test/manual/parentwindow.qml b/src/window/test/manual/parentwindow.qml new file mode 100644 index 0000000..214ee25 --- /dev/null +++ b/src/window/test/manual/parentwindow.qml @@ -0,0 +1,42 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Fusion +import Quickshell + +Scope { + FloatingWindow { + id: control + color: contentItem.palette.window + ColumnLayout { + CheckBox { + id: parentCb + text: "Show parent" + } + + CheckBox { + id: dialogCb + text: "Show dialog" + } + } + } + + FloatingWindow { + id: parentw + Text { + text: "parent" + } + visible: parentCb.checked + color: contentItem.palette.window + + FloatingWindow { + id: dialog + parentWindow: parentw + visible: dialogCb.checked + color: contentItem.palette.window + + Text { + text: "dialog" + } + } + } +} From 854088c48c4020f35019851137197b1112a9b9ee Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 3 Apr 2026 22:57:58 -0700 Subject: [PATCH 4/7] io/fileview: convert containers to QVariantList/Map before serialize QJsonValue::fromVariant doesn't do this automatically for some reason. --- changelog/next.md | 1 + src/io/jsonadapter.cpp | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 1137e9a..86687eb 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -69,6 +69,7 @@ set shell id. - Fixed ScreencopyView pixelation when scaled. - Fixed JsonAdapter crashing and providing bad data on read when using JsonObject. - Fixed JsonAdapter sending unnecessary property changes for primitive values. +- Fixed JsonAdapter serialization for lists. ## Packaging Changes diff --git a/src/io/jsonadapter.cpp b/src/io/jsonadapter.cpp index 9ca7060..df65120 100644 --- a/src/io/jsonadapter.cpp +++ b/src/io/jsonadapter.cpp @@ -1,5 +1,6 @@ #include "jsonadapter.hpp" +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include #include #include @@ -131,13 +133,16 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas } json.insert(prop.name(), array); - } else if (val.canConvert()) { - auto variant = val.value().toVariant(); - auto jv = QJsonValue::fromVariant(variant); - json.insert(prop.name(), jv); } else { - auto jv = QJsonValue::fromVariant(val); - json.insert(prop.name(), jv); + if (val.canConvert()) val = val.value().toVariant(); + + if (val.canConvert()) { + val.convert(QMetaType::fromType()); + } else if (val.canConvert()) { + val.convert(QMetaType::fromType()); + } + + json.insert(prop.name(), QJsonValue::fromVariant(val)); } } } From 9b98d101786da92be63ce0f4072a98152f07afcc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 4 Apr 2026 12:28:40 -0700 Subject: [PATCH 5/7] io/fileview: try to convert values to json before handling sequences The previous code was interpreting a string as a list of characters and therefore a sequence. --- src/io/jsonadapter.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/io/jsonadapter.cpp b/src/io/jsonadapter.cpp index df65120..6d1f080 100644 --- a/src/io/jsonadapter.cpp +++ b/src/io/jsonadapter.cpp @@ -136,13 +136,19 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas } else { if (val.canConvert()) val = val.value().toVariant(); - if (val.canConvert()) { - val.convert(QMetaType::fromType()); - } else if (val.canConvert()) { - val.convert(QMetaType::fromType()); + auto jsonVal = QJsonValue::fromVariant(val); + + if (jsonVal.isNull() && !val.isNull() && val.isValid()) { + if (val.canConvert()) { + val.convert(QMetaType::fromType()); + } else if (val.canConvert()) { + val.convert(QMetaType::fromType()); + } + + jsonVal = QJsonValue::fromVariant(val); } - json.insert(prop.name(), QJsonValue::fromVariant(val)); + json.insert(prop.name(), jsonVal); } } } From 49d4f46cf1b2d40e4095791d56e3b88eb8f0d0df Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 4 Apr 2026 13:05:33 -0700 Subject: [PATCH 6/7] io/fileview: handle deserialization to list properties --- src/io/jsonadapter.cpp | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/io/jsonadapter.cpp b/src/io/jsonadapter.cpp index 6d1f080..369ccbe 100644 --- a/src/io/jsonadapter.cpp +++ b/src/io/jsonadapter.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -260,12 +261,35 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM } } else { auto variant = jval.toVariant(); + auto convVariant = variant; - if (variant.convert(prop.metaType())) { - prop.write(obj, variant); + if (convVariant.convert(prop.metaType())) { + prop.write(obj, convVariant); } else { - qmlWarning(this) << "Failed to deserialize property " << prop.name() << ": expected " - << prop.metaType().name() << " but got " << jval.toVariant().typeName(); + auto pval = prop.read(obj); + if (variant.canConvert() && pval.canView()) { + auto targetv = QVariant(pval.metaType()); + auto target = targetv.view().metaContainer(); + auto valueType = target.valueMetaType(); + auto i = 0; + + for (QVariant item: variant.value()) { + if (item.convert(valueType)) { + target.addValueAtEnd(targetv.data(), item.constData()); + } else { + qmlWarning(this) << "Failed to deserialize list member " << i << " of property " + << prop.name() << ": expected " << valueType.name() << " but got " + << item.typeName(); + } + + ++i; + } + prop.write(obj, targetv); + } else { + qmlWarning(this) << "Failed to deserialize property " << prop.name() << ": expected " + << prop.metaType().name() << " but got " + << jval.toVariant().typeName(); + } } } } From ad5fd9116e25bc502468f4dfa884ee027887c51c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 4 Apr 2026 13:51:32 -0700 Subject: [PATCH 7/7] wm: add nullptr guard to WindowManager::screenProjection --- src/windowmanager/windowmanager.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/windowmanager/windowmanager.cpp b/src/windowmanager/windowmanager.cpp index 6b51db1..511e8ec 100644 --- a/src/windowmanager/windowmanager.cpp +++ b/src/windowmanager/windowmanager.cpp @@ -21,6 +21,8 @@ WindowManager* WindowManager::instance() { } ScreenProjection* WindowManager::screenProjection(QuickshellScreenInfo* screen) { + if (!screen) return nullptr; + auto* qscreen = screen->screen; auto it = this->mScreenProjections.find(qscreen); if (it != this->mScreenProjections.end()) {