feat: completely redesign hot reloader

The hot reloader previously attempted to figure out which parent a
component would attach to as it loaded. This was fairly error prone as
it was heuristic based and didn't work as soon as you split
definitions into multiple QML files.

The new hot reloader functions by first completely building the widget
tree, then applying the old tree to the first tree and pulling out
usable values. Proxy windows now wait to appear until being reloaded.

Additionally added support for `reloadableId` to help match a
Reloadable to its value in the previous widget tree.
This commit is contained in:
outfoxxed 2024-02-16 06:38:20 -08:00
parent d6ed717c39
commit 1da43be6c0
Signed by untrusted user: outfoxxed
GPG key ID: 4C88A185FB89301E
17 changed files with 518 additions and 442 deletions

View file

@ -34,6 +34,7 @@ Checks: >
-readability-uppercase-literal-suffix,
-readability-braces-around-statements,
-readability-redundant-access-specifiers,
-readability-else-after-return,
tidyfox-*,
CheckOptions:
performance-for-range-copy.WarnOnAllAutoCopies: true

View file

@ -33,7 +33,7 @@ qt_add_executable(quickshell
src/cpp/variants.cpp
src/cpp/rootwrapper.cpp
src/cpp/proxywindow.cpp
src/cpp/scavenge.cpp
src/cpp/reload.cpp
src/cpp/rootwrapper.cpp
src/cpp/qmlglobal.cpp
src/cpp/qmlscreen.cpp

View file

@ -14,18 +14,11 @@
#include "proxywindow.hpp"
#include "qmlscreen.hpp"
void ProxyShellWindow::earlyInit(QObject* old) {
this->ProxyWindowBase::earlyInit(old);
void ProxyShellWindow::setupWindow() {
QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyShellWindow::screenChanged);
this->shellWindow = LayerShellQt::Window::get(this->window);
// dont want to steal focus unless actually configured to
this->shellWindow->setKeyboardInteractivity(LayerShellQt::Window::KeyboardInteractivityNone);
// this dosent reset if its unset
this->shellWindow->setExclusiveZone(0);
this->ProxyWindowBase::setupWindow();
// clang-format off
QObject::connect(this->shellWindow, &LayerShellQt::Window::anchorsChanged, this, &ProxyShellWindow::anchorsChanged);
@ -38,26 +31,16 @@ void ProxyShellWindow::earlyInit(QObject* old) {
QObject::connect(this, &ProxyShellWindow::anchorsChanged, this, &ProxyShellWindow::updateExclusionZone);
QObject::connect(this, &ProxyShellWindow::marginsChanged, this, &ProxyShellWindow::updateExclusionZone);
// clang-format on
}
void ProxyShellWindow::componentComplete() {
this->complete = true;
this->window->setScreen(this->mScreen);
this->setAnchors(this->mAnchors);
this->setMargins(this->mMargins);
this->setExclusionMode(this->mExclusionMode); // also sets exclusion zone
this->setLayer(this->mLayer);
this->shellWindow->setScope(this->mScope);
this->setKeyboardFocus(this->mKeyboardFocus);
// The default anchor settings are a hazard because they cover the entire screen.
// We opt for 0 anchors by default to avoid blocking user input.
this->setAnchors(this->stagingAnchors);
this->updateExclusionZone();
// Make sure we signal changes from anchors, but only if this is a reload.
// If we do it on first load then it sends an extra change at 0px.
if (this->stagingAnchors.mLeft && this->stagingAnchors.mRight && this->width() != 0)
this->widthChanged(this->width());
if (this->stagingAnchors.mTop && this->stagingAnchors.mBottom && this->height() != 0)
this->heightChanged(this->height());
this->window->setVisible(this->stagingVisible);
this->ProxyWindowBase::componentComplete();
this->connected = true;
}
QQuickWindow* ProxyShellWindow::disownWindow() {
@ -65,54 +48,56 @@ QQuickWindow* ProxyShellWindow::disownWindow() {
return this->ProxyWindowBase::disownWindow();
}
void ProxyShellWindow::setVisible(bool visible) {
if (!this->complete) this->stagingVisible = visible;
else this->ProxyWindowBase::setVisible(visible);
}
bool ProxyShellWindow::isVisible() {
return this->complete ? this->ProxyWindowBase::isVisible() : this->stagingVisible;
}
void ProxyShellWindow::setWidth(qint32 width) {
this->requestedWidth = width;
this->mWidth = width;
// only update the actual size if not blocked by anchors
auto anchors = this->anchors();
if (this->complete && (!anchors.mLeft || !anchors.mRight)) this->ProxyWindowBase::setWidth(width);
}
qint32 ProxyShellWindow::width() {
return this->complete ? this->ProxyWindowBase::width() : this->requestedWidth;
if (!anchors.mLeft || !anchors.mRight) this->ProxyWindowBase::setWidth(width);
}
void ProxyShellWindow::setHeight(qint32 height) {
this->requestedHeight = height;
this->mHeight = height;
// only update the actual size if not blocked by anchors
auto anchors = this->anchors();
if (this->complete && (!anchors.mTop || !anchors.mBottom))
this->ProxyWindowBase::setHeight(height);
}
qint32 ProxyShellWindow::height() {
return this->complete ? this->ProxyWindowBase::height() : this->requestedHeight;
if (!anchors.mTop || !anchors.mBottom) this->ProxyWindowBase::setHeight(height);
}
void ProxyShellWindow::setScreen(QuickShellScreenInfo* screen) {
this->window->setScreen(screen->screen);
if (this->mScreen != nullptr) {
QObject::disconnect(this->mScreen, nullptr, this, nullptr);
}
auto* qscreen = screen == nullptr ? nullptr : screen->screen;
if (qscreen != nullptr) {
QObject::connect(qscreen, &QObject::destroyed, this, &ProxyShellWindow::onScreenDestroyed);
}
if (this->window == nullptr) this->mScreen = qscreen;
else this->window->setScreen(qscreen);
}
void ProxyShellWindow::onScreenDestroyed() { this->mScreen = nullptr; }
QuickShellScreenInfo* ProxyShellWindow::screen() const {
QScreen* qscreen = nullptr;
if (this->window == nullptr) {
if (this->mScreen != nullptr) qscreen = this->mScreen;
} else {
qscreen = this->window->screen();
}
return new QuickShellScreenInfo(
const_cast<ProxyShellWindow*>(this), // NOLINT
this->window->screen()
qscreen
);
}
void ProxyShellWindow::setAnchors(Anchors anchors) {
if (!this->complete) {
this->stagingAnchors = anchors;
if (this->window == nullptr) {
this->mAnchors = anchors;
return;
}
@ -122,14 +107,14 @@ void ProxyShellWindow::setAnchors(Anchors anchors) {
if (anchors.mTop) lsAnchors |= LayerShellQt::Window::AnchorTop;
if (anchors.mBottom) lsAnchors |= LayerShellQt::Window::AnchorBottom;
if (!anchors.mLeft || !anchors.mRight) this->ProxyWindowBase::setWidth(this->requestedWidth);
if (!anchors.mTop || !anchors.mBottom) this->ProxyWindowBase::setHeight(this->requestedHeight);
if (!anchors.mLeft || !anchors.mRight) this->ProxyWindowBase::setWidth(this->mWidth);
if (!anchors.mTop || !anchors.mBottom) this->ProxyWindowBase::setHeight(this->mHeight);
this->shellWindow->setAnchors(lsAnchors);
}
Anchors ProxyShellWindow::anchors() const {
if (!this->complete) return this->stagingAnchors;
if (this->window == nullptr) return this->mAnchors;
auto lsAnchors = this->shellWindow->anchors();
@ -144,25 +129,29 @@ Anchors ProxyShellWindow::anchors() const {
void ProxyShellWindow::setExclusiveZone(qint32 zone) {
if (zone < 0) zone = 0;
if (zone == this->requestedExclusionZone) return;
this->requestedExclusionZone = zone;
if (this->connected && zone == this->mExclusionZone) return;
this->mExclusionZone = zone;
if (this->exclusionMode() == ExclusionMode::Normal) {
if (this->window != nullptr && this->exclusionMode() == ExclusionMode::Normal) {
this->shellWindow->setExclusiveZone(zone);
emit this->exclusionZoneChanged();
}
}
qint32 ProxyShellWindow::exclusiveZone() const { return this->shellWindow->exclusionZone(); }
qint32 ProxyShellWindow::exclusiveZone() const {
if (this->window == nullptr) return this->mExclusionZone;
else return this->shellWindow->exclusionZone();
}
ExclusionMode::Enum ProxyShellWindow::exclusionMode() const { return this->mExclusionMode; }
void ProxyShellWindow::setExclusionMode(ExclusionMode::Enum exclusionMode) {
if (exclusionMode == this->mExclusionMode) return;
if (this->connected && exclusionMode == this->mExclusionMode) return;
this->mExclusionMode = exclusionMode;
if (this->window != nullptr) {
if (exclusionMode == ExclusionMode::Normal) {
this->shellWindow->setExclusiveZone(this->requestedExclusionZone);
this->shellWindow->setExclusiveZone(this->mExclusionZone);
emit this->exclusionZoneChanged();
} else if (exclusionMode == ExclusionMode::Ignore) {
this->shellWindow->setExclusiveZone(-1);
@ -170,14 +159,19 @@ void ProxyShellWindow::setExclusionMode(ExclusionMode::Enum exclusionMode) {
} else {
this->updateExclusionZone();
}
}
}
void ProxyShellWindow::setMargins(Margins margins) {
if (this->window == nullptr) this->mMargins = margins;
else {
auto lsMargins = QMargins(margins.mLeft, margins.mTop, margins.mRight, margins.mBottom);
this->shellWindow->setMargins(lsMargins);
}
}
Margins ProxyShellWindow::margins() const {
if (this->window == nullptr) return this->mMargins;
auto lsMargins = this->shellWindow->margins();
auto margins = Margins();
@ -190,6 +184,11 @@ Margins ProxyShellWindow::margins() const {
}
void ProxyShellWindow::setLayer(Layer::Enum layer) {
if (this->window == nullptr) {
this->mLayer = layer;
return;
}
auto lsLayer = LayerShellQt::Window::LayerBackground;
// clang-format off
@ -205,6 +204,8 @@ void ProxyShellWindow::setLayer(Layer::Enum layer) {
}
Layer::Enum ProxyShellWindow::layer() const {
if (this->window == nullptr) return this->mLayer;
auto layer = Layer::Top;
auto lsLayer = this->shellWindow->layer();
@ -220,11 +221,22 @@ Layer::Enum ProxyShellWindow::layer() const {
return layer;
}
void ProxyShellWindow::setScope(const QString& scope) { this->shellWindow->setScope(scope); }
void ProxyShellWindow::setScope(const QString& scope) {
if (this->window == nullptr) this->mScope = scope;
else this->shellWindow->setScope(scope);
}
QString ProxyShellWindow::scope() const { return this->shellWindow->scope(); }
QString ProxyShellWindow::scope() const {
if (this->window == nullptr) return this->mScope;
else return this->shellWindow->scope();
}
void ProxyShellWindow::setKeyboardFocus(KeyboardFocus::Enum focus) {
if (this->window == nullptr) {
this->mKeyboardFocus = focus;
return;
}
auto lsFocus = LayerShellQt::Window::KeyboardInteractivityNone;
// clang-format off
@ -239,6 +251,8 @@ void ProxyShellWindow::setKeyboardFocus(KeyboardFocus::Enum focus) {
}
KeyboardFocus::Enum ProxyShellWindow::keyboardFocus() const {
if (this->window == nullptr) return this->mKeyboardFocus;
auto focus = KeyboardFocus::None;
auto lsFocus = this->shellWindow->keyboardInteractivity();
@ -254,6 +268,11 @@ KeyboardFocus::Enum ProxyShellWindow::keyboardFocus() const {
}
void ProxyShellWindow::setScreenConfiguration(ScreenConfiguration::Enum configuration) {
if (this->window == nullptr) {
this->mScreenConfiguration = configuration;
return;
}
auto lsConfiguration = LayerShellQt::Window::ScreenFromQWindow;
// clang-format off
@ -267,6 +286,8 @@ void ProxyShellWindow::setScreenConfiguration(ScreenConfiguration::Enum configur
}
ScreenConfiguration::Enum ProxyShellWindow::screenConfiguration() const {
if (this->window == nullptr) return this->mScreenConfiguration;
auto configuration = ScreenConfiguration::Window;
auto lsConfiguration = this->shellWindow->screenConfiguration();
@ -280,14 +301,8 @@ ScreenConfiguration::Enum ProxyShellWindow::screenConfiguration() const {
return configuration;
}
void ProxyShellWindow::setCloseOnDismissed(bool close) {
this->shellWindow->setCloseOnDismissed(close);
}
bool ProxyShellWindow::closeOnDismissed() const { return this->shellWindow->closeOnDismissed(); }
void ProxyShellWindow::updateExclusionZone() {
if (this->exclusionMode() == ExclusionMode::Auto) {
if (this->window != nullptr && this->exclusionMode() == ExclusionMode::Auto) {
auto anchors = this->anchors();
auto zone = -1;

View file

@ -162,27 +162,18 @@ class ProxyShellWindow: public ProxyWindowBase {
/// The degree of keyboard focus taken. Defaults to `KeyboardFocus.None`.
Q_PROPERTY(KeyboardFocus::Enum keyboardFocus READ keyboardFocus WRITE setKeyboardFocus NOTIFY keyboardFocusChanged);
Q_PROPERTY(ScreenConfiguration::Enum screenConfiguration READ screenConfiguration WRITE setScreenConfiguration);
Q_PROPERTY(bool closeOnDismissed READ closeOnDismissed WRITE setCloseOnDismissed);
QML_ELEMENT;
// clang-format on
protected:
void earlyInit(QObject* old) override;
public:
void componentComplete() override;
void setupWindow() override;
QQuickWindow* disownWindow() override;
QQmlListProperty<QObject> data();
void setVisible(bool visible) override;
bool isVisible() override;
void setWidth(qint32 width) override;
qint32 width() override;
void setHeight(qint32 height) override;
qint32 height() override;
void setScreen(QuickShellScreenInfo* screen);
[[nodiscard]] QuickShellScreenInfo* screen() const;
@ -211,9 +202,6 @@ public:
void setScreenConfiguration(ScreenConfiguration::Enum configuration);
[[nodiscard]] ScreenConfiguration::Enum screenConfiguration() const;
void setCloseOnDismissed(bool close);
[[nodiscard]] bool closeOnDismissed() const;
signals:
void screenChanged();
void anchorsChanged();
@ -225,20 +213,19 @@ signals:
private slots:
void updateExclusionZone();
void onScreenDestroyed();
private:
LayerShellQt::Window* shellWindow = nullptr;
bool anchorsInitialized = false;
QScreen* mScreen = nullptr;
ExclusionMode::Enum mExclusionMode = ExclusionMode::Normal;
qint32 requestedExclusionZone = 0;
qint32 mExclusionZone = 0;
Anchors mAnchors;
Margins mMargins;
Layer::Enum mLayer = Layer::Top;
QString mScope;
KeyboardFocus::Enum mKeyboardFocus = KeyboardFocus::None;
ScreenConfiguration::Enum mScreenConfiguration = ScreenConfiguration::Window;
// needed to ensure size dosent fuck up when changing layershell attachments
// along with setWidth and setHeight overrides
qint32 requestedWidth = 100;
qint32 requestedHeight = 100;
// width/height must be set before anchors, so we have to track anchors and apply them late
bool complete = false;
bool stagingVisible = false;
Anchors stagingAnchors;
bool connected = false;
};

View file

@ -3,7 +3,7 @@ description = "Core QuickShell types"
headers = [
"qmlglobal.hpp",
"qmlscreen.hpp",
"scavenge.hpp",
"reload.hpp",
"shell.hpp",
"variants.hpp",
"proxywindow.hpp",

View file

@ -17,17 +17,29 @@ ProxyWindowBase::~ProxyWindowBase() {
}
}
void ProxyWindowBase::earlyInit(QObject* old) {
auto* oldpw = qobject_cast<ProxyWindowBase*>(old);
void ProxyWindowBase::onReload(QObject* oldInstance) {
auto* old = qobject_cast<ProxyWindowBase*>(oldInstance);
if (oldpw == nullptr || oldpw->window == nullptr) {
if (old == nullptr || old->window == nullptr) {
this->window = new QQuickWindow();
} else {
this->window = oldpw->disownWindow();
this->window = old->disownWindow();
}
this->window->setMask(QRegion());
this->setupWindow();
auto backer = this->dataBacker();
for (auto* child: this->pendingChildren) {
backer.append(&backer, child);
}
this->pendingChildren.clear();
emit this->windowConnected();
this->window->setVisible(this->mVisible);
}
void ProxyWindowBase::setupWindow() {
// clang-format off
QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged);
QObject::connect(this->window, &QWindow::widthChanged, this, &ProxyWindowBase::widthChanged);
@ -38,6 +50,11 @@ void ProxyWindowBase::earlyInit(QObject* old) {
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onMaskChanged);
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onMaskChanged);
// clang-format on
this->setWidth(this->mWidth);
this->setHeight(this->mHeight);
this->setColor(this->mColor);
this->updateMask();
}
QQuickWindow* ProxyWindowBase::disownWindow() {
@ -52,20 +69,57 @@ QQuickWindow* ProxyWindowBase::disownWindow() {
return window;
}
QQuickWindow* ProxyWindowBase::backingWindow() { return this->window; }
QQuickItem* ProxyWindowBase::item() { return this->window->contentItem(); }
QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; }
// NOLINTNEXTLINE
#define PROXYPROP(type, get, set) \
type ProxyWindowBase::get() { return this->window->get(); } \
void ProxyWindowBase::set(type value) { this->window->set(value); }
bool ProxyWindowBase::isVisible() const {
if (this->window == nullptr) return this->mVisible;
else return this->window->isVisible();
}
PROXYPROP(bool, isVisible, setVisible);
PROXYPROP(qint32, width, setWidth);
PROXYPROP(qint32, height, setHeight);
PROXYPROP(QColor, color, setColor);
void ProxyWindowBase::setVisible(bool visible) {
if (this->window == nullptr) {
this->mVisible = visible;
emit this->visibleChanged();
} else this->window->setVisible(visible);
}
PendingRegion* ProxyWindowBase::mask() { return this->mMask; }
qint32 ProxyWindowBase::width() const {
if (this->window == nullptr) return this->mWidth;
else return this->window->width();
}
void ProxyWindowBase::setWidth(qint32 width) {
if (this->window == nullptr) {
this->mWidth = width;
emit this->widthChanged();
} else this->window->setWidth(width);
}
qint32 ProxyWindowBase::height() const {
if (this->window == nullptr) return this->mHeight;
else return this->window->height();
}
void ProxyWindowBase::setHeight(qint32 height) {
if (this->window == nullptr) {
this->mHeight = height;
emit this->heightChanged();
} else this->window->setHeight(height);
}
QColor ProxyWindowBase::color() const {
if (this->window == nullptr) return this->mColor;
else return this->window->color();
}
void ProxyWindowBase::setColor(QColor color) {
if (this->window == nullptr) {
this->mColor = color;
emit this->colorChanged();
} else this->window->setColor(color);
}
PendingRegion* ProxyWindowBase::mask() const { return this->mMask; }
void ProxyWindowBase::setMask(PendingRegion* mask) {
if (this->mMask != nullptr) {
@ -81,6 +135,10 @@ void ProxyWindowBase::setMask(PendingRegion* mask) {
}
void ProxyWindowBase::onMaskChanged() {
if (this->window != nullptr) this->updateMask();
}
void ProxyWindowBase::updateMask() {
QRegion mask;
if (this->mMask != nullptr) {
// if left as the default, dont combine it with the whole window area, leave it as is.
@ -114,59 +172,87 @@ QQmlListProperty<QObject> ProxyWindowBase::data() {
);
}
QQmlListProperty<QObject> ProxyWindowBase::dataBacker(QQmlListProperty<QObject>* prop) {
auto* that = static_cast<ProxyWindowBase*>(prop->object); // NOLINT
return that->window->property("data").value<QQmlListProperty<QObject>>();
QQmlListProperty<QObject> ProxyWindowBase::dataBacker() {
return this->window->property("data").value<QQmlListProperty<QObject>>();
}
void ProxyWindowBase::dataAppend(QQmlListProperty<QObject>* prop, QObject* obj) {
auto backer = dataBacker(prop);
auto* self = static_cast<ProxyWindowBase*>(prop->object); // NOLINT
if (self->window == nullptr) {
if (obj != nullptr) {
obj->setParent(self);
self->pendingChildren.append(obj);
}
} else {
auto backer = self->dataBacker();
backer.append(&backer, obj);
}
}
qsizetype ProxyWindowBase::dataCount(QQmlListProperty<QObject>* prop) {
auto backer = dataBacker(prop);
auto* self = static_cast<ProxyWindowBase*>(prop->object); // NOLINT
if (self->window == nullptr) {
return self->pendingChildren.count();
} else {
auto backer = self->dataBacker();
return backer.count(&backer);
}
}
QObject* ProxyWindowBase::dataAt(QQmlListProperty<QObject>* prop, qsizetype i) {
auto backer = dataBacker(prop);
auto* self = static_cast<ProxyWindowBase*>(prop->object); // NOLINT
if (self->window == nullptr) {
return self->pendingChildren.at(i);
} else {
auto backer = self->dataBacker();
return backer.at(&backer, i);
}
}
void ProxyWindowBase::dataClear(QQmlListProperty<QObject>* prop) {
auto backer = dataBacker(prop);
auto* self = static_cast<ProxyWindowBase*>(prop->object); // NOLINT
if (self->window == nullptr) {
self->pendingChildren.clear();
} else {
auto backer = self->dataBacker();
backer.clear(&backer);
}
}
void ProxyWindowBase::dataReplace(QQmlListProperty<QObject>* prop, qsizetype i, QObject* obj) {
auto backer = dataBacker(prop);
auto* self = static_cast<ProxyWindowBase*>(prop->object); // NOLINT
if (self->window == nullptr) {
if (obj != nullptr) {
obj->setParent(self);
self->pendingChildren.replace(i, obj);
}
} else {
auto backer = self->dataBacker();
backer.replace(&backer, i, obj);
}
}
void ProxyWindowBase::dataRemoveLast(QQmlListProperty<QObject>* prop) {
auto backer = dataBacker(prop);
auto* self = static_cast<ProxyWindowBase*>(prop->object); // NOLINT
if (self->window == nullptr) {
self->pendingChildren.removeLast();
} else {
auto backer = self->dataBacker();
backer.removeLast(&backer);
}
void ProxyFloatingWindow::earlyInit(QObject* old) {
this->ProxyWindowBase::earlyInit(old);
this->geometryLocked = this->window->isVisible();
}
void ProxyFloatingWindow::componentComplete() {
this->ProxyWindowBase::componentComplete();
this->geometryLocked = true;
}
void ProxyFloatingWindow::setWidth(qint32 value) {
if (!this->geometryLocked) {
this->ProxyWindowBase::setWidth(value);
}
}
void ProxyFloatingWindow::setHeight(qint32 value) {
if (!this->geometryLocked) {
this->ProxyWindowBase::setHeight(value);
}
void ProxyFloatingWindow::setWidth(qint32 width) {
if (this->window == nullptr || !this->window->isVisible()) this->ProxyWindowBase::setWidth(width);
}
void ProxyFloatingWindow::setHeight(qint32 height) {
if (this->window == nullptr || !this->window->isVisible())
this->ProxyWindowBase::setHeight(height);
}

View file

@ -1,6 +1,7 @@
#pragma once
#include <qcolor.h>
#include <qcontainerfwd.h>
#include <qevent.h>
#include <qobject.h>
#include <qqmllist.h>
@ -11,7 +12,7 @@
#include <qtypes.h>
#include "region.hpp"
#include "scavenge.hpp"
#include "reload.hpp"
// Proxy to an actual window exposing a limited property set with the ability to
// transfer it to a new window.
@ -19,7 +20,7 @@
//
// NOTE: setting an `id` in qml will point to the proxy window and not the real window so things
// like anchors must use `item`.
class ProxyWindowBase: public Scavenger {
class ProxyWindowBase: public Reloadable {
Q_OBJECT;
/// The QtQuick window backing this window.
///
@ -29,8 +30,6 @@ class ProxyWindowBase: public Scavenger {
/// >
/// > Use **only** if you know what you are doing.
Q_PROPERTY(QQuickWindow* _backingWindow READ backingWindow);
/// The content item of the window.
Q_PROPERTY(QQuickItem* item READ item CONSTANT);
/// The visibility of the window.
///
/// > [!INFO] Windows are not visible by default so you will need to set this to make the window
@ -99,12 +98,8 @@ class ProxyWindowBase: public Scavenger {
Q_PROPERTY(QQmlListProperty<QObject> data READ data);
Q_CLASSINFO("DefaultProperty", "data");
protected:
void earlyInit(QObject* old) override;
QQuickWindow* window = nullptr;
public:
explicit ProxyWindowBase(QObject* parent = nullptr): Scavenger(parent) {}
explicit ProxyWindowBase(QObject* parent = nullptr): Reloadable(parent) {}
~ProxyWindowBase() override;
ProxyWindowBase(ProxyWindowBase&) = delete;
@ -112,41 +107,55 @@ public:
void operator=(ProxyWindowBase&) = delete;
void operator=(ProxyWindowBase&&) = delete;
void onReload(QObject* oldInstance) override;
virtual void setupWindow();
// Disown the backing window and delete all its children.
virtual QQuickWindow* disownWindow();
QQuickWindow* backingWindow();
QQuickItem* item();
[[nodiscard]] QQuickWindow* backingWindow() const;
virtual bool isVisible();
virtual void setVisible(bool value);
[[nodiscard]] virtual bool isVisible() const;
virtual void setVisible(bool visible);
virtual qint32 width();
virtual void setWidth(qint32 value);
[[nodiscard]] virtual qint32 width() const;
virtual void setWidth(qint32 width);
virtual qint32 height();
virtual void setHeight(qint32 value);
[[nodiscard]] virtual qint32 height() const;
virtual void setHeight(qint32 height);
QColor color();
void setColor(QColor value);
[[nodiscard]] QColor color() const;
void setColor(QColor color);
PendingRegion* mask();
[[nodiscard]] PendingRegion* mask() const;
void setMask(PendingRegion* mask);
QQmlListProperty<QObject> data();
signals:
void visibleChanged(bool visible);
void widthChanged(qint32 width);
void heightChanged(qint32 width);
void colorChanged(QColor color);
void windowConnected();
void visibleChanged();
void widthChanged();
void heightChanged();
void colorChanged();
void maskChanged();
private slots:
void onMaskChanged();
protected:
bool mVisible = false;
qint32 mWidth = 100;
qint32 mHeight = 100;
QColor mColor;
PendingRegion* mMask = nullptr;
QQuickWindow* window = nullptr;
private:
static QQmlListProperty<QObject> dataBacker(QQmlListProperty<QObject>* prop);
void updateMask();
QQmlListProperty<QObject> dataBacker();
static void dataAppend(QQmlListProperty<QObject>* prop, QObject* obj);
static qsizetype dataCount(QQmlListProperty<QObject>* prop);
static QObject* dataAt(QQmlListProperty<QObject>* prop, qsizetype i);
@ -154,7 +163,7 @@ private:
static void dataReplace(QQmlListProperty<QObject>* prop, qsizetype i, QObject* obj);
static void dataRemoveLast(QQmlListProperty<QObject>* prop);
PendingRegion* mMask = nullptr;
QVector<QObject*> pendingChildren;
};
// qt attempts to resize the window but fails because wayland
@ -164,12 +173,8 @@ class ProxyFloatingWindow: public ProxyWindowBase {
QML_ELEMENT;
public:
void earlyInit(QObject* old) override;
void componentComplete() override;
void setWidth(qint32 value) override;
void setHeight(qint32 value) override;
private:
bool geometryLocked = false;
// 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.
void setWidth(qint32 width) override;
void setHeight(qint32 height) override;
};

View file

@ -45,36 +45,9 @@ public:
/// `hard` - perform a hard reload. If this is false, QuickShell will attempt to reuse windows
/// that already exist. If true windows will be recreated.
///
/// > [!INFO] QuickShell can only reuse windows that are in a hierarchy of elements known
/// > internally as `Scavengeable`. These types are [ShellRoot] and [Variants].
/// >
/// > ```qml
/// > // this will reuse the window on reload
/// > ShellRoot {
/// > Varaints {
/// > ProxyShellWindow {
/// > // ...
/// > }
/// >
/// > // ...
/// > }
/// > }
/// >
/// > // this will NOT reuse the window on reload,
/// > // and will destroy the old one / create a new one every time
/// > ShellRoot {
/// > AnyNonScavengeableType {
/// > ProxyShellWindow {
/// > // ...
/// > }
/// >
/// > // ...
/// > }
/// > }
/// > ```
/// >
/// > [ShellRoot]: ../shellroot
/// > [Variants]: ../variants
/// See [Reloadable] for more information on what can be reloaded and how.
///
/// [Reloadable]: ../reloadable
Q_INVOKABLE void reload(bool hard);
signals:

77
src/cpp/reload.cpp Normal file
View file

@ -0,0 +1,77 @@
#include "reload.hpp"
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmllist.h>
void ReloadPropagator::onReload(QObject* oldInstance) {
auto* old = qobject_cast<ReloadPropagator*>(oldInstance);
for (auto i = 0; i < this->mChildren.length(); i++) {
auto* newChild = qobject_cast<Reloadable*>(this->mChildren.at(i));
if (newChild != nullptr) {
auto* oldChild = old == nullptr || old->mChildren.length() <= i
? nullptr
: qobject_cast<Reloadable*>(old->mChildren.at(i));
newChild->onReload(oldChild);
} else {
Reloadable::reloadRecursive(newChild, oldInstance);
}
}
}
QQmlListProperty<QObject> ReloadPropagator::data() {
return QQmlListProperty<QObject>(
this,
nullptr,
&ReloadPropagator::appendComponent,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr
);
}
void ReloadPropagator::appendComponent(QQmlListProperty<QObject>* list, QObject* obj) {
auto* self = static_cast<ReloadPropagator*>(list->object); // NOLINT
obj->setParent(self);
self->mChildren.append(obj);
}
void Reloadable::reloadRecursive(QObject* newObj, QObject* oldRoot) {
auto* reloadable = qobject_cast<Reloadable*>(newObj);
if (reloadable != nullptr) {
QObject* oldInstance = nullptr;
if (oldRoot != nullptr && !reloadable->mReloadableId.isEmpty()) {
oldInstance = Reloadable::getChildByReloadId(oldRoot, reloadable->mReloadableId);
}
// pass handling to the child's onReload, which should call back into reloadRecursive,
// with its oldInstance becoming the new oldRoot.
reloadable->onReload(oldInstance);
} else if (newObj != nullptr) {
Reloadable::reloadChildrenRecursive(newObj, oldRoot);
}
}
void Reloadable::reloadChildrenRecursive(QObject* newRoot, QObject* oldRoot) {
for (auto* child: newRoot->children()) {
Reloadable::reloadRecursive(child, oldRoot);
}
}
QObject* Reloadable::getChildByReloadId(QObject* parent, const QString& reloadId) {
for (auto* child: parent->children()) {
auto* reloadable = qobject_cast<Reloadable*>(child);
if (reloadable != nullptr) {
if (reloadable->mReloadableId == reloadId) return reloadable;
// if not then don't check its children as thats a seperate reload scope.
} else {
auto* reloadable = Reloadable::getChildByReloadId(child, reloadId);
if (reloadable != nullptr) return reloadable;
}
}
return nullptr;
}

107
src/cpp/reload.hpp Normal file
View file

@ -0,0 +1,107 @@
#pragma once
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qqmlparserstatus.h>
#include <qtmetamacros.h>
///! The base class of all types that can be reloaded.
/// Reloadables will attempt to take specific state from previous config revisions if possible.
/// Some examples are `ProxyShellWindow` and `ProxyFloatingWindow` which will attempt to find the
/// windows assigned to them in the previous configuration.
class Reloadable: public QObject, public QQmlParserStatus {
Q_OBJECT;
Q_INTERFACES(QQmlParserStatus);
/// An additional identifier that can be used to try to match a reloadable object to its
/// previous state.
///
/// Simply keeping a stable identifier across config versions (saves) is
/// enough to help the reloader figure out which object in the old revision corrosponds to
/// this object in the current revision, and facilitate smoother reloading.
///
/// Note that identifiers are scoped, and will try to do the right thing in context.
/// For example if you have a `Variants` wrapping an object with an identified element inside,
/// a scope is created at the variant level.
///
/// ```qml
/// Variants {
/// // multiple variants of the same object tree
/// variants: [ { foo: 1 }, { foo: 2 } ]
///
/// // any non `Reloadable` object
/// QtObject {
/// ProxyFloatingWindow {
/// // this ProxyFloatingWindow will now be matched to the same one in the previous
/// // widget tree for its variant. "myFloatingWindow" refers to both the variant in
/// // `foo: 1` and `foo: 2` for each tree.
/// reloadableId: "myFloatingWindow"
///
/// // ...
/// }
/// }
/// }
/// ```
Q_PROPERTY(QString reloadableId MEMBER mReloadableId);
QML_ELEMENT;
QML_UNCREATABLE(
"Reloadable is the base class of reloadable types and cannot be created on its own."
);
public:
explicit Reloadable(QObject* parent = nullptr): QObject(parent) {}
/// called unconditionally in the reload phase, with nullptr if no source could be determined
virtual void onReload(QObject* oldInstance) = 0;
// TODO: onReload runs after initialization for reloadable objects created late
void classBegin() override {}
void componentComplete() override {}
// Reload objects in the parent->child graph recursively.
static void reloadRecursive(QObject* newObj, QObject* oldRoot);
// Same as above but does not reload the passed object, only its children.
static void reloadChildrenRecursive(QObject* newRoot, QObject* oldRoot);
QString mReloadableId;
private:
static QObject* getChildByReloadId(QObject* parent, const QString& reloadId);
};
///! Basic type that propagates reloads to child items in order.
/// Convenience type equivalent to setting `reloadableId` on properties in a
/// QtObject instance.
///
/// Note that this does not work for visible `Item`s (all widgets).
///
/// ```qml
/// ShellRoot {
/// Variants {
/// variants: ...
///
/// ReloadPropagator {
/// // everything in here behaves the same as if it was defined
/// // directly in `Variants` reload-wise.
/// }
/// }
/// }
class ReloadPropagator: public Reloadable {
Q_OBJECT;
Q_PROPERTY(QQmlListProperty<QObject> children READ data);
Q_CLASSINFO("DefaultProperty", "children");
QML_ELEMENT;
public:
explicit ReloadPropagator(QObject* parent = nullptr): Reloadable(parent) {}
void onReload(QObject* oldInstance) override;
QQmlListProperty<QObject> data();
private:
static void appendComponent(QQmlListProperty<QObject>* list, QObject* obj);
QList<QObject*> mChildren;
};

View file

@ -9,7 +9,6 @@
#include <qqmlengine.h>
#include <qurl.h>
#include "scavenge.hpp"
#include "shell.hpp"
#include "watcher.hpp"
@ -23,8 +22,6 @@ RootWrapper::RootWrapper(QString rootPath):
}
}
QObject* RootWrapper::scavengeTargetFor(QObject* /* child */) { return this->root; }
void RootWrapper::reloadGraph(bool hard) {
if (this->root != nullptr) {
this->engine.clearComponentCache();
@ -32,9 +29,7 @@ void RootWrapper::reloadGraph(bool hard) {
auto component = QQmlComponent(&this->engine, QUrl::fromLocalFile(this->rootPath));
SCAVENGE_PARENT = hard ? nullptr : this;
auto* obj = component.beginCreate(this->engine.rootContext());
SCAVENGE_PARENT = nullptr;
if (obj == nullptr) {
qWarning() << component.errorString().toStdString().c_str();
@ -51,6 +46,8 @@ void RootWrapper::reloadGraph(bool hard) {
component.completeCreate();
newRoot->onReload(hard ? nullptr : this->root);
if (this->root != nullptr) {
this->root->deleteLater();
this->root = nullptr;

View file

@ -5,18 +5,15 @@
#include <qtmetamacros.h>
#include <qurl.h>
#include "scavenge.hpp"
#include "shell.hpp"
#include "watcher.hpp"
class RootWrapper: public QObject, virtual public Scavengeable {
class RootWrapper: public QObject {
Q_OBJECT;
public:
explicit RootWrapper(QString rootPath);
QObject* scavengeTargetFor(QObject* child) override;
void reloadGraph(bool hard);
private slots:

View file

@ -1,87 +0,0 @@
#include "scavenge.hpp"
#include <utility>
#include <qcontainerfwd.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlengine.h>
#include <qqmllist.h>
// FIXME: there are core problems with SCAVENGE_PARENT due to the qml engine liking to set parents really late.
// this should instead be handled by proxying all property values until a possible target is ready or definitely not coming.
// The parent should probably be stable in componentComplete() but should be tested.
QObject* SCAVENGE_PARENT = nullptr; // NOLINT
void Scavenger::classBegin() {
// prayers
if (this->parent() == nullptr) {
this->setParent(SCAVENGE_PARENT);
SCAVENGE_PARENT = nullptr;
}
auto* parent = dynamic_cast<Scavengeable*>(this->parent());
QObject* old = nullptr;
if (parent != nullptr) {
old = parent->scavengeTargetFor(this);
}
this->earlyInit(old);
}
QObject* createComponentScavengeable(
QObject& parent,
QQmlComponent& component,
QVariantMap& initialProperties
) {
SCAVENGE_PARENT = &parent;
auto* instance = component.beginCreate(QQmlEngine::contextForObject(&parent));
SCAVENGE_PARENT = nullptr;
if (instance == nullptr) return nullptr;
if (instance->parent() != nullptr) instance->setParent(&parent);
component.setInitialProperties(instance, initialProperties);
component.completeCreate();
if (instance == nullptr) {
qWarning() << component.errorString().toStdString().c_str();
}
return instance;
}
void ScavengeableScope::earlyInit(QObject* old) {
auto* oldshell = qobject_cast<ScavengeableScope*>(old);
if (oldshell != nullptr) {
this->scavengeableData = std::move(oldshell->mData);
}
}
QObject* ScavengeableScope::scavengeTargetFor(QObject* /* child */) {
if (this->scavengeableData.length() > this->mData.length()) {
return this->scavengeableData[this->mData.length()];
}
return nullptr;
}
QQmlListProperty<QObject> ScavengeableScope::data() {
return QQmlListProperty<QObject>(
this,
nullptr,
&ScavengeableScope::appendComponent,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr
);
}
void ScavengeableScope::appendComponent(QQmlListProperty<QObject>* list, QObject* component) {
auto* self = static_cast<ScavengeableScope*>(list->object); // NOLINT
component->setParent(self);
self->mData.append(component);
}

View file

@ -1,79 +0,0 @@
#pragma once
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qqmlparserstatus.h>
#include <qtmetamacros.h>
extern QObject* SCAVENGE_PARENT; // NOLINT
class Scavenger: public QObject, public QQmlParserStatus {
Q_OBJECT;
Q_INTERFACES(QQmlParserStatus);
public:
explicit Scavenger(QObject* parent = nullptr): QObject(parent) {}
~Scavenger() override = default;
Scavenger(Scavenger&) = delete;
Scavenger(Scavenger&&) = delete;
void operator=(Scavenger&) = delete;
void operator=(Scavenger&&) = delete;
void classBegin() override;
void componentComplete() override {}
protected:
// do early init, sometimes with a scavengeable target
virtual void earlyInit(QObject* old) = 0;
};
class Scavengeable {
public:
explicit Scavengeable() = default;
virtual ~Scavengeable() = default;
Scavengeable(Scavengeable&) = delete;
Scavengeable(Scavengeable&&) = delete;
void operator=(Scavengeable&) = delete;
void operator=(Scavengeable&&) = delete;
// return an old object that might have salvageable resources
virtual QObject* scavengeTargetFor(QObject* child) = 0;
};
QObject* createComponentScavengeable(
QObject& parent,
QQmlComponent& component,
QVariantMap& initialProperties
);
///! Reloader connection scope
/// Attempts to maintain scavengeable connections.
/// This is mostly useful to split a scavengeable component slot (e.g. `Variants`)
/// into multiple slots.
///
/// If you don't know what that means you probably don't need it.
class ScavengeableScope: public Scavenger, virtual public Scavengeable {
Q_OBJECT;
Q_PROPERTY(QQmlListProperty<QObject> data READ data);
Q_CLASSINFO("DefaultProperty", "data");
QML_ELEMENT;
public:
explicit ScavengeableScope(QObject* parent = nullptr): Scavenger(parent) {}
void earlyInit(QObject* old) override;
QObject* scavengeTargetFor(QObject* child) override;
QQmlListProperty<QObject> data();
private:
static void appendComponent(QQmlListProperty<QObject>* list, QObject* component);
// track only the children assigned to `data` in order
QList<QObject*> mData;
QList<QObject*> scavengeableData;
};

View file

@ -5,7 +5,7 @@
#include <qqmlengine.h>
#include <qtmetamacros.h>
#include "scavenge.hpp"
#include "reload.hpp"
class ShellConfig {
Q_GADGET;
@ -16,7 +16,7 @@ public:
};
///! Root config element
class ShellRoot: public ScavengeableScope {
class ShellRoot: public ReloadPropagator {
Q_OBJECT;
/// If `config.watchFiles` is true the configuration will be reloaded whenever it changes.
/// Defaults to true.
@ -24,7 +24,7 @@ class ShellRoot: public ScavengeableScope {
QML_ELEMENT;
public:
explicit ShellRoot(QObject* parent = nullptr): ScavengeableScope(parent) {}
explicit ShellRoot(QObject* parent = nullptr): ReloadPropagator(parent) {}
void setConfig(ShellConfig config);
[[nodiscard]] ShellConfig config() const;

View file

@ -5,31 +5,24 @@
#include <qcontainerfwd.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlengine.h>
#include "scavenge.hpp"
#include "reload.hpp"
void Variants::earlyInit(QObject* old) {
auto* oldv = qobject_cast<Variants*>(old);
if (oldv != nullptr) {
this->scavengeableInstances = std::move(oldv->instances);
}
}
void Variants::onReload(QObject* oldInstance) {
auto* old = qobject_cast<Variants*>(oldInstance);
QObject* Variants::scavengeTargetFor(QObject* /* child */) {
// Attempt to find the set that most closely matches the current one.
// This is biased to the order of the scavenge list which should help in
// case of conflicts as long as variants have not been reordered.
if (this->activeScavengeVariant != nullptr) {
auto& values = this->scavengeableInstances.values;
if (values.empty()) return nullptr;
for (auto& [variant, instanceObj]: this->instances.values) {
QObject* oldInstance = nullptr;
if (old != nullptr) {
auto& values = old->instances.values;
int matchcount = 0;
int matchi = 0;
int i = 0;
for (auto& [valueSet, _]: values) {
int count = 0;
for (auto& [k, v]: this->activeScavengeVariant->toStdMap()) {
for (auto& [k, v]: variant.toStdMap()) {
if (valueSet.contains(k) && valueSet.value(k) == v) {
count++;
}
@ -44,11 +37,15 @@ QObject* Variants::scavengeTargetFor(QObject* /* child */) {
}
if (matchcount > 0) {
return values.takeAt(matchi).second;
oldInstance = values.takeAt(matchi).second;
}
}
return nullptr;
auto* instance = qobject_cast<Reloadable*>(instanceObj);
if (instance != nullptr) instance->onReload(oldInstance);
else Reloadable::reloadChildrenRecursive(instanceObj, oldInstance);
}
}
void Variants::setVariants(QVariantList variants) {
@ -57,7 +54,7 @@ void Variants::setVariants(QVariantList variants) {
}
void Variants::componentComplete() {
this->Scavenger::componentComplete();
this->Reloadable::componentComplete();
this->updateVariants();
}
@ -96,14 +93,18 @@ void Variants::updateVariants() {
continue; // we dont need to recreate this one
}
this->activeScavengeVariant = &variant;
auto* instance = createComponentScavengeable(*this, *this->mComponent, variant);
auto* instance = this->mComponent->createWithInitialProperties(
variant,
QQmlEngine::contextForObject(this)
);
if (instance == nullptr) {
qWarning() << this->mComponent->errorString().toStdString().c_str();
qWarning() << "failed to create variant with object" << variant;
continue;
}
instance->setParent(this);
this->instances.insert(variant, instance);
}

View file

@ -7,8 +7,9 @@
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlparserstatus.h>
#include <qtmetamacros.h>
#include "scavenge.hpp"
#include "reload.hpp"
// extremely inefficient map
template <typename K, typename V>
@ -28,7 +29,7 @@ public:
/// screen.
///
/// [QuickShell.screens]: ../quickshell#prop.screens
class Variants: public Scavenger, virtual public Scavengeable {
class Variants: public Reloadable {
Q_OBJECT;
/// The component to create instances of
Q_PROPERTY(QQmlComponent* component MEMBER mComponent);
@ -39,10 +40,9 @@ class Variants: public Scavenger, virtual public Scavengeable {
QML_ELEMENT;
public:
explicit Variants(QObject* parent = nullptr): Scavenger(parent) {}
explicit Variants(QObject* parent = nullptr): Reloadable(parent) {}
void earlyInit(QObject* old) override;
QObject* scavengeTargetFor(QObject* child) override;
void onReload(QObject* oldInstance) override;
void componentComplete() override;
@ -53,8 +53,4 @@ private:
QQmlComponent* mComponent = nullptr;
QVariantList mVariants;
AwfulMap<QVariantMap, QObject*> instances;
// pointers may die post componentComplete.
AwfulMap<QVariantMap, QObject*> scavengeableInstances;
QVariantMap* activeScavengeVariant = nullptr;
};