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 8b22d07..86687eb 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 @@ -66,6 +67,9 @@ 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. +- 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 e80c6f2..369ccbe 100644 --- a/src/io/jsonadapter.cpp +++ b/src/io/jsonadapter.cpp @@ -1,11 +1,13 @@ #include "jsonadapter.hpp" +#include #include #include #include #include #include #include +#include #include #include #include @@ -14,6 +16,7 @@ #include #include #include +#include #include #include @@ -131,13 +134,22 @@ 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(); + + 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(), jsonVal); } } } @@ -154,14 +166,16 @@ 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(this).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()); - prop.write(this, QVariant::fromValue(jsValue)); + if (newVariant != oldVariant) { + auto jsValue = qmlEngine(this)->fromVariant(newVariant); + 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 +210,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>(); @@ -247,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(); + } } } } 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" + } + } + } +} 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()) {