diff --git a/.clang-tidy b/.clang-tidy index b53ebf3..6642fa7 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index db9fe59..590b4c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/cpp/layershell.cpp b/src/cpp/layershell.cpp index ed752ea..922f742 100644 --- a/src/cpp/layershell.cpp +++ b/src/cpp/layershell.cpp @@ -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(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,40 +129,49 @@ 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 (exclusionMode == ExclusionMode::Normal) { - this->shellWindow->setExclusiveZone(this->requestedExclusionZone); - emit this->exclusionZoneChanged(); - } else if (exclusionMode == ExclusionMode::Ignore) { - this->shellWindow->setExclusiveZone(-1); - emit this->exclusionZoneChanged(); - } else { - this->updateExclusionZone(); + if (this->window != nullptr) { + if (exclusionMode == ExclusionMode::Normal) { + this->shellWindow->setExclusiveZone(this->mExclusionZone); + emit this->exclusionZoneChanged(); + } else if (exclusionMode == ExclusionMode::Ignore) { + this->shellWindow->setExclusiveZone(-1); + emit this->exclusionZoneChanged(); + } else { + this->updateExclusionZone(); + } } } void ProxyShellWindow::setMargins(Margins margins) { - auto lsMargins = QMargins(margins.mLeft, margins.mTop, margins.mRight, margins.mBottom); - this->shellWindow->setMargins(lsMargins); + 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; diff --git a/src/cpp/layershell.hpp b/src/cpp/layershell.hpp index a7f53c5..597e75d 100644 --- a/src/cpp/layershell.hpp +++ b/src/cpp/layershell.hpp @@ -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 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; }; diff --git a/src/cpp/module.md b/src/cpp/module.md index c24bc8b..86faae2 100644 --- a/src/cpp/module.md +++ b/src/cpp/module.md @@ -3,7 +3,7 @@ description = "Core QuickShell types" headers = [ "qmlglobal.hpp", "qmlscreen.hpp", - "scavenge.hpp", + "reload.hpp", "shell.hpp", "variants.hpp", "proxywindow.hpp", diff --git a/src/cpp/proxywindow.cpp b/src/cpp/proxywindow.cpp index 96f1928..6e17a55 100644 --- a/src/cpp/proxywindow.cpp +++ b/src/cpp/proxywindow.cpp @@ -17,17 +17,29 @@ ProxyWindowBase::~ProxyWindowBase() { } } -void ProxyWindowBase::earlyInit(QObject* old) { - auto* oldpw = qobject_cast(old); +void ProxyWindowBase::onReload(QObject* oldInstance) { + auto* old = qobject_cast(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 ProxyWindowBase::data() { ); } -QQmlListProperty ProxyWindowBase::dataBacker(QQmlListProperty* prop) { - auto* that = static_cast(prop->object); // NOLINT - return that->window->property("data").value>(); +QQmlListProperty ProxyWindowBase::dataBacker() { + return this->window->property("data").value>(); } void ProxyWindowBase::dataAppend(QQmlListProperty* prop, QObject* obj) { - auto backer = dataBacker(prop); - backer.append(&backer, obj); + auto* self = static_cast(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* prop) { - auto backer = dataBacker(prop); - return backer.count(&backer); + auto* self = static_cast(prop->object); // NOLINT + + if (self->window == nullptr) { + return self->pendingChildren.count(); + } else { + auto backer = self->dataBacker(); + return backer.count(&backer); + } } QObject* ProxyWindowBase::dataAt(QQmlListProperty* prop, qsizetype i) { - auto backer = dataBacker(prop); - return backer.at(&backer, i); + auto* self = static_cast(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* prop) { - auto backer = dataBacker(prop); - backer.clear(&backer); + auto* self = static_cast(prop->object); // NOLINT + + if (self->window == nullptr) { + self->pendingChildren.clear(); + } else { + auto backer = self->dataBacker(); + backer.clear(&backer); + } } void ProxyWindowBase::dataReplace(QQmlListProperty* prop, qsizetype i, QObject* obj) { - auto backer = dataBacker(prop); - backer.replace(&backer, i, obj); + auto* self = static_cast(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* prop) { - auto backer = dataBacker(prop); - backer.removeLast(&backer); -} + auto* self = static_cast(prop->object); // NOLINT -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); + if (self->window == nullptr) { + self->pendingChildren.removeLast(); + } else { + auto backer = self->dataBacker(); + backer.removeLast(&backer); } } -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); } diff --git a/src/cpp/proxywindow.hpp b/src/cpp/proxywindow.hpp index 27ab57b..b1f9443 100644 --- a/src/cpp/proxywindow.hpp +++ b/src/cpp/proxywindow.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -11,7 +12,7 @@ #include #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 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 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 dataBacker(QQmlListProperty* prop); + void updateMask(); + QQmlListProperty dataBacker(); + static void dataAppend(QQmlListProperty* prop, QObject* obj); static qsizetype dataCount(QQmlListProperty* prop); static QObject* dataAt(QQmlListProperty* prop, qsizetype i); @@ -154,7 +163,7 @@ private: static void dataReplace(QQmlListProperty* prop, qsizetype i, QObject* obj); static void dataRemoveLast(QQmlListProperty* prop); - PendingRegion* mMask = nullptr; + QVector 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; }; diff --git a/src/cpp/qmlglobal.hpp b/src/cpp/qmlglobal.hpp index 03009d5..f0e964a 100644 --- a/src/cpp/qmlglobal.hpp +++ b/src/cpp/qmlglobal.hpp @@ -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: diff --git a/src/cpp/reload.cpp b/src/cpp/reload.cpp new file mode 100644 index 0000000..187fc7a --- /dev/null +++ b/src/cpp/reload.cpp @@ -0,0 +1,77 @@ +#include "reload.hpp" + +#include +#include +#include + +void ReloadPropagator::onReload(QObject* oldInstance) { + auto* old = qobject_cast(oldInstance); + + for (auto i = 0; i < this->mChildren.length(); i++) { + auto* newChild = qobject_cast(this->mChildren.at(i)); + if (newChild != nullptr) { + auto* oldChild = old == nullptr || old->mChildren.length() <= i + ? nullptr + : qobject_cast(old->mChildren.at(i)); + newChild->onReload(oldChild); + } else { + Reloadable::reloadRecursive(newChild, oldInstance); + } + } +} + +QQmlListProperty ReloadPropagator::data() { + return QQmlListProperty( + this, + nullptr, + &ReloadPropagator::appendComponent, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr + ); +} + +void ReloadPropagator::appendComponent(QQmlListProperty* list, QObject* obj) { + auto* self = static_cast(list->object); // NOLINT + obj->setParent(self); + self->mChildren.append(obj); +} + +void Reloadable::reloadRecursive(QObject* newObj, QObject* oldRoot) { + auto* reloadable = qobject_cast(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(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; +} diff --git a/src/cpp/reload.hpp b/src/cpp/reload.hpp new file mode 100644 index 0000000..fc70184 --- /dev/null +++ b/src/cpp/reload.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +///! 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 children READ data); + Q_CLASSINFO("DefaultProperty", "children"); + QML_ELEMENT; + +public: + explicit ReloadPropagator(QObject* parent = nullptr): Reloadable(parent) {} + + void onReload(QObject* oldInstance) override; + + QQmlListProperty data(); + +private: + static void appendComponent(QQmlListProperty* list, QObject* obj); + + QList mChildren; +}; diff --git a/src/cpp/rootwrapper.cpp b/src/cpp/rootwrapper.cpp index 886e2ae..17dd549 100644 --- a/src/cpp/rootwrapper.cpp +++ b/src/cpp/rootwrapper.cpp @@ -9,7 +9,6 @@ #include #include -#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; diff --git a/src/cpp/rootwrapper.hpp b/src/cpp/rootwrapper.hpp index 2f46304..534a766 100644 --- a/src/cpp/rootwrapper.hpp +++ b/src/cpp/rootwrapper.hpp @@ -5,18 +5,15 @@ #include #include -#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: diff --git a/src/cpp/scavenge.cpp b/src/cpp/scavenge.cpp deleted file mode 100644 index 2ad7769..0000000 --- a/src/cpp/scavenge.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include "scavenge.hpp" -#include - -#include -#include -#include -#include -#include -#include - -// 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(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(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 ScavengeableScope::data() { - return QQmlListProperty( - this, - nullptr, - &ScavengeableScope::appendComponent, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr - ); -} - -void ScavengeableScope::appendComponent(QQmlListProperty* list, QObject* component) { - auto* self = static_cast(list->object); // NOLINT - component->setParent(self); - self->mData.append(component); -} diff --git a/src/cpp/scavenge.hpp b/src/cpp/scavenge.hpp deleted file mode 100644 index 6e9e6d0..0000000 --- a/src/cpp/scavenge.hpp +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -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 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 data(); - -private: - static void appendComponent(QQmlListProperty* list, QObject* component); - - // track only the children assigned to `data` in order - QList mData; - QList scavengeableData; -}; diff --git a/src/cpp/shell.hpp b/src/cpp/shell.hpp index cc2862c..318be18 100644 --- a/src/cpp/shell.hpp +++ b/src/cpp/shell.hpp @@ -5,7 +5,7 @@ #include #include -#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; diff --git a/src/cpp/variants.cpp b/src/cpp/variants.cpp index a02740f..f984f6d 100644 --- a/src/cpp/variants.cpp +++ b/src/cpp/variants.cpp @@ -5,50 +5,47 @@ #include #include #include +#include -#include "scavenge.hpp" +#include "reload.hpp" -void Variants::earlyInit(QObject* old) { - auto* oldv = qobject_cast(old); - if (oldv != nullptr) { - this->scavengeableInstances = std::move(oldv->instances); - } -} +void Variants::onReload(QObject* oldInstance) { + auto* old = qobject_cast(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. + for (auto& [variant, instanceObj]: this->instances.values) { + QObject* oldInstance = nullptr; + if (old != nullptr) { + auto& values = old->instances.values; - if (this->activeScavengeVariant != nullptr) { - auto& values = this->scavengeableInstances.values; - if (values.empty()) return nullptr; - - int matchcount = 0; - int matchi = 0; - int i = 0; - for (auto& [valueSet, _]: values) { - int count = 0; - for (auto& [k, v]: this->activeScavengeVariant->toStdMap()) { - if (valueSet.contains(k) && valueSet.value(k) == v) { - count++; + int matchcount = 0; + int matchi = 0; + int i = 0; + for (auto& [valueSet, _]: values) { + int count = 0; + for (auto& [k, v]: variant.toStdMap()) { + if (valueSet.contains(k) && valueSet.value(k) == v) { + count++; + } } + + if (count > matchcount) { + matchcount = count; + matchi = i; + } + + i++; } - if (count > matchcount) { - matchcount = count; - matchi = i; + if (matchcount > 0) { + oldInstance = values.takeAt(matchi).second; } - - i++; } - if (matchcount > 0) { - return values.takeAt(matchi).second; - } + auto* instance = qobject_cast(instanceObj); + + if (instance != nullptr) instance->onReload(oldInstance); + else Reloadable::reloadChildrenRecursive(instanceObj, oldInstance); } - - return nullptr; } 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); } diff --git a/src/cpp/variants.hpp b/src/cpp/variants.hpp index 9526538..6b07663 100644 --- a/src/cpp/variants.hpp +++ b/src/cpp/variants.hpp @@ -7,8 +7,9 @@ #include #include #include +#include -#include "scavenge.hpp" +#include "reload.hpp" // extremely inefficient map template @@ -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 instances; - - // pointers may die post componentComplete. - AwfulMap scavengeableInstances; - QVariantMap* activeScavengeVariant = nullptr; };