Compare commits

...

7 commits

Author SHA1 Message Date
outfoxxed
ad5fd9116e
wm: add nullptr guard to WindowManager::screenProjection 2026-04-04 13:51:32 -07:00
outfoxxed
49d4f46cf1
io/fileview: handle deserialization to list<T> properties 2026-04-04 13:05:33 -07:00
outfoxxed
9b98d10178
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.
2026-04-04 12:28:40 -07:00
outfoxxed
854088c48c
io/fileview: convert containers to QVariantList/Map before serialize
QJsonValue::fromVariant doesn't do this automatically for some reason.
2026-04-04 02:06:22 -07:00
bbedward
b4e71cb2c0
core/window: add parentWindow property to FloatingWindow 2026-04-03 21:36:18 -07:00
outfoxxed
ceac3c6cfa
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.
2026-04-03 21:36:02 -07:00
outfoxxed
aaff22f4b0
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.
2026-04-03 21:35:11 -07:00
7 changed files with 219 additions and 18 deletions

View file

@ -4,6 +4,7 @@ project(quickshell VERSION "0.2.1" LANGUAGES CXX C)
set(UNRELEASED_FEATURES set(UNRELEASED_FEATURES
"network.2" "network.2"
"colorquant-imagerect" "colorquant-imagerect"
"window-parent"
) )
set(QT_MIN_VERSION "6.6.0") set(QT_MIN_VERSION "6.6.0")

View file

@ -30,6 +30,7 @@ set shell id.
- Added ext-background-effect window blur support. - Added ext-background-effect window blur support.
- Added per-corner radius support to Region. - Added per-corner radius support to Region.
- Added ColorQuantizer region selection. - Added ColorQuantizer region selection.
- Added dialog window support to FloatingWindow.
## Other Changes ## Other Changes
@ -66,6 +67,9 @@ set shell id.
- Worked around Qt bug causing crashes when plugging and unplugging monitors. - Worked around Qt bug causing crashes when plugging and unplugging monitors.
- Fixed HyprlandFocusGrab crashing if windows were destroyed after being passed to it. - Fixed HyprlandFocusGrab crashing if windows were destroyed after being passed to it.
- Fixed ScreencopyView pixelation when scaled. - 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 ## Packaging Changes

View file

@ -1,11 +1,13 @@
#include "jsonadapter.hpp" #include "jsonadapter.hpp"
#include <qassociativeiterable.h>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qjsonarray.h> #include <qjsonarray.h>
#include <qjsondocument.h> #include <qjsondocument.h>
#include <qjsonobject.h> #include <qjsonobject.h>
#include <qjsonvalue.h> #include <qjsonvalue.h>
#include <qjsvalue.h> #include <qjsvalue.h>
#include <qmetacontainer.h>
#include <qmetaobject.h> #include <qmetaobject.h>
#include <qnamespace.h> #include <qnamespace.h>
#include <qobject.h> #include <qobject.h>
@ -14,6 +16,7 @@
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qqmlinfo.h> #include <qqmlinfo.h>
#include <qqmllist.h> #include <qqmllist.h>
#include <qsequentialiterable.h>
#include <qstringview.h> #include <qstringview.h>
#include <qvariant.h> #include <qvariant.h>
@ -131,13 +134,22 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas
} }
json.insert(prop.name(), array); json.insert(prop.name(), array);
} else if (val.canConvert<QJSValue>()) {
auto variant = val.value<QJSValue>().toVariant();
auto jv = QJsonValue::fromVariant(variant);
json.insert(prop.name(), jv);
} else { } else {
auto jv = QJsonValue::fromVariant(val); if (val.canConvert<QJSValue>()) val = val.value<QJSValue>().toVariant();
json.insert(prop.name(), jv);
auto jsonVal = QJsonValue::fromVariant(val);
if (jsonVal.isNull() && !val.isNull() && val.isValid()) {
if (val.canConvert<QAssociativeIterable>()) {
val.convert(QMetaType::fromType<QVariantMap>());
} else if (val.canConvert<QSequentialIterable>()) {
val.convert(QMetaType::fromType<QVariantList>());
}
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()); auto jval = json.value(prop.name());
if (prop.metaType() == QMetaType::fromType<QVariant>()) { if (prop.metaType() == QMetaType::fromType<QVariant>()) {
auto variant = jval.toVariant(); auto newVariant = jval.toVariant();
auto oldValue = prop.read(this).value<QJSValue>(); auto oldValue = prop.read(obj);
auto oldVariant =
oldValue.canConvert<QJSValue>() ? oldValue.value<QJSValue>().toVariant() : oldValue;
// Calling prop.write with a new QJSValue will cause a property update // Calling prop.write with a new QJSValue will cause a property update
// even if content is identical. // even if content is identical.
if (jval.toVariant() != oldValue.toVariant()) { if (newVariant != oldVariant) {
auto jsValue = qmlEngine(this)->fromVariant<QJSValue>(jval.toVariant()); auto jsValue = qmlEngine(this)->fromVariant<QJSValue>(newVariant);
prop.write(this, QVariant::fromValue(jsValue)); prop.write(obj, QVariant::fromValue(jsValue));
} }
} else if (QMetaType::canView(prop.metaType(), QMetaType::fromType<JsonObject*>())) { } else if (QMetaType::canView(prop.metaType(), QMetaType::fromType<JsonObject*>())) {
// FIXME: This doesn't support creating descendants of JsonObject, as QMetaType.metaObject() // 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<QQmlListProperty<JsonObject>>() QMetaType::fromType<QQmlListProperty<JsonObject>>()
)) ))
{ {
auto pval = prop.read(this); auto pval = prop.read(obj);
if (pval.canConvert<QQmlListProperty<JsonObject>>()) { if (pval.canConvert<QQmlListProperty<JsonObject>>()) {
auto lp = pval.value<QQmlListProperty<JsonObject>>(); auto lp = pval.value<QQmlListProperty<JsonObject>>();
@ -247,12 +261,35 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM
} }
} else { } else {
auto variant = jval.toVariant(); auto variant = jval.toVariant();
auto convVariant = variant;
if (variant.convert(prop.metaType())) { if (convVariant.convert(prop.metaType())) {
prop.write(obj, variant); prop.write(obj, convVariant);
} else { } else {
qmlWarning(this) << "Failed to deserialize property " << prop.name() << ": expected " auto pval = prop.read(obj);
<< prop.metaType().name() << " but got " << jval.toVariant().typeName(); if (variant.canConvert<QSequentialIterable>() && pval.canView<QSequentialIterable>()) {
auto targetv = QVariant(pval.metaType());
auto target = targetv.view<QSequentialIterable>().metaContainer();
auto valueType = target.valueMetaType();
auto i = 0;
for (QVariant item: variant.value<QSequentialIterable>()) {
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();
}
} }
} }
} }

View file

@ -3,7 +3,7 @@
#include <qnamespace.h> #include <qnamespace.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qqmllist.h> #include <qqmlinfo.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h> #include <qtypes.h>
#include <qwindow.h> #include <qwindow.h>
@ -11,6 +11,27 @@
#include "proxywindow.hpp" #include "proxywindow.hpp"
#include "windowinterface.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() { void ProxyFloatingWindow::connectWindow() {
this->ProxyWindowBase::connectWindow(); this->ProxyWindowBase::connectWindow();
@ -19,6 +40,25 @@ void ProxyFloatingWindow::connectWindow() {
this->window->setMaximumSize(this->bMaximumSize); 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) { void ProxyFloatingWindow::trySetWidth(qint32 implicitWidth) {
if (!this->window->isVisible()) { if (!this->window->isVisible()) {
this->ProxyWindowBase::trySetWidth(implicitWidth); this->ProxyWindowBase::trySetWidth(implicitWidth);
@ -46,6 +86,42 @@ void ProxyFloatingWindow::onMaximumSizeChanged() {
emit this->maximumSizeChanged(); 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::FloatingWindowInterface(QObject* parent) 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::titleChanged, this, &FloatingWindowInterface::titleChanged);
QObject::connect(this->window, &ProxyFloatingWindow::minimumSizeChanged, this, &FloatingWindowInterface::minimumSizeChanged); QObject::connect(this->window, &ProxyFloatingWindow::minimumSizeChanged, this, &FloatingWindowInterface::minimumSizeChanged);
QObject::connect(this->window, &ProxyFloatingWindow::maximumSizeChanged, this, &FloatingWindowInterface::maximumSizeChanged); 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); QObject::connect(this->window, &ProxyWindowBase::windowConnected, this, &FloatingWindowInterface::onWindowConnected);
// clang-format on // clang-format on
} }
@ -169,3 +246,9 @@ bool FloatingWindowInterface::startSystemResize(Qt::Edges edges) const {
if (!qw) return false; if (!qw) return false;
return qw->startSystemResize(edges); return qw->startSystemResize(edges);
} }
QObject* FloatingWindowInterface::parentWindow() const { return this->window->parentWindow(); }
void FloatingWindowInterface::setParentWindow(QObject* window) {
this->window->setParentWindow(window);
}

View file

@ -16,9 +16,15 @@ class ProxyFloatingWindow: public ProxyWindowBase {
Q_OBJECT; Q_OBJECT;
public: public:
explicit ProxyFloatingWindow(QObject* parent = nullptr): ProxyWindowBase(parent) {} explicit ProxyFloatingWindow(QObject* parent = nullptr);
void connectWindow() override; 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 // 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. // which is awful so we disable it for floating windows.
@ -29,11 +35,28 @@ signals:
void minimumSizeChanged(); void minimumSizeChanged();
void maximumSizeChanged(); void maximumSizeChanged();
void titleChanged(); void titleChanged();
void parentWindowChanged();
private slots:
void onParentDestroyed();
private: private:
void onMinimumSizeChanged(); void onMinimumSizeChanged();
void onMaximumSizeChanged(); void onMaximumSizeChanged();
void onTitleChanged(); 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: public:
Q_OBJECT_BINDABLE_PROPERTY( Q_OBJECT_BINDABLE_PROPERTY(
@ -75,6 +98,11 @@ class FloatingWindowInterface: public WindowInterface {
Q_PROPERTY(bool maximized READ isMaximized WRITE setMaximized NOTIFY maximizedChanged); Q_PROPERTY(bool maximized READ isMaximized WRITE setMaximized NOTIFY maximizedChanged);
/// Whether the window is currently fullscreen. /// Whether the window is currently fullscreen.
Q_PROPERTY(bool fullscreen READ isFullscreen WRITE setFullscreen NOTIFY fullscreenChanged); 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 // clang-format on
QML_NAMED_ELEMENT(FloatingWindow); QML_NAMED_ELEMENT(FloatingWindow);
@ -101,6 +129,9 @@ public:
/// Start a system resize operation. Must be called during a pointer press/drag. /// Start a system resize operation. Must be called during a pointer press/drag.
Q_INVOKABLE [[nodiscard]] bool startSystemResize(Qt::Edges edges) const; Q_INVOKABLE [[nodiscard]] bool startSystemResize(Qt::Edges edges) const;
[[nodiscard]] QObject* parentWindow() const;
void setParentWindow(QObject* window);
signals: signals:
void minimumSizeChanged(); void minimumSizeChanged();
void maximumSizeChanged(); void maximumSizeChanged();
@ -108,6 +139,7 @@ signals:
void minimizedChanged(); void minimizedChanged();
void maximizedChanged(); void maximizedChanged();
void fullscreenChanged(); void fullscreenChanged();
void parentWindowChanged();
private slots: private slots:
void onWindowConnected(); void onWindowConnected();

View file

@ -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"
}
}
}
}

View file

@ -21,6 +21,8 @@ WindowManager* WindowManager::instance() {
} }
ScreenProjection* WindowManager::screenProjection(QuickshellScreenInfo* screen) { ScreenProjection* WindowManager::screenProjection(QuickshellScreenInfo* screen) {
if (!screen) return nullptr;
auto* qscreen = screen->screen; auto* qscreen = screen->screen;
auto it = this->mScreenProjections.find(qscreen); auto it = this->mScreenProjections.find(qscreen);
if (it != this->mScreenProjections.end()) { if (it != this->mScreenProjections.end()) {