From 73cfeba61bdc8fcebad10e1888b7ffe23862638f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 20 May 2024 02:16:44 -0700 Subject: [PATCH] x11: add XPanelWindow --- CMakeLists.txt | 2 + README.md | 8 +- default.nix | 6 +- src/CMakeLists.txt | 6 +- src/core/panelinterface.hpp | 12 + src/core/proxywindow.cpp | 2 +- src/wayland/init.cpp | 16 +- src/wayland/wlr_layershell.cpp | 16 ++ src/wayland/wlr_layershell.hpp | 15 ++ src/x11/CMakeLists.txt | 22 ++ src/x11/init.cpp | 29 +++ src/x11/panel_window.cpp | 431 +++++++++++++++++++++++++++++++++ src/x11/panel_window.hpp | 160 ++++++++++++ src/x11/util.cpp | 55 +++++ src/x11/util.hpp | 29 +++ 15 files changed, 804 insertions(+), 5 deletions(-) create mode 100644 src/x11/CMakeLists.txt create mode 100644 src/x11/init.cpp create mode 100644 src/x11/panel_window.cpp create mode 100644 src/x11/panel_window.hpp create mode 100644 src/x11/util.cpp create mode 100644 src/x11/util.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 159acd49..0bf20ab4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ option(SOCKETS "Enable unix socket support" ON) option(WAYLAND "Enable wayland support" ON) option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON) option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON) +option(X11 "Enable X11 support" ON) option(HYPRLAND "Support hyprland specific features" ON) option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON) option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON) @@ -29,6 +30,7 @@ if (WAYLAND) message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}") message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}") endif () +message(STATUS " X11: ${X11}") message(STATUS " Services") message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") diff --git a/README.md b/README.md index d05e3347..c17af3a8 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,22 @@ To build quickshell at all, you will need the following packages (names may vary - just - cmake -- pkg-config - ninja - Qt6 [ QtBase, QtDeclarative ] To build with wayland support you will additionally need: +- pkg-config - wayland - wayland-scanner (may be part of wayland on some distros) - wayland-protocols - Qt6 [ QtWayland ] +To build with x11 support you will additionally need: +- libxcb + +To build with pipewire support you will additionally need: +- libpipewire + ### Building To make a release build of quickshell run: diff --git a/default.nix b/default.nix index 514c7946..0985d843 100644 --- a/default.nix +++ b/default.nix @@ -10,6 +10,8 @@ qt6, wayland, wayland-protocols, + xorg, + pipewire, gitRev ? (let headExists = builtins.pathExists ./.git/HEAD; @@ -24,6 +26,7 @@ debug ? false, enableWayland ? true, + enableX11 ? true, enablePipewire ? true, nvidiaCompat ? false, svgSupport ? true, # you almost always want this @@ -42,11 +45,12 @@ wayland-scanner ]); - buildInputs = with pkgs; [ + buildInputs = [ qt6.qtbase qt6.qtdeclarative ] ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ]) + ++ (lib.optionals enableX11 [ xorg.libxcb ]) ++ (lib.optionals svgSupport [ qt6.qtsvg ]) ++ (lib.optionals enablePipewire [ pipewire ]); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8fe9c651..be3adaf8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,10 @@ endif() if (WAYLAND) add_subdirectory(wayland) -endif () +endif() + +if (X11) + add_subdirectory(x11) +endif() add_subdirectory(services) diff --git a/src/core/panelinterface.hpp b/src/core/panelinterface.hpp index b46c25ca..e7ae0322 100644 --- a/src/core/panelinterface.hpp +++ b/src/core/panelinterface.hpp @@ -117,6 +117,10 @@ class PanelWindowInterface: public WindowInterface { Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged); /// Defaults to `ExclusionMode.Auto`. Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged); + /// If the panel should render above standard windows. Defaults to true. + QSDOC_HIDE Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged); + /// Defaults to false. + QSDOC_HIDE Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged); // clang-format on QSDOC_NAMED_ELEMENT(PanelWindow); @@ -135,9 +139,17 @@ public: [[nodiscard]] virtual ExclusionMode::Enum exclusionMode() const = 0; virtual void setExclusionMode(ExclusionMode::Enum exclusionMode) = 0; + [[nodiscard]] virtual bool aboveWindows() const = 0; + virtual void setAboveWindows(bool aboveWindows) = 0; + + [[nodiscard]] virtual bool focusable() const = 0; + virtual void setFocusable(bool focusable) = 0; + signals: void anchorsChanged(); void marginsChanged(); void exclusiveZoneChanged(); void exclusionModeChanged(); + void aboveWindowsChanged(); + void focusableChanged(); }; diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index e2a80a54..50370d9d 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -46,7 +46,7 @@ ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(); } void ProxyWindowBase::onReload(QObject* oldInstance) { this->window = this->retrieveWindow(oldInstance); auto wasVisible = this->window != nullptr && this->window->isVisible(); - if (this->window == nullptr) this->window = new QQuickWindow(); + 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 diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp index 4a70de8d..194bad4c 100644 --- a/src/wayland/init.cpp +++ b/src/wayland/init.cpp @@ -1,5 +1,7 @@ #include +#include #include +#include #include "../core/plugin.hpp" @@ -10,7 +12,19 @@ namespace { class WaylandPlugin: public QuickshellPlugin { - bool applies() override { return QGuiApplication::platformName() == "wayland"; } + bool applies() override { + auto isWayland = QGuiApplication::platformName() == "wayland"; + + if (!isWayland && !qEnvironmentVariable("WAYLAND_DISPLAY").isEmpty()) { + qWarning() << "--- WARNING ---"; + qWarning() << "WAYLAND_DISPLAY is present but QT_QPA_PLATFORM is" + << QGuiApplication::platformName(); + qWarning() << "If you are actually running wayland, set QT_QPA_PLATFORM to \"wayland\" or " + "most functionality will be broken."; + } + + return isWayland; + } void registerTypes() override { #ifdef QS_WAYLAND_WLR_LAYERSHELL diff --git a/src/wayland/wlr_layershell.cpp b/src/wayland/wlr_layershell.cpp index dd1fee93..6a381c01 100644 --- a/src/wayland/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell.cpp @@ -114,6 +114,18 @@ void WlrLayershell::setAnchors(Anchors anchors) { if (!anchors.verticalConstraint()) this->ProxyWindowBase::setHeight(this->mHeight); } +bool WlrLayershell::aboveWindows() const { return this->layer() > WlrLayer::Bottom; } + +void WlrLayershell::setAboveWindows(bool aboveWindows) { + this->setLayer(aboveWindows ? WlrLayer::Top : WlrLayer::Bottom); +} + +bool WlrLayershell::focusable() const { return this->keyboardFocus() != WlrKeyboardFocus::None; } + +void WlrLayershell::setFocusable(bool focusable) { + this->setKeyboardFocus(focusable ? WlrKeyboardFocus::OnDemand : WlrKeyboardFocus::None); +} + QString WlrLayershell::ns() const { return this->ext->ns(); } void WlrLayershell::setNamespace(QString ns) { @@ -190,6 +202,8 @@ WaylandPanelInterface::WaylandPanelInterface(QObject* parent) QObject::connect(this->layer, &WlrLayershell::marginsChanged, this, &WaylandPanelInterface::marginsChanged); QObject::connect(this->layer, &WlrLayershell::exclusiveZoneChanged, this, &WaylandPanelInterface::exclusiveZoneChanged); QObject::connect(this->layer, &WlrLayershell::exclusionModeChanged, this, &WaylandPanelInterface::exclusionModeChanged); + QObject::connect(this->layer, &WlrLayershell::layerChanged, this, &WaylandPanelInterface::aboveWindowsChanged); + QObject::connect(this->layer, &WlrLayershell::keyboardFocusChanged, this, &WaylandPanelInterface::focusableChanged); // clang-format on } @@ -224,6 +238,8 @@ proxyPair(Anchors, anchors, setAnchors); proxyPair(Margins, margins, setMargins); proxyPair(qint32, exclusiveZone, setExclusiveZone); proxyPair(ExclusionMode::Enum, exclusionMode, setExclusionMode); +proxyPair(bool, focusable, setFocusable); +proxyPair(bool, aboveWindows, setAboveWindows); #undef proxyPair // NOLINTEND diff --git a/src/wayland/wlr_layershell.hpp b/src/wayland/wlr_layershell.hpp index 4a176bde..cf9abe4f 100644 --- a/src/wayland/wlr_layershell.hpp +++ b/src/wayland/wlr_layershell.hpp @@ -8,6 +8,7 @@ #include #include "../core/doc.hpp" +#include "../core/panelinterface.hpp" #include "../core/proxywindow.hpp" #include "wlr_layershell/window.hpp" @@ -54,6 +55,8 @@ class WlrLayershell: public ProxyWindowBase { QSDOC_HIDE Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged); QSDOC_HIDE Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged); QSDOC_HIDE Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged); + QSDOC_HIDE Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY layerChanged); + QSDOC_HIDE Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY keyboardFocusChanged); QML_ATTACHED(WlrLayershell); QML_ELEMENT; // clang-format on @@ -92,6 +95,12 @@ public: [[nodiscard]] Margins margins() const; void setMargins(Margins margins); // NOLINT + [[nodiscard]] bool aboveWindows() const; + void setAboveWindows(bool aboveWindows); + + [[nodiscard]] bool focusable() const; + void setFocusable(bool focusable); + static WlrLayershell* qmlAttachedProperties(QObject* object); signals: @@ -161,6 +170,12 @@ public: [[nodiscard]] ExclusionMode::Enum exclusionMode() const override; void setExclusionMode(ExclusionMode::Enum exclusionMode) override; + + [[nodiscard]] bool aboveWindows() const override; + void setAboveWindows(bool aboveWindows) override; + + [[nodiscard]] bool focusable() const override; + void setFocusable(bool focusable) override; // NOLINTEND private: diff --git a/src/x11/CMakeLists.txt b/src/x11/CMakeLists.txt new file mode 100644 index 00000000..2da30238 --- /dev/null +++ b/src/x11/CMakeLists.txt @@ -0,0 +1,22 @@ +find_package(XCB REQUIRED COMPONENTS XCB) + +qt_add_library(quickshell-x11 STATIC + util.cpp + panel_window.cpp +) + +qt_add_qml_module(quickshell-x11 + URI Quickshell.X11 + VERSION 0.1 +) + +add_library(quickshell-x11-init OBJECT init.cpp) + +target_link_libraries(quickshell-x11 PRIVATE ${QT_DEPS} ${XCB_LIBRARIES}) +target_link_libraries(quickshell-x11-init PRIVATE ${QT_DEPS} ${XCB_LIBRARIES}) + +qs_pch(quickshell-x11) +qs_pch(quickshell-x11plugin) +qs_pch(quickshell-x11-init) + +target_link_libraries(quickshell PRIVATE quickshell-x11plugin quickshell-x11-init) diff --git a/src/x11/init.cpp b/src/x11/init.cpp new file mode 100644 index 00000000..00080036 --- /dev/null +++ b/src/x11/init.cpp @@ -0,0 +1,29 @@ +#include +#include + +#include "../core/plugin.hpp" +#include "panel_window.hpp" +#include "util.hpp" + +namespace { + +class X11Plugin: public QuickshellPlugin { + bool applies() override { return QGuiApplication::platformName() == "xcb"; } + + void init() override { XAtom::initAtoms(); } + + void registerTypes() override { + qmlRegisterType("Quickshell._X11Overlay", 1, 0, "PanelWindow"); + + qmlRegisterModuleImport( + "Quickshell", + QQmlModuleImportModuleAny, + "Quickshell._X11Overlay", + QQmlModuleImportLatest + ); + } +}; + +QS_REGISTER_PLUGIN(X11Plugin); + +} // namespace diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp new file mode 100644 index 00000000..3a65ec92 --- /dev/null +++ b/src/x11/panel_window.cpp @@ -0,0 +1,431 @@ +#include "panel_window.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/generation.hpp" +#include "../core/panelinterface.hpp" +#include "../core/proxywindow.hpp" +#include "util.hpp" + +class XPanelStack { +public: + static XPanelStack* instance() { + static XPanelStack* stack = nullptr; // NOLINT + + if (stack == nullptr) { + stack = new XPanelStack(); + } + + return stack; + } + + [[nodiscard]] const QList& panels(XPanelWindow* panel) { + return this->mPanels[EngineGeneration::findObjectGeneration(panel)]; + } + + void addPanel(XPanelWindow* panel) { + auto& panels = this->mPanels[EngineGeneration::findObjectGeneration(panel)]; + if (!panels.contains(panel)) { + panels.push_back(panel); + } + } + + void removePanel(XPanelWindow* panel) { + auto& panels = this->mPanels[EngineGeneration::findObjectGeneration(panel)]; + if (panels.removeOne(panel)) { + if (panels.isEmpty()) { + this->mPanels.erase(EngineGeneration::findObjectGeneration(panel)); + } + + // from the bottom up, update all panels + for (auto* panel: panels) { + panel->updateDimensions(); + } + } + } + +private: + std::map> mPanels; +}; + +bool XPanelEventFilter::eventFilter(QObject* watched, QEvent* event) { + if (event->type() == QEvent::PlatformSurface) { + auto* surfaceEvent = static_cast(event); // NOLINT + + if (surfaceEvent->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) { + emit this->surfaceCreated(); + } + } + + return this->QObject::eventFilter(watched, event); +} + +XPanelWindow::XPanelWindow(QObject* parent): ProxyWindowBase(parent) { + QObject::connect( + &this->eventFilter, + &XPanelEventFilter::surfaceCreated, + this, + &XPanelWindow::xInit + ); +} + +XPanelWindow::~XPanelWindow() { XPanelStack::instance()->removePanel(this); } + +void XPanelWindow::connectWindow() { + this->ProxyWindowBase::connectWindow(); + + this->window->installEventFilter(&this->eventFilter); + this->connectScreen(); + // clang-format off + QObject::connect(this->window, &QQuickWindow::screenChanged, this, &XPanelWindow::connectScreen); + QObject::connect(this->window, &QQuickWindow::visibleChanged, this, &XPanelWindow::updatePanelStack); + // clang-format on + + // qt overwrites _NET_WM_STATE, so we have to use the qt api + // QXcbWindow::WindowType::Dock in qplatformwindow_p.h + // see QXcbWindow::setWindowFlags in qxcbwindow.cpp + this->window->setProperty("_q_xcb_wm_window_type", 0x000004); + + // at least one flag needs to change for the above property to apply + this->window->setFlag(Qt::FramelessWindowHint); + this->updateAboveWindows(); + this->updateFocusable(); + + if (this->window->handle() != nullptr) { + this->xInit(); + this->updatePanelStack(); + } +} + +void XPanelWindow::setWidth(qint32 width) { + this->mWidth = width; + + // only update the actual size if not blocked by anchors + if (!this->mAnchors.horizontalConstraint()) { + this->ProxyWindowBase::setWidth(width); + this->updateDimensions(); + } +} + +void XPanelWindow::setHeight(qint32 height) { + this->mHeight = height; + + // only update the actual size if not blocked by anchors + if (!this->mAnchors.verticalConstraint()) { + this->ProxyWindowBase::setHeight(height); + this->updateDimensions(); + } +} + +Anchors XPanelWindow::anchors() const { return this->mAnchors; } + +void XPanelWindow::setAnchors(Anchors anchors) { + if (this->mAnchors == anchors) return; + this->mAnchors = anchors; + this->updateDimensions(); + emit this->anchorsChanged(); +} + +qint32 XPanelWindow::exclusiveZone() const { return this->mExclusiveZone; } + +void XPanelWindow::setExclusiveZone(qint32 exclusiveZone) { + if (this->mExclusiveZone == exclusiveZone) return; + this->mExclusiveZone = exclusiveZone; + const bool wasNormal = this->mExclusionMode == ExclusionMode::Normal; + this->setExclusionMode(ExclusionMode::Normal); + if (wasNormal) this->updateStrut(); + emit this->exclusiveZoneChanged(); +} + +ExclusionMode::Enum XPanelWindow::exclusionMode() const { return this->mExclusionMode; } + +void XPanelWindow::setExclusionMode(ExclusionMode::Enum exclusionMode) { + if (this->mExclusionMode == exclusionMode) return; + this->mExclusionMode = exclusionMode; + this->updateStrut(); + emit this->exclusionModeChanged(); +} + +Margins XPanelWindow::margins() const { return this->mMargins; } + +void XPanelWindow::setMargins(Margins margins) { + if (this->mMargins == margins) return; + this->mMargins = margins; + this->updateDimensions(); + emit this->marginsChanged(); +} + +bool XPanelWindow::aboveWindows() const { return this->mAboveWindows; } + +void XPanelWindow::setAboveWindows(bool aboveWindows) { + if (this->mAboveWindows == aboveWindows) return; + this->mAboveWindows = aboveWindows; + this->updateAboveWindows(); + emit this->aboveWindowsChanged(); +} + +bool XPanelWindow::focusable() const { return this->mFocusable; } + +void XPanelWindow::setFocusable(bool focusable) { + if (this->mFocusable == focusable) return; + this->mFocusable = focusable; + this->updateFocusable(); + emit this->focusableChanged(); +} + +void XPanelWindow::xInit() { this->updateDimensions(); } + +void XPanelWindow::connectScreen() { + if (this->mTrackedScreen != nullptr) { + QObject::disconnect(this->mTrackedScreen, nullptr, this, nullptr); + } + + this->mTrackedScreen = this->window->screen(); + + if (this->mTrackedScreen != nullptr) { + QObject::connect( + this->mTrackedScreen, + &QScreen::geometryChanged, + this, + &XPanelWindow::updateDimensions + ); + } +} + +void XPanelWindow::updateDimensions() { + if (this->window == nullptr || this->window->handle() == nullptr) return; + + auto screenGeometry = this->window->screen()->virtualGeometry(); + + if (this->mExclusionMode != ExclusionMode::Ignore) { + for (auto* panel: XPanelStack::instance()->panels(this)) { + // we only care about windows below us + if (panel == this) break; + + int side = -1; + quint32 exclusiveZone = 0; + panel->getExclusion(side, exclusiveZone); + + if (exclusiveZone == 0) continue; + + auto zone = static_cast(exclusiveZone); + + screenGeometry.adjust( + side == 0 ? zone : 0, + side == 2 ? zone : 0, + side == 1 ? -zone : 0, + side == 3 ? -zone : 0 + ); + } + } + + auto geometry = QRect(); + + if (this->mAnchors.horizontalConstraint()) { + geometry.setX(screenGeometry.x() + this->mMargins.mLeft); + geometry.setWidth(screenGeometry.width() - this->mMargins.mLeft - this->mMargins.mRight); + } else { + if (this->mAnchors.mLeft) { + geometry.setX(screenGeometry.x() + this->mMargins.mLeft); + } else if (this->mAnchors.mRight) { + geometry.setX( + screenGeometry.x() + screenGeometry.width() - this->mWidth - this->mMargins.mRight + ); + } else { + geometry.setX(screenGeometry.x() + screenGeometry.width() / 2 - this->mWidth / 2); + } + + geometry.setWidth(this->mWidth); + } + + if (this->mAnchors.verticalConstraint()) { + geometry.setY(screenGeometry.y() + this->mMargins.mTop); + geometry.setHeight(screenGeometry.height() - this->mMargins.mTop - this->mMargins.mBottom); + } else { + if (this->mAnchors.mTop) { + geometry.setY(screenGeometry.y() + this->mMargins.mTop); + } else if (this->mAnchors.mBottom) { + geometry.setY( + screenGeometry.y() + screenGeometry.height() - this->mHeight - this->mMargins.mBottom + ); + } else { + geometry.setY(screenGeometry.y() + screenGeometry.height() / 2 - this->mHeight / 2); + } + + geometry.setHeight(this->mHeight); + } + + this->window->setGeometry(geometry); + this->updateStrut(); +} + +void XPanelWindow::updatePanelStack() { + if (this->window->isVisible()) { + XPanelStack::instance()->addPanel(this); + } else { + XPanelStack::instance()->removePanel(this); + } +} + +void XPanelWindow::getExclusion(int& side, quint32& exclusiveZone) { + if (this->mExclusionMode == ExclusionMode::Ignore) return; + + auto& anchors = this->mAnchors; + if (anchors.mLeft || anchors.mRight || anchors.mTop || anchors.mBottom) { + if (!anchors.horizontalConstraint() + && (anchors.verticalConstraint() || (!anchors.mTop && !anchors.mBottom))) + { + side = anchors.mLeft ? 0 : anchors.mRight ? 1 : -1; + } else if (!anchors.verticalConstraint() + && (anchors.horizontalConstraint() || (!anchors.mLeft && !anchors.mRight))) + { + side = anchors.mTop ? 2 : anchors.mBottom ? 3 : -1; + } + } + + if (side == -1) return; + + auto autoExclude = this->mExclusionMode == ExclusionMode::Auto; + + if (autoExclude) { + if (side == 0 || side == 1) { + exclusiveZone = this->mWidth + (side == 0 ? this->mMargins.mLeft : this->mMargins.mRight); + } else { + exclusiveZone = this->mHeight + (side == 2 ? this->mMargins.mTop : this->mMargins.mBottom); + } + } else { + exclusiveZone = this->mExclusiveZone; + } +} + +void XPanelWindow::updateStrut() { + if (this->window == nullptr || this->window->handle() == nullptr) return; + auto* conn = x11Connection(); + + int side = -1; + quint32 exclusiveZone = 0; + + this->getExclusion(side, exclusiveZone); + + if (side == -1 || this->mExclusionMode == ExclusionMode::Ignore) { + xcb_delete_property(conn, this->window->winId(), XAtom::_NET_WM_STRUT.atom()); + xcb_delete_property(conn, this->window->winId(), XAtom::_NET_WM_STRUT_PARTIAL.atom()); + return; + } + + auto data = std::array(); + data[side] = exclusiveZone; + + // https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm45573693101552 + // assuming "specified in root window coordinates" means relative to the window geometry + // in which case only the end position should be set, to the opposite extent. + data[side * 2 + 5] = side == 0 || side == 1 ? this->window->height() : this->window->width(); + + xcb_change_property( + conn, + XCB_PROP_MODE_REPLACE, + this->window->winId(), + XAtom::_NET_WM_STRUT.atom(), + XCB_ATOM_CARDINAL, + 32, + 4, + data.data() + ); + + xcb_change_property( + conn, + XCB_PROP_MODE_REPLACE, + this->window->winId(), + XAtom::_NET_WM_STRUT_PARTIAL.atom(), + XCB_ATOM_CARDINAL, + 32, + 12, + data.data() + ); +} + +void XPanelWindow::updateAboveWindows() { + if (this->window == nullptr) return; + + this->window->setFlag(Qt::WindowStaysOnBottomHint, !this->mAboveWindows); + this->window->setFlag(Qt::WindowStaysOnTopHint, this->mAboveWindows); +} + +void XPanelWindow::updateFocusable() { + if (this->window == nullptr) return; + this->window->setFlag(Qt::WindowDoesNotAcceptFocus, !this->mFocusable); +} + +// XPanelInterface + +XPanelInterface::XPanelInterface(QObject* parent) + : PanelWindowInterface(parent) + , panel(new XPanelWindow(this)) { + + // clang-format off + QObject::connect(this->panel, &ProxyWindowBase::windowConnected, this, &XPanelInterface::windowConnected); + QObject::connect(this->panel, &ProxyWindowBase::visibleChanged, this, &XPanelInterface::visibleChanged); + QObject::connect(this->panel, &ProxyWindowBase::backerVisibilityChanged, this, &XPanelInterface::backingWindowVisibleChanged); + QObject::connect(this->panel, &ProxyWindowBase::heightChanged, this, &XPanelInterface::heightChanged); + QObject::connect(this->panel, &ProxyWindowBase::widthChanged, this, &XPanelInterface::widthChanged); + QObject::connect(this->panel, &ProxyWindowBase::screenChanged, this, &XPanelInterface::screenChanged); + QObject::connect(this->panel, &ProxyWindowBase::windowTransformChanged, this, &XPanelInterface::windowTransformChanged); + QObject::connect(this->panel, &ProxyWindowBase::colorChanged, this, &XPanelInterface::colorChanged); + QObject::connect(this->panel, &ProxyWindowBase::maskChanged, this, &XPanelInterface::maskChanged); + + // panel specific + QObject::connect(this->panel, &XPanelWindow::anchorsChanged, this, &XPanelInterface::anchorsChanged); + QObject::connect(this->panel, &XPanelWindow::marginsChanged, this, &XPanelInterface::marginsChanged); + QObject::connect(this->panel, &XPanelWindow::exclusiveZoneChanged, this, &XPanelInterface::exclusiveZoneChanged); + QObject::connect(this->panel, &XPanelWindow::exclusionModeChanged, this, &XPanelInterface::exclusionModeChanged); + QObject::connect(this->panel, &XPanelWindow::aboveWindowsChanged, this, &XPanelInterface::aboveWindowsChanged); + QObject::connect(this->panel, &XPanelWindow::focusableChanged, this, &XPanelInterface::focusableChanged); + // clang-format on +} + +void XPanelInterface::onReload(QObject* oldInstance) { + QQmlEngine::setContextForObject(this->panel, QQmlEngine::contextForObject(this)); + + auto* old = qobject_cast(oldInstance); + this->panel->reload(old != nullptr ? old->panel : nullptr); +} + +QQmlListProperty XPanelInterface::data() { return this->panel->data(); } +ProxyWindowBase* XPanelInterface::proxyWindow() const { return this->panel; } +QQuickItem* XPanelInterface::contentItem() const { return this->panel->contentItem(); } +bool XPanelInterface::isBackingWindowVisible() const { return this->panel->isVisibleDirect(); } + +// NOLINTBEGIN +#define proxyPair(type, get, set) \ + type XPanelInterface::get() const { return this->panel->get(); } \ + void XPanelInterface::set(type value) { this->panel->set(value); } + +proxyPair(bool, isVisible, setVisible); +proxyPair(qint32, width, setWidth); +proxyPair(qint32, height, setHeight); +proxyPair(QuickshellScreenInfo*, screen, setScreen); +proxyPair(QColor, color, setColor); +proxyPair(PendingRegion*, mask, setMask); + +// panel specific +proxyPair(Anchors, anchors, setAnchors); +proxyPair(Margins, margins, setMargins); +proxyPair(qint32, exclusiveZone, setExclusiveZone); +proxyPair(ExclusionMode::Enum, exclusionMode, setExclusionMode); +proxyPair(bool, focusable, setFocusable); +proxyPair(bool, aboveWindows, setAboveWindows); + +#undef proxyPair +// NOLINTEND diff --git a/src/x11/panel_window.hpp b/src/x11/panel_window.hpp new file mode 100644 index 00000000..db8de7d2 --- /dev/null +++ b/src/x11/panel_window.hpp @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../core/doc.hpp" +#include "../core/panelinterface.hpp" +#include "../core/proxywindow.hpp" + +class XPanelStack; + +class XPanelEventFilter: public QObject { + Q_OBJECT; + +public: + explicit XPanelEventFilter(QObject* parent = nullptr): QObject(parent) {} + +signals: + void surfaceCreated(); + +protected: + bool eventFilter(QObject* watched, QEvent* event) override; +}; + +class XPanelWindow: public ProxyWindowBase { + QSDOC_BASECLASS(PanelWindowInterface); + Q_OBJECT; + // clang-format off + QSDOC_HIDE Q_PROPERTY(Anchors anchors READ anchors WRITE setAnchors NOTIFY anchorsChanged); + QSDOC_HIDE Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged); + QSDOC_HIDE Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged); + QSDOC_HIDE Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged); + QSDOC_HIDE Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged); + QSDOC_HIDE Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged); + // clang-format on + QML_ELEMENT; + +public: + explicit XPanelWindow(QObject* parent = nullptr); + ~XPanelWindow() override; + Q_DISABLE_COPY_MOVE(XPanelWindow); + + void connectWindow() override; + + void setWidth(qint32 width) override; + void setHeight(qint32 height) override; + + [[nodiscard]] Anchors anchors() const; + void setAnchors(Anchors anchors); + + [[nodiscard]] qint32 exclusiveZone() const; + void setExclusiveZone(qint32 exclusiveZone); + + [[nodiscard]] ExclusionMode::Enum exclusionMode() const; + void setExclusionMode(ExclusionMode::Enum exclusionMode); + + [[nodiscard]] Margins margins() const; + void setMargins(Margins margins); + + [[nodiscard]] bool aboveWindows() const; + void setAboveWindows(bool aboveWindows); + + [[nodiscard]] bool focusable() const; + void setFocusable(bool focusable); + +signals: + QSDOC_HIDE void anchorsChanged(); + QSDOC_HIDE void exclusiveZoneChanged(); + QSDOC_HIDE void exclusionModeChanged(); + QSDOC_HIDE void marginsChanged(); + QSDOC_HIDE void aboveWindowsChanged(); + QSDOC_HIDE void focusableChanged(); + +private slots: + void xInit(); + void connectScreen(); + void updateDimensions(); + void updatePanelStack(); + +private: + void getExclusion(int& side, quint32& exclusiveZone); + void updateStrut(); + void updateAboveWindows(); + void updateFocusable(); + + QPointer mTrackedScreen = nullptr; + bool mAboveWindows = true; + bool mFocusable = false; + Anchors mAnchors; + Margins mMargins; + qint32 mExclusiveZone = 0; + ExclusionMode::Enum mExclusionMode = ExclusionMode::Auto; + XPanelEventFilter eventFilter; + + friend class XPanelStack; +}; + +class XPanelInterface: public PanelWindowInterface { + Q_OBJECT; + +public: + explicit XPanelInterface(QObject* parent = nullptr); + + void onReload(QObject* oldInstance) override; + + [[nodiscard]] ProxyWindowBase* proxyWindow() const override; + [[nodiscard]] QQuickItem* contentItem() const override; + + // NOLINTBEGIN + [[nodiscard]] bool isVisible() const override; + [[nodiscard]] bool isBackingWindowVisible() const override; + void setVisible(bool visible) override; + + [[nodiscard]] qint32 width() const override; + void setWidth(qint32 width) override; + + [[nodiscard]] qint32 height() const override; + void setHeight(qint32 height) override; + + [[nodiscard]] QuickshellScreenInfo* screen() const override; + void setScreen(QuickshellScreenInfo* screen) override; + + [[nodiscard]] QColor color() const override; + void setColor(QColor color) override; + + [[nodiscard]] PendingRegion* mask() const override; + void setMask(PendingRegion* mask) override; + + [[nodiscard]] QQmlListProperty data() override; + + // panel specific + + [[nodiscard]] Anchors anchors() const override; + void setAnchors(Anchors anchors) override; + + [[nodiscard]] Margins margins() const override; + void setMargins(Margins margins) override; + + [[nodiscard]] qint32 exclusiveZone() const override; + void setExclusiveZone(qint32 exclusiveZone) override; + + [[nodiscard]] ExclusionMode::Enum exclusionMode() const override; + void setExclusionMode(ExclusionMode::Enum exclusionMode) override; + + [[nodiscard]] bool aboveWindows() const override; + void setAboveWindows(bool aboveWindows) override; + + [[nodiscard]] bool focusable() const override; + void setFocusable(bool focusable) override; + // NOLINTEND + +private: + XPanelWindow* panel; + + friend class WlrLayershell; +}; diff --git a/src/x11/util.cpp b/src/x11/util.cpp new file mode 100644 index 00000000..8760ea30 --- /dev/null +++ b/src/x11/util.cpp @@ -0,0 +1,55 @@ +#include "util.hpp" + +#include +#include +#include +#include +#include + +xcb_connection_t* x11Connection() { + static xcb_connection_t* conn = nullptr; // NOLINT + + if (conn == nullptr) { + if (auto* x11Application = dynamic_cast(QGuiApplication::instance()) + ->nativeInterface()) + { + conn = x11Application->connection(); + } + } + + return conn; +} + +// NOLINTBEGIN +XAtom XAtom::_NET_WM_STRUT {}; +XAtom XAtom::_NET_WM_STRUT_PARTIAL {}; +// NOLINTEND + +void XAtom::initAtoms() { + _NET_WM_STRUT.init("_NET_WM_STRUT"); + _NET_WM_STRUT_PARTIAL.init("_NET_WM_STRUT_PARTIAL"); +} + +void XAtom::init(const QByteArray& name) { + this->cookie = xcb_intern_atom(x11Connection(), 0, name.length(), name.data()); +} + +bool XAtom::isValid() { + this->resolve(); + return this->mAtom != XCB_ATOM_NONE; +} + +const xcb_atom_t& XAtom::atom() { + this->resolve(); + return this->mAtom; +} + +void XAtom::resolve() { + if (!this->resolved) { + this->resolved = true; + + auto* reply = xcb_intern_atom_reply(x11Connection(), this->cookie, nullptr); + if (reply != nullptr) this->mAtom = reply->atom; + free(reply); // NOLINT + } +} diff --git a/src/x11/util.hpp b/src/x11/util.hpp new file mode 100644 index 00000000..da3f718a --- /dev/null +++ b/src/x11/util.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include +#include + +xcb_connection_t* x11Connection(); + +class XAtom { +public: + [[nodiscard]] bool isValid(); + [[nodiscard]] const xcb_atom_t& atom(); + + // NOLINTBEGIN + static XAtom _NET_WM_STRUT; + static XAtom _NET_WM_STRUT_PARTIAL; + // NOLINTEND + + static void initAtoms(); + +private: + void init(const QByteArray& name); + void resolve(); + + bool resolved = false; + xcb_atom_t mAtom = XCB_ATOM_NONE; + xcb_intern_atom_cookie_t cookie {}; +};