quickshell/src/window/proxywindow.cpp
outfoxxed 611cd76abc
core/proxywindow: connect mScreen's destroy signal in all cases
Fixes edge case crashes when unplugging and replugging monitors.
2024-12-19 03:34:07 -08:00

404 lines
13 KiB
C++

#include "proxywindow.hpp"
#include <private/qquickwindow_p.h>
#include <qevent.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmllist.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qregion.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#include <qwindow.h>
#include "../core/generation.hpp"
#include "../core/qmlglobal.hpp"
#include "../core/qmlscreen.hpp"
#include "../core/region.hpp"
#include "../core/reload.hpp"
#include "../debug/lint.hpp"
#include "windowinterface.hpp"
ProxyWindowBase::ProxyWindowBase(QObject* parent)
: Reloadable(parent)
, mContentItem(new QQuickItem()) {
QQmlEngine::setObjectOwnership(this->mContentItem, QQmlEngine::CppOwnership);
this->mContentItem->setParent(this);
// clang-format off
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onWidthChanged);
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onHeightChanged);
QObject::connect(this, &ProxyWindowBase::maskChanged, this, &ProxyWindowBase::onMaskChanged);
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onMaskChanged);
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onMaskChanged);
QObject::connect(this, &ProxyWindowBase::xChanged, this, &ProxyWindowBase::windowTransformChanged);
QObject::connect(this, &ProxyWindowBase::yChanged, this, &ProxyWindowBase::windowTransformChanged);
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::windowTransformChanged);
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::windowTransformChanged);
QObject::connect(this, &ProxyWindowBase::backerVisibilityChanged, this, &ProxyWindowBase::windowTransformChanged);
// clang-format on
}
ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(true); }
void ProxyWindowBase::onReload(QObject* oldInstance) {
this->window = this->retrieveWindow(oldInstance);
auto wasVisible = this->window != nullptr && this->window->isVisible();
if (this->window == nullptr) this->window = this->createQQuickWindow();
// The qml engine will leave the WindowInterface as owner of everything
// nested in an item, so we have to make sure the interface's children
// are also reloaded.
// Reparenting from the interface does not work reliably, so instead
// we check if the parent is one, as it proxies reloads to here.
if (auto* w = qobject_cast<WindowInterface*>(this->parent())) {
for (auto* child: w->children()) {
if (child == this) continue;
auto* oldInterfaceParent = oldInstance == nullptr ? nullptr : oldInstance->parent();
Reloadable::reloadRecursive(child, oldInterfaceParent);
}
}
Reloadable::reloadChildrenRecursive(this, oldInstance);
this->connectWindow();
this->completeWindow();
this->reloadComplete = true;
emit this->windowConnected();
this->postCompleteWindow();
if (wasVisible && this->isVisibleDirect()) {
emit this->backerVisibilityChanged();
this->runLints();
}
}
void ProxyWindowBase::postCompleteWindow() { this->setVisible(this->mVisible); }
ProxiedWindow* ProxyWindowBase::createQQuickWindow() { return new ProxiedWindow(this); }
void ProxyWindowBase::createWindow() {
if (this->window != nullptr) return;
this->window = this->createQQuickWindow();
this->connectWindow();
this->completeWindow();
emit this->windowConnected();
}
void ProxyWindowBase::deleteWindow(bool keepItemOwnership) {
if (this->window != nullptr) emit this->windowDestroyed();
if (auto* window = this->disownWindow(keepItemOwnership)) {
if (auto* generation = EngineGeneration::findObjectGeneration(this)) {
generation->deregisterIncubationController(window->incubationController());
}
window->deleteLater();
}
}
ProxiedWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) {
if (this->window == nullptr) return nullptr;
QObject::disconnect(this->window, nullptr, this, nullptr);
if (!keepItemOwnership) {
this->mContentItem->setParentItem(nullptr);
}
auto* window = this->window;
this->window = nullptr;
return window;
}
ProxiedWindow* ProxyWindowBase::retrieveWindow(QObject* oldInstance) {
auto* old = qobject_cast<ProxyWindowBase*>(oldInstance);
return old == nullptr ? nullptr : old->disownWindow();
}
void ProxyWindowBase::connectWindow() {
if (auto* generation = EngineGeneration::findObjectGeneration(this)) {
// All windows have effectively the same incubation controller so it dosen't matter
// which window it belongs to. We do want to replace the delay one though.
generation->registerIncubationController(this->window->incubationController());
}
this->window->setProxy(this);
// clang-format off
QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged);
QObject::connect(this->window, &QWindow::xChanged, this, &ProxyWindowBase::xChanged);
QObject::connect(this->window, &QWindow::yChanged, this, &ProxyWindowBase::yChanged);
QObject::connect(this->window, &QWindow::widthChanged, this, &ProxyWindowBase::widthChanged);
QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyWindowBase::heightChanged);
QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyWindowBase::screenChanged);
QObject::connect(this->window, &QQuickWindow::colorChanged, this, &ProxyWindowBase::colorChanged);
QObject::connect(this->window, &ProxiedWindow::exposed, this, &ProxyWindowBase::runLints);
// clang-format on
}
void ProxyWindowBase::completeWindow() {
if (this->mScreen != nullptr && this->window->screen() != this->mScreen) {
if (this->window->isVisible()) this->window->setVisible(false);
this->window->setScreen(this->mScreen);
} else if (this->mScreen == nullptr) {
this->mScreen = this->window->screen();
QObject::connect(this->mScreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed);
}
this->setWidth(this->mWidth);
this->setHeight(this->mHeight);
this->setColor(this->mColor);
this->updateMask();
// notify initial / post-connection geometry
emit this->xChanged();
emit this->yChanged();
emit this->widthChanged();
emit this->heightChanged();
this->mContentItem->setParentItem(this->window->contentItem());
this->mContentItem->setWidth(this->width());
this->mContentItem->setHeight(this->height());
// without this the dangling screen pointer wont be updated to a real screen
emit this->screenChanged();
}
bool ProxyWindowBase::deleteOnInvisible() const { return false; }
QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; }
QQuickItem* ProxyWindowBase::contentItem() const { return this->mContentItem; }
bool ProxyWindowBase::isVisible() const {
if (this->window == nullptr) return this->mVisible;
else return this->isVisibleDirect();
}
bool ProxyWindowBase::isVisibleDirect() const {
if (this->window == nullptr) return false;
else return this->window->isVisible();
}
void ProxyWindowBase::setVisible(bool visible) {
this->mVisible = visible;
if (this->reloadComplete) this->setVisibleDirect(visible);
}
void ProxyWindowBase::setVisibleDirect(bool visible) {
if (this->deleteOnInvisible()) {
if (visible == this->isVisibleDirect()) return;
if (visible) {
this->createWindow();
this->polishItems();
this->window->setVisible(true);
emit this->backerVisibilityChanged();
} else {
if (this->window != nullptr) {
this->window->setVisible(false);
emit this->backerVisibilityChanged();
this->deleteWindow();
}
}
} else if (this->window != nullptr) {
if (visible) this->polishItems();
this->window->setVisible(visible);
emit this->backerVisibilityChanged();
}
}
void ProxyWindowBase::polishItems() {
// Due to QTBUG-126704, layouts in invisible windows don't update their dimensions.
// Usually this isn't an issue, but it is when the size of a window is based on the size
// of its content, and that content is in a layout.
//
// This hack manually polishes the item tree right before showing the window so it will
// always be created with the correct size.
QQuickWindowPrivate::get(this->window)->polishItems();
}
void ProxyWindowBase::runLints() {
if (!this->ranLints) {
qs::debug::lintItemTree(this->mContentItem);
this->ranLints = true;
}
}
qint32 ProxyWindowBase::x() const {
if (this->window == nullptr) return 0;
else return this->window->x();
}
qint32 ProxyWindowBase::y() const {
if (this->window == nullptr) return 0;
else return this->window->y();
}
qint32 ProxyWindowBase::width() const {
if (this->window == nullptr) return this->mWidth;
else return this->window->width();
}
void ProxyWindowBase::setWidth(qint32 width) {
this->mWidth = width;
if (this->window == nullptr) {
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) {
this->mHeight = height;
if (this->window == nullptr) {
emit this->heightChanged();
} else this->window->setHeight(height);
}
void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) {
auto* qscreen = screen == nullptr ? nullptr : screen->screen;
if (qscreen == this->mScreen) return;
if (this->mScreen != nullptr) {
QObject::disconnect(this->mScreen, nullptr, this, nullptr);
}
if (this->window == nullptr) {
emit this->screenChanged();
} else {
auto reshow = this->isVisibleDirect();
if (reshow) this->setVisibleDirect(false);
if (this->window != nullptr) this->window->setScreen(qscreen);
if (reshow) this->setVisibleDirect(true);
}
if (qscreen) this->mScreen = qscreen;
else this->mScreen = this->window->screen();
QObject::connect(this->mScreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed);
}
void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; }
QuickshellScreenInfo* ProxyWindowBase::screen() const {
QScreen* qscreen = nullptr;
if (this->window == nullptr) {
if (this->mScreen != nullptr) qscreen = this->mScreen;
} else {
qscreen = this->window->screen();
}
return QuickshellTracked::instance()->screenInfo(qscreen);
}
QColor ProxyWindowBase::color() const { return this->mColor; }
void ProxyWindowBase::setColor(QColor color) {
this->mColor = color;
if (this->window == nullptr) {
if (color != this->mColor) emit this->colorChanged();
} else {
auto premultiplied = QColor::fromRgbF(
color.redF() * color.alphaF(),
color.greenF() * color.alphaF(),
color.blueF() * color.alphaF(),
color.alphaF()
);
this->window->setColor(premultiplied);
}
}
PendingRegion* ProxyWindowBase::mask() const { return this->mMask; }
void ProxyWindowBase::setMask(PendingRegion* mask) {
if (mask == this->mMask) return;
if (this->mMask != nullptr) {
QObject::disconnect(this->mMask, nullptr, this, nullptr);
}
this->mMask = mask;
if (mask != nullptr) {
mask->setParent(this);
QObject::connect(mask, &QObject::destroyed, this, &ProxyWindowBase::onMaskDestroyed);
QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::maskChanged);
}
emit this->maskChanged();
}
void ProxyWindowBase::onMaskChanged() {
if (this->window != nullptr) this->updateMask();
}
void ProxyWindowBase::onMaskDestroyed() {
this->mMask = nullptr;
emit this->maskChanged();
}
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.
if (this->mMask->mIntersection == Intersection::Combine) {
mask = this->mMask->build();
} else {
auto windowRegion = QRegion(QRect(0, 0, this->width(), this->height()));
mask = this->mMask->applyTo(windowRegion);
}
}
this->window->setFlag(Qt::WindowTransparentForInput, this->mMask != nullptr && mask.isEmpty());
this->window->setMask(mask);
}
QQmlListProperty<QObject> ProxyWindowBase::data() {
return this->mContentItem->property("data").value<QQmlListProperty<QObject>>();
}
void ProxyWindowBase::onWidthChanged() { this->mContentItem->setWidth(this->width()); }
void ProxyWindowBase::onHeightChanged() { this->mContentItem->setHeight(this->height()); }
ProxyWindowAttached::ProxyWindowAttached(QQuickItem* parent): QsWindowAttached(parent) {
this->updateWindow();
}
QObject* ProxyWindowAttached::window() const { return this->mWindow; }
QQuickItem* ProxyWindowAttached::contentItem() const { return this->mWindow->contentItem(); }
void ProxyWindowAttached::updateWindow() {
auto* window = static_cast<QQuickItem*>(this->parent())->window(); // NOLINT
if (auto* proxy = qobject_cast<ProxiedWindow*>(window)) {
this->setWindow(proxy->proxy());
} else {
this->setWindow(nullptr);
}
}
void ProxyWindowAttached::setWindow(ProxyWindowBase* window) {
if (window == this->mWindow) return;
this->mWindow = window;
emit this->windowChanged();
}
void ProxiedWindow::exposeEvent(QExposeEvent* event) {
this->QQuickWindow::exposeEvent(event);
emit this->exposed();
}