quickshell/src/window/proxywindow.cpp

611 lines
19 KiB
C++

#include "proxywindow.hpp"
#include <private/qquickwindow_p.h>
#include <qcontainerfwd.h>
#include <qcoreevent.h>
#include <qevent.h>
#include <qguiapplication.h>
#include <qlogging.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qpoint.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmlinfo.h>
#include <qqmllist.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qregion.h>
#include <qsurfaceformat.h>
#include <qtenvironmentvariables.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 ProxyWindowContentItem()) {
QQmlEngine::setObjectOwnership(this->mContentItem, QQmlEngine::CppOwnership);
this->mContentItem->setParent(this);
// clang-format off
QObject::connect(this->mContentItem, &ProxyWindowContentItem::polished, this, &ProxyWindowBase::onPolished);
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onWidthChanged);
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onHeightChanged);
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();
this->ensureQWindow();
// 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->onExposed();
}
}
void ProxyWindowBase::postCompleteWindow() { this->setVisible(this->mVisible); }
ProxiedWindow* ProxyWindowBase::createQQuickWindow() { return new ProxiedWindow(this); }
void ProxyWindowBase::ensureQWindow() {
auto format = QSurfaceFormat::defaultFormat();
{
// match QtQuick's default format, including env var controls
static const auto useDepth = qEnvironmentVariableIsEmpty("QSG_NO_DEPTH_BUFFER");
static const auto useStencil = qEnvironmentVariableIsEmpty("QSG_NO_STENCIL_BUFFER");
static const auto enableDebug = qEnvironmentVariableIsSet("QSG_OPENGL_DEBUG");
static const auto disableVSync = qEnvironmentVariableIsSet("QSG_NO_VSYNC");
if (useDepth && format.depthBufferSize() == -1) format.setDepthBufferSize(24);
else if (!useDepth) format.setDepthBufferSize(0);
if (useStencil && format.stencilBufferSize() == -1) format.setStencilBufferSize(8);
else if (!useStencil) format.setStencilBufferSize(0);
auto opaque = this->qsSurfaceFormat.opaqueModified ? this->qsSurfaceFormat.opaque
: this->mColor.alpha() >= 255;
format.setOption(QSurfaceFormat::ResetNotification);
if (opaque) format.setAlphaBufferSize(0);
else format.setAlphaBufferSize(8);
if (enableDebug) format.setOption(QSurfaceFormat::DebugContext);
if (disableVSync) format.setSwapInterval(0);
format.setSwapBehavior(QSurfaceFormat::DoubleBuffer);
format.setRedBufferSize(8);
format.setGreenBufferSize(8);
format.setBlueBufferSize(8);
}
this->mSurfaceFormat = format;
auto useOldWindow = this->window != nullptr;
if (useOldWindow) {
if (this->window->requestedFormat() != format) {
useOldWindow = false;
}
}
if (useOldWindow) return;
delete this->window;
this->window = nullptr; // createQQuickWindow may indirectly reference this->window
this->window = this->createQQuickWindow();
this->window->setFormat(format);
}
void ProxyWindowBase::createWindow() {
this->ensureQWindow();
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::onVisibleChanged);
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, &QQuickWindow::sceneGraphError, this, &ProxyWindowBase::onSceneGraphError);
QObject::connect(this->window, &ProxiedWindow::exposed, this, &ProxyWindowBase::onExposed);
QObject::connect(this->window, &ProxiedWindow::devicePixelRatioChanged, this, &ProxyWindowBase::devicePixelRatioChanged);
// 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);
}
this->trySetWidth(this->implicitWidth());
this->trySetHeight(this->implicitHeight());
this->setColor(this->mColor);
this->updateMask();
// notify initial / post-connection geometry
emit this->xChanged();
emit this->yChanged();
emit this->widthChanged();
emit this->heightChanged();
emit this->devicePixelRatioChanged();
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();
}
void ProxyWindowBase::onSceneGraphError(
QQuickWindow::SceneGraphError error,
const QString& message
) {
if (error == QQuickWindow::ContextNotAvailable) {
qCritical().nospace() << "Failed to create graphics context for " << this << ": " << message;
} else {
qCritical().nospace() << "Scene graph error " << error << " occurred for " << this << ": "
<< message;
}
emit this->resourcesLost();
this->mVisible = false;
this->setVisibleDirect(false);
}
void ProxyWindowBase::onVisibleChanged() {
if (this->mVisible && !this->window->isVisible()) {
this->mVisible = false;
this->setVisibleDirect(false);
emit this->closed();
}
emit this->visibleChanged();
}
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::schedulePolish() {
if (this->isVisibleDirect()) {
this->mContentItem->polish();
}
}
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::onExposed() {
this->onPolished();
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();
}
void ProxyWindowBase::setImplicitWidth(qint32 implicitWidth) {
if (implicitWidth == this->bImplicitWidth) return;
this->bImplicitWidth = implicitWidth;
if (this->window) this->trySetWidth(implicitWidth);
else emit this->widthChanged();
}
void ProxyWindowBase::trySetWidth(qint32 implicitWidth) { this->window->setWidth(implicitWidth); }
void ProxyWindowBase::setImplicitHeight(qint32 implicitHeight) {
if (implicitHeight == this->bImplicitHeight) return;
this->bImplicitHeight = implicitHeight;
if (this->window) this->trySetHeight(implicitHeight);
else emit this->heightChanged();
}
void ProxyWindowBase::trySetHeight(qint32 implicitHeight) {
this->window->setHeight(implicitHeight);
}
qint32 ProxyWindowBase::width() const {
if (this->window == nullptr) return this->implicitWidth();
else return this->window->width();
}
void ProxyWindowBase::setWidth(qint32 width) {
this->setImplicitWidth(width);
qmlWarning(this) << "Setting `width` is deprecated. Set `implicitWidth` instead.";
}
qint32 ProxyWindowBase::height() const {
if (this->window == nullptr) return this->implicitHeight();
else return this->window->height();
}
void ProxyWindowBase::setHeight(qint32 height) {
this->setImplicitHeight(height);
qmlWarning(this) << "Setting `height` is deprecated. Set `implicitHeight` instead.";
}
void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) {
auto* qscreen = screen == nullptr ? nullptr : screen->screen;
auto newMScreen = this->mScreen != qscreen;
if (this->mScreen && newMScreen) {
QObject::disconnect(this->mScreen, nullptr, this, nullptr);
}
auto* oldScreen = this->qscreen();
this->mScreen = qscreen;
if (oldScreen != qscreen) {
if (this->window == nullptr) {
emit this->screenChanged();
} else if (qscreen) {
auto reshow = this->isVisibleDirect();
if (reshow) this->setVisibleDirect(false);
if (this->window != nullptr) this->window->setScreen(qscreen);
if (reshow) this->setVisibleDirect(true);
}
}
if (qscreen && newMScreen) {
QObject::connect(this->mScreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed);
}
}
void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; }
QScreen* ProxyWindowBase::qscreen() const {
if (this->window) return this->window->screen();
if (this->mScreen) return this->mScreen;
return QGuiApplication::primaryScreen();
}
QuickshellScreenInfo* ProxyWindowBase::screen() const {
return QuickshellTracked::instance()->screenInfo(this->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);
// setColor also modifies the alpha buffer size of the surface format
this->window->setFormat(this->mSurfaceFormat);
}
}
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) {
QObject::connect(mask, &QObject::destroyed, this, &ProxyWindowBase::onMaskDestroyed);
QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::onMaskChanged);
}
this->onMaskChanged();
emit this->maskChanged();
}
void ProxyWindowBase::setSurfaceFormat(QsSurfaceFormat format) {
if (format == this->qsSurfaceFormat) return;
if (this->window != nullptr) {
qmlWarning(this) << "Cannot set window surface format.";
return;
}
this->qsSurfaceFormat = format;
emit this->surfaceFormatChanged();
}
qreal ProxyWindowBase::devicePixelRatio() const {
if (this->window != nullptr) return this->window->devicePixelRatio();
if (this->mScreen != nullptr) return this->mScreen->devicePixelRatio();
return 1.0;
}
void ProxyWindowBase::onMaskChanged() {
if (this->window != nullptr) this->updateMask();
}
void ProxyWindowBase::onMaskDestroyed() {
this->mMask = nullptr;
this->onMaskChanged();
emit this->maskChanged();
}
void ProxyWindowBase::updateMask() {
this->pendingPolish.inputMask = true;
this->schedulePolish();
}
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()); }
QPointF ProxyWindowBase::itemPosition(QQuickItem* item) const {
if (!item) {
qCritical() << "Cannot map position of null item.";
return {};
}
return this->mContentItem->mapFromItem(item, 0, 0);
}
QRectF ProxyWindowBase::itemRect(QQuickItem* item) const {
if (!item) {
qCritical() << "Cannot map position of null item.";
return {};
}
return this->mContentItem->mapFromItem(item, item->boundingRect());
}
QPointF ProxyWindowBase::mapFromItem(QQuickItem* item, QPointF point) const {
if (!item) {
qCritical() << "Cannot map position of null item.";
return {};
}
return this->mContentItem->mapFromItem(item, point);
}
QPointF ProxyWindowBase::mapFromItem(QQuickItem* item, qreal x, qreal y) const {
if (!item) {
qCritical() << "Cannot map position of null item.";
return {};
}
return this->mContentItem->mapFromItem(item, x, y);
}
QRectF ProxyWindowBase::mapFromItem(QQuickItem* item, QRectF rect) const {
if (!item) {
qCritical() << "Cannot map position of null item.";
return {};
}
return this->mContentItem->mapFromItem(item, rect);
}
QRectF
ProxyWindowBase::mapFromItem(QQuickItem* item, qreal x, qreal y, qreal width, qreal height) const {
if (!item) {
qCritical() << "Cannot map position of null item.";
return {};
}
return this->mContentItem->mapFromItem(item, x, y, width, height);
}
ProxyWindowAttached::ProxyWindowAttached(QQuickItem* parent): QsWindowAttached(parent) {
this->updateWindow();
}
QObject* ProxyWindowAttached::window() const { return this->mWindowInterface; }
ProxyWindowBase* ProxyWindowAttached::proxyWindow() const { return this->mWindow; }
QQuickItem* ProxyWindowAttached::contentItem() const {
return this->mWindow ? this->mWindow->contentItem() : nullptr;
}
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;
auto* parentInterface = window ? qobject_cast<WindowInterface*>(window->parent()) : nullptr;
this->mWindowInterface = parentInterface ? static_cast<QObject*>(parentInterface) : window;
emit this->windowChanged();
}
bool ProxiedWindow::event(QEvent* event) {
if (event->type() == QEvent::DevicePixelRatioChange) {
emit this->devicePixelRatioChanged();
}
return this->QQuickWindow::event(event);
}
void ProxiedWindow::exposeEvent(QExposeEvent* event) {
this->QQuickWindow::exposeEvent(event);
emit this->exposed();
}
void ProxyWindowContentItem::updatePolish() { emit this->polished(); }
void ProxyWindowBase::onPolished() {
if (this->pendingPolish.inputMask) {
QRegion mask;
if (this->mMask != nullptr) {
mask = this->mMask->applyTo(QRect(0, 0, this->width(), this->height()));
}
this->window->setFlag(Qt::WindowTransparentForInput, this->mMask != nullptr && mask.isEmpty());
this->window->setMask(mask);
this->pendingPolish.inputMask = false;
}
emit this->polished();
}