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-uppercase-literal-suffix,
-readability-braces-around-statements, -readability-braces-around-statements,
-readability-redundant-access-specifiers, -readability-redundant-access-specifiers,
-readability-else-after-return,
tidyfox-*, tidyfox-*,
CheckOptions: CheckOptions:
performance-for-range-copy.WarnOnAllAutoCopies: true performance-for-range-copy.WarnOnAllAutoCopies: true

View File

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

View File

@ -14,18 +14,11 @@
#include "proxywindow.hpp" #include "proxywindow.hpp"
#include "qmlscreen.hpp" #include "qmlscreen.hpp"
void ProxyShellWindow::earlyInit(QObject* old) { void ProxyShellWindow::setupWindow() {
this->ProxyWindowBase::earlyInit(old);
QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyShellWindow::screenChanged); QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyShellWindow::screenChanged);
this->shellWindow = LayerShellQt::Window::get(this->window); this->shellWindow = LayerShellQt::Window::get(this->window);
// dont want to steal focus unless actually configured to this->ProxyWindowBase::setupWindow();
this->shellWindow->setKeyboardInteractivity(LayerShellQt::Window::KeyboardInteractivityNone);
// this dosent reset if its unset
this->shellWindow->setExclusiveZone(0);
// clang-format off // clang-format off
QObject::connect(this->shellWindow, &LayerShellQt::Window::anchorsChanged, this, &ProxyShellWindow::anchorsChanged); 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::anchorsChanged, this, &ProxyShellWindow::updateExclusionZone);
QObject::connect(this, &ProxyShellWindow::marginsChanged, this, &ProxyShellWindow::updateExclusionZone); QObject::connect(this, &ProxyShellWindow::marginsChanged, this, &ProxyShellWindow::updateExclusionZone);
// clang-format on // clang-format on
}
void ProxyShellWindow::componentComplete() { this->window->setScreen(this->mScreen);
this->complete = true; 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. this->connected = true;
// 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();
} }
QQuickWindow* ProxyShellWindow::disownWindow() { QQuickWindow* ProxyShellWindow::disownWindow() {
@ -65,54 +48,56 @@ QQuickWindow* ProxyShellWindow::disownWindow() {
return this->ProxyWindowBase::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) { void ProxyShellWindow::setWidth(qint32 width) {
this->requestedWidth = width; this->mWidth = width;
// only update the actual size if not blocked by anchors // only update the actual size if not blocked by anchors
auto anchors = this->anchors(); auto anchors = this->anchors();
if (this->complete && (!anchors.mLeft || !anchors.mRight)) this->ProxyWindowBase::setWidth(width); if (!anchors.mLeft || !anchors.mRight) this->ProxyWindowBase::setWidth(width);
}
qint32 ProxyShellWindow::width() {
return this->complete ? this->ProxyWindowBase::width() : this->requestedWidth;
} }
void ProxyShellWindow::setHeight(qint32 height) { void ProxyShellWindow::setHeight(qint32 height) {
this->requestedHeight = height; this->mHeight = height;
// only update the actual size if not blocked by anchors // only update the actual size if not blocked by anchors
auto anchors = this->anchors(); auto anchors = this->anchors();
if (this->complete && (!anchors.mTop || !anchors.mBottom)) if (!anchors.mTop || !anchors.mBottom) this->ProxyWindowBase::setHeight(height);
this->ProxyWindowBase::setHeight(height);
}
qint32 ProxyShellWindow::height() {
return this->complete ? this->ProxyWindowBase::height() : this->requestedHeight;
} }
void ProxyShellWindow::setScreen(QuickShellScreenInfo* screen) { 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 { 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( return new QuickShellScreenInfo(
const_cast<ProxyShellWindow*>(this), // NOLINT const_cast<ProxyShellWindow*>(this), // NOLINT
this->window->screen() qscreen
); );
} }
void ProxyShellWindow::setAnchors(Anchors anchors) { void ProxyShellWindow::setAnchors(Anchors anchors) {
if (!this->complete) { if (this->window == nullptr) {
this->stagingAnchors = anchors; this->mAnchors = anchors;
return; return;
} }
@ -122,14 +107,14 @@ void ProxyShellWindow::setAnchors(Anchors anchors) {
if (anchors.mTop) lsAnchors |= LayerShellQt::Window::AnchorTop; if (anchors.mTop) lsAnchors |= LayerShellQt::Window::AnchorTop;
if (anchors.mBottom) lsAnchors |= LayerShellQt::Window::AnchorBottom; if (anchors.mBottom) lsAnchors |= LayerShellQt::Window::AnchorBottom;
if (!anchors.mLeft || !anchors.mRight) this->ProxyWindowBase::setWidth(this->requestedWidth); if (!anchors.mLeft || !anchors.mRight) this->ProxyWindowBase::setWidth(this->mWidth);
if (!anchors.mTop || !anchors.mBottom) this->ProxyWindowBase::setHeight(this->requestedHeight); if (!anchors.mTop || !anchors.mBottom) this->ProxyWindowBase::setHeight(this->mHeight);
this->shellWindow->setAnchors(lsAnchors); this->shellWindow->setAnchors(lsAnchors);
} }
Anchors ProxyShellWindow::anchors() const { Anchors ProxyShellWindow::anchors() const {
if (!this->complete) return this->stagingAnchors; if (this->window == nullptr) return this->mAnchors;
auto lsAnchors = this->shellWindow->anchors(); auto lsAnchors = this->shellWindow->anchors();
@ -144,40 +129,49 @@ Anchors ProxyShellWindow::anchors() const {
void ProxyShellWindow::setExclusiveZone(qint32 zone) { void ProxyShellWindow::setExclusiveZone(qint32 zone) {
if (zone < 0) zone = 0; if (zone < 0) zone = 0;
if (zone == this->requestedExclusionZone) return; if (this->connected && zone == this->mExclusionZone) return;
this->requestedExclusionZone = zone; this->mExclusionZone = zone;
if (this->exclusionMode() == ExclusionMode::Normal) { if (this->window != nullptr && this->exclusionMode() == ExclusionMode::Normal) {
this->shellWindow->setExclusiveZone(zone); this->shellWindow->setExclusiveZone(zone);
emit this->exclusionZoneChanged(); 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; } ExclusionMode::Enum ProxyShellWindow::exclusionMode() const { return this->mExclusionMode; }
void ProxyShellWindow::setExclusionMode(ExclusionMode::Enum exclusionMode) { void ProxyShellWindow::setExclusionMode(ExclusionMode::Enum exclusionMode) {
if (exclusionMode == this->mExclusionMode) return; if (this->connected && exclusionMode == this->mExclusionMode) return;
this->mExclusionMode = exclusionMode; this->mExclusionMode = exclusionMode;
if (exclusionMode == ExclusionMode::Normal) { if (this->window != nullptr) {
this->shellWindow->setExclusiveZone(this->requestedExclusionZone); if (exclusionMode == ExclusionMode::Normal) {
emit this->exclusionZoneChanged(); this->shellWindow->setExclusiveZone(this->mExclusionZone);
} else if (exclusionMode == ExclusionMode::Ignore) { emit this->exclusionZoneChanged();
this->shellWindow->setExclusiveZone(-1); } else if (exclusionMode == ExclusionMode::Ignore) {
emit this->exclusionZoneChanged(); this->shellWindow->setExclusiveZone(-1);
} else { emit this->exclusionZoneChanged();
this->updateExclusionZone(); } else {
this->updateExclusionZone();
}
} }
} }
void ProxyShellWindow::setMargins(Margins margins) { void ProxyShellWindow::setMargins(Margins margins) {
auto lsMargins = QMargins(margins.mLeft, margins.mTop, margins.mRight, margins.mBottom); if (this->window == nullptr) this->mMargins = margins;
this->shellWindow->setMargins(lsMargins); else {
auto lsMargins = QMargins(margins.mLeft, margins.mTop, margins.mRight, margins.mBottom);
this->shellWindow->setMargins(lsMargins);
}
} }
Margins ProxyShellWindow::margins() const { Margins ProxyShellWindow::margins() const {
if (this->window == nullptr) return this->mMargins;
auto lsMargins = this->shellWindow->margins(); auto lsMargins = this->shellWindow->margins();
auto margins = Margins(); auto margins = Margins();
@ -190,6 +184,11 @@ Margins ProxyShellWindow::margins() const {
} }
void ProxyShellWindow::setLayer(Layer::Enum layer) { void ProxyShellWindow::setLayer(Layer::Enum layer) {
if (this->window == nullptr) {
this->mLayer = layer;
return;
}
auto lsLayer = LayerShellQt::Window::LayerBackground; auto lsLayer = LayerShellQt::Window::LayerBackground;
// clang-format off // clang-format off
@ -205,6 +204,8 @@ void ProxyShellWindow::setLayer(Layer::Enum layer) {
} }
Layer::Enum ProxyShellWindow::layer() const { Layer::Enum ProxyShellWindow::layer() const {
if (this->window == nullptr) return this->mLayer;
auto layer = Layer::Top; auto layer = Layer::Top;
auto lsLayer = this->shellWindow->layer(); auto lsLayer = this->shellWindow->layer();
@ -220,11 +221,22 @@ Layer::Enum ProxyShellWindow::layer() const {
return layer; 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) { void ProxyShellWindow::setKeyboardFocus(KeyboardFocus::Enum focus) {
if (this->window == nullptr) {
this->mKeyboardFocus = focus;
return;
}
auto lsFocus = LayerShellQt::Window::KeyboardInteractivityNone; auto lsFocus = LayerShellQt::Window::KeyboardInteractivityNone;
// clang-format off // clang-format off
@ -239,6 +251,8 @@ void ProxyShellWindow::setKeyboardFocus(KeyboardFocus::Enum focus) {
} }
KeyboardFocus::Enum ProxyShellWindow::keyboardFocus() const { KeyboardFocus::Enum ProxyShellWindow::keyboardFocus() const {
if (this->window == nullptr) return this->mKeyboardFocus;
auto focus = KeyboardFocus::None; auto focus = KeyboardFocus::None;
auto lsFocus = this->shellWindow->keyboardInteractivity(); auto lsFocus = this->shellWindow->keyboardInteractivity();
@ -254,6 +268,11 @@ KeyboardFocus::Enum ProxyShellWindow::keyboardFocus() const {
} }
void ProxyShellWindow::setScreenConfiguration(ScreenConfiguration::Enum configuration) { void ProxyShellWindow::setScreenConfiguration(ScreenConfiguration::Enum configuration) {
if (this->window == nullptr) {
this->mScreenConfiguration = configuration;
return;
}
auto lsConfiguration = LayerShellQt::Window::ScreenFromQWindow; auto lsConfiguration = LayerShellQt::Window::ScreenFromQWindow;
// clang-format off // clang-format off
@ -267,6 +286,8 @@ void ProxyShellWindow::setScreenConfiguration(ScreenConfiguration::Enum configur
} }
ScreenConfiguration::Enum ProxyShellWindow::screenConfiguration() const { ScreenConfiguration::Enum ProxyShellWindow::screenConfiguration() const {
if (this->window == nullptr) return this->mScreenConfiguration;
auto configuration = ScreenConfiguration::Window; auto configuration = ScreenConfiguration::Window;
auto lsConfiguration = this->shellWindow->screenConfiguration(); auto lsConfiguration = this->shellWindow->screenConfiguration();
@ -280,14 +301,8 @@ ScreenConfiguration::Enum ProxyShellWindow::screenConfiguration() const {
return configuration; return configuration;
} }
void ProxyShellWindow::setCloseOnDismissed(bool close) {
this->shellWindow->setCloseOnDismissed(close);
}
bool ProxyShellWindow::closeOnDismissed() const { return this->shellWindow->closeOnDismissed(); }
void ProxyShellWindow::updateExclusionZone() { void ProxyShellWindow::updateExclusionZone() {
if (this->exclusionMode() == ExclusionMode::Auto) { if (this->window != nullptr && this->exclusionMode() == ExclusionMode::Auto) {
auto anchors = this->anchors(); auto anchors = this->anchors();
auto zone = -1; auto zone = -1;

View File

@ -162,27 +162,18 @@ class ProxyShellWindow: public ProxyWindowBase {
/// The degree of keyboard focus taken. Defaults to `KeyboardFocus.None`. /// The degree of keyboard focus taken. Defaults to `KeyboardFocus.None`.
Q_PROPERTY(KeyboardFocus::Enum keyboardFocus READ keyboardFocus WRITE setKeyboardFocus NOTIFY keyboardFocusChanged); Q_PROPERTY(KeyboardFocus::Enum keyboardFocus READ keyboardFocus WRITE setKeyboardFocus NOTIFY keyboardFocusChanged);
Q_PROPERTY(ScreenConfiguration::Enum screenConfiguration READ screenConfiguration WRITE setScreenConfiguration); Q_PROPERTY(ScreenConfiguration::Enum screenConfiguration READ screenConfiguration WRITE setScreenConfiguration);
Q_PROPERTY(bool closeOnDismissed READ closeOnDismissed WRITE setCloseOnDismissed);
QML_ELEMENT; QML_ELEMENT;
// clang-format on // clang-format on
protected:
void earlyInit(QObject* old) override;
public: public:
void componentComplete() override; void setupWindow() override;
QQuickWindow* disownWindow() override; QQuickWindow* disownWindow() override;
QQmlListProperty<QObject> data(); QQmlListProperty<QObject> data();
void setVisible(bool visible) override;
bool isVisible() override;
void setWidth(qint32 width) override; void setWidth(qint32 width) override;
qint32 width() override;
void setHeight(qint32 height) override; void setHeight(qint32 height) override;
qint32 height() override;
void setScreen(QuickShellScreenInfo* screen); void setScreen(QuickShellScreenInfo* screen);
[[nodiscard]] QuickShellScreenInfo* screen() const; [[nodiscard]] QuickShellScreenInfo* screen() const;
@ -211,9 +202,6 @@ public:
void setScreenConfiguration(ScreenConfiguration::Enum configuration); void setScreenConfiguration(ScreenConfiguration::Enum configuration);
[[nodiscard]] ScreenConfiguration::Enum screenConfiguration() const; [[nodiscard]] ScreenConfiguration::Enum screenConfiguration() const;
void setCloseOnDismissed(bool close);
[[nodiscard]] bool closeOnDismissed() const;
signals: signals:
void screenChanged(); void screenChanged();
void anchorsChanged(); void anchorsChanged();
@ -225,20 +213,19 @@ signals:
private slots: private slots:
void updateExclusionZone(); void updateExclusionZone();
void onScreenDestroyed();
private: private:
LayerShellQt::Window* shellWindow = nullptr; LayerShellQt::Window* shellWindow = nullptr;
bool anchorsInitialized = false; QScreen* mScreen = nullptr;
ExclusionMode::Enum mExclusionMode = ExclusionMode::Normal; 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 bool connected = false;
// 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;
}; };

View File

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

View File

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

View File

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

View File

@ -45,36 +45,9 @@ public:
/// `hard` - perform a hard reload. If this is false, QuickShell will attempt to reuse windows /// `hard` - perform a hard reload. If this is false, QuickShell will attempt to reuse windows
/// that already exist. If true windows will be recreated. /// that already exist. If true windows will be recreated.
/// ///
/// > [!INFO] QuickShell can only reuse windows that are in a hierarchy of elements known /// See [Reloadable] for more information on what can be reloaded and how.
/// > internally as `Scavengeable`. These types are [ShellRoot] and [Variants]. ///
/// > /// [Reloadable]: ../reloadable
/// > ```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
Q_INVOKABLE void reload(bool hard); Q_INVOKABLE void reload(bool hard);
signals: 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 <qqmlengine.h>
#include <qurl.h> #include <qurl.h>
#include "scavenge.hpp"
#include "shell.hpp" #include "shell.hpp"
#include "watcher.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) { void RootWrapper::reloadGraph(bool hard) {
if (this->root != nullptr) { if (this->root != nullptr) {
this->engine.clearComponentCache(); this->engine.clearComponentCache();
@ -32,9 +29,7 @@ void RootWrapper::reloadGraph(bool hard) {
auto component = QQmlComponent(&this->engine, QUrl::fromLocalFile(this->rootPath)); auto component = QQmlComponent(&this->engine, QUrl::fromLocalFile(this->rootPath));
SCAVENGE_PARENT = hard ? nullptr : this;
auto* obj = component.beginCreate(this->engine.rootContext()); auto* obj = component.beginCreate(this->engine.rootContext());
SCAVENGE_PARENT = nullptr;
if (obj == nullptr) { if (obj == nullptr) {
qWarning() << component.errorString().toStdString().c_str(); qWarning() << component.errorString().toStdString().c_str();
@ -51,6 +46,8 @@ void RootWrapper::reloadGraph(bool hard) {
component.completeCreate(); component.completeCreate();
newRoot->onReload(hard ? nullptr : this->root);
if (this->root != nullptr) { if (this->root != nullptr) {
this->root->deleteLater(); this->root->deleteLater();
this->root = nullptr; this->root = nullptr;

View File

@ -5,18 +5,15 @@
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qurl.h> #include <qurl.h>
#include "scavenge.hpp"
#include "shell.hpp" #include "shell.hpp"
#include "watcher.hpp" #include "watcher.hpp"
class RootWrapper: public QObject, virtual public Scavengeable { class RootWrapper: public QObject {
Q_OBJECT; Q_OBJECT;
public: public:
explicit RootWrapper(QString rootPath); explicit RootWrapper(QString rootPath);
QObject* scavengeTargetFor(QObject* child) override;
void reloadGraph(bool hard); void reloadGraph(bool hard);
private slots: 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 <qqmlengine.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include "scavenge.hpp" #include "reload.hpp"
class ShellConfig { class ShellConfig {
Q_GADGET; Q_GADGET;
@ -16,7 +16,7 @@ public:
}; };
///! Root config element ///! Root config element
class ShellRoot: public ScavengeableScope { class ShellRoot: public ReloadPropagator {
Q_OBJECT; Q_OBJECT;
/// If `config.watchFiles` is true the configuration will be reloaded whenever it changes. /// If `config.watchFiles` is true the configuration will be reloaded whenever it changes.
/// Defaults to true. /// Defaults to true.
@ -24,7 +24,7 @@ class ShellRoot: public ScavengeableScope {
QML_ELEMENT; QML_ELEMENT;
public: public:
explicit ShellRoot(QObject* parent = nullptr): ScavengeableScope(parent) {} explicit ShellRoot(QObject* parent = nullptr): ReloadPropagator(parent) {}
void setConfig(ShellConfig config); void setConfig(ShellConfig config);
[[nodiscard]] ShellConfig config() const; [[nodiscard]] ShellConfig config() const;

View File

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

View File

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