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
"network.2"
"colorquant-imagerect"
"window-parent"
)
set(QT_MIN_VERSION "6.6.0")

View file

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

View file

@ -1,11 +1,13 @@
#include "jsonadapter.hpp"
#include <qassociativeiterable.h>
#include <qcontainerfwd.h>
#include <qjsonarray.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qjsonvalue.h>
#include <qjsvalue.h>
#include <qmetacontainer.h>
#include <qmetaobject.h>
#include <qnamespace.h>
#include <qobject.h>
@ -14,6 +16,7 @@
#include <qqmlengine.h>
#include <qqmlinfo.h>
#include <qqmllist.h>
#include <qsequentialiterable.h>
#include <qstringview.h>
#include <qvariant.h>
@ -131,13 +134,22 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas
}
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 {
auto jv = QJsonValue::fromVariant(val);
json.insert(prop.name(), jv);
if (val.canConvert<QJSValue>()) val = val.value<QJSValue>().toVariant();
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());
if (prop.metaType() == QMetaType::fromType<QVariant>()) {
auto variant = jval.toVariant();
auto oldValue = prop.read(this).value<QJSValue>();
auto newVariant = jval.toVariant();
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
// even if content is identical.
if (jval.toVariant() != oldValue.toVariant()) {
auto jsValue = qmlEngine(this)->fromVariant<QJSValue>(jval.toVariant());
prop.write(this, QVariant::fromValue(jsValue));
if (newVariant != oldVariant) {
auto jsValue = qmlEngine(this)->fromVariant<QJSValue>(newVariant);
prop.write(obj, QVariant::fromValue(jsValue));
}
} else if (QMetaType::canView(prop.metaType(), QMetaType::fromType<JsonObject*>())) {
// 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>>()
))
{
auto pval = prop.read(this);
auto pval = prop.read(obj);
if (pval.canConvert<QQmlListProperty<JsonObject>>()) {
auto lp = pval.value<QQmlListProperty<JsonObject>>();
@ -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 {
auto pval = prop.read(obj);
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();
<< prop.metaType().name() << " but got "
<< jval.toVariant().typeName();
}
}
}
}

View file

@ -3,7 +3,7 @@
#include <qnamespace.h>
#include <qobject.h>
#include <qqmlengine.h>
#include <qqmllist.h>
#include <qqmlinfo.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwindow.h>
@ -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);
}

View file

@ -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();

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) {
if (!screen) return nullptr;
auto* qscreen = screen->screen;
auto it = this->mScreenProjections.find(qscreen);
if (it != this->mScreenProjections.end()) {