diff --git a/.clang-format b/.clang-format index eb81a3b..610ee65 100644 --- a/.clang-format +++ b/.clang-format @@ -31,12 +31,12 @@ SpaceBeforeInheritanceColon: false SpaceBeforeParens: ControlStatementsExceptControlMacros SortIncludes: CaseSensitive PointerAlignment: Left -PackConstructorInitializers: NextLine +PackConstructorInitializers: CurrentLine LineEnding: LF InsertBraces: false -BreakConstructorInitializers: AfterColon +BreakConstructorInitializers: BeforeComma BreakBeforeBraces: Custom -BreakInheritanceList: AfterColon +BreakInheritanceList: BeforeComma AllowAllParametersOfDeclarationOnNextLine: false AllowAllArgumentsOnNextLine: false AlwaysBreakTemplateDeclarations: Yes diff --git a/CMakeLists.txt b/CMakeLists.txt index 1289416..abec58b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ set(QT_MIN_VERSION "6.6.0") set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -option(LAYERSHELL "Enable wayland layershell support" ON) +option(WAYLAND "Enable wayland support" ON) add_compile_options(-Wall -Wextra) @@ -19,8 +19,17 @@ if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Debug) endif() -find_package(Qt6 REQUIRED COMPONENTS Gui Qml Quick QuickControls2) +set(QT_DEPS Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2) +set(QT_FPDEPS Gui Qml Quick QuickControls2) + +if (WAYLAND) + list(APPEND QT_DEPS Qt6::WaylandClient Qt6::WaylandClientPrivate) + list(APPEND QT_FPDEPS WaylandClient) +endif() + +find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS}) qt_standard_project_setup(REQUIRES 6.6) add_subdirectory(src/core) +add_subdirectory(src/wayland) diff --git a/Justfile b/Justfile index c4ffa6e..687f266 100644 --- a/Justfile +++ b/Justfile @@ -30,4 +30,4 @@ clean: rm -rf {{builddir}} run *ARGS='': build - {{builddir}}/quickshell {{ARGS}} + {{builddir}}/src/core/quickshell {{ARGS}} diff --git a/docs b/docs index 94f0754..cc201af 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 94f07543939dfe682bb382f6802cbe9ff3eea061 +Subproject commit cc201afd3a352a28f5daddbe00d7aed974d52d30 diff --git a/shell.nix b/shell.nix index 3325fc5..357b037 100644 --- a/shell.nix +++ b/shell.nix @@ -6,20 +6,15 @@ rev = "1f062cc198d1112d13e5128fa1f2ee3dbffe613b"; sha256 = "kbt0Zc1qHE5fhqBkKz8iue+B+ZANjF1AR/RdgmX1r0I="; }) {}; - - qtlayershell = pkgs.callPackage (import (pkgs.fetchFromGitea { - domain = "git.outfoxxed.me"; - owner = "outfoxxed"; - repo = "layer-shell-qt-nokde"; - rev = "2ebe7b313efbacfcd62ec39e2fda6b4c740d0770"; - sha256 = "N/nMwf5LQMMwCJvG7J/6xug/EUppHedQCngzCkH8Auk="; - })) {}; #pkgs.callPackage (import /home/admin/programming/outfoxxed/layer-shell-qt) {}; in pkgs.mkShell { nativeBuildInputs = with pkgs; [ just clang-tools_17 cmake + pkg-config + wayland-scanner + qt6.wrapQtAppsHook makeWrapper ]; @@ -28,9 +23,11 @@ in pkgs.mkShell { qt6.qtbase qt6.qtdeclarative qt6.qtwayland - qtlayershell + wayland + wayland-protocols ]; + QTWAYLANDSCANNER = "${pkgs.qt6.qtwayland}/libexec/qtwaylandscanner"; TIDYFOX = "${tidyfox}/lib/libtidyfox.so"; shellHook = '' diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index d5cd27d..deb5612 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,5 +1,6 @@ qt_add_executable(quickshell main.cpp + plugin.cpp shell.cpp variants.cpp rootwrapper.cpp @@ -11,21 +12,9 @@ qt_add_executable(quickshell watcher.cpp region.cpp persistentprops.cpp - shellwindow.cpp + windowinterface.cpp + floatingwindow.cpp + panelinterface.cpp ) qt_add_qml_module(quickshell URI QuickShell) - -# qml type registration requires this -target_include_directories(quickshell PRIVATE src/core) - -target_link_libraries(quickshell PRIVATE Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2) - -if (LAYERSHELL) - find_package(LayerShellQt REQUIRED) - - target_link_libraries(quickshell PRIVATE LayerShellQtInterface) - target_compile_definitions(quickshell PRIVATE CONF_LAYERSHELL) - - target_sources(quickshell PRIVATE layershell.cpp) -endif() diff --git a/src/core/doc.hpp b/src/core/doc.hpp new file mode 100644 index 0000000..e4c907a --- /dev/null +++ b/src/core/doc.hpp @@ -0,0 +1,11 @@ +#pragma once + +// hide a property, function, or signal from typegen +#define QSDOC_HIDE + +// override the base class as seen by typegen +#define QSDOC_BASECLASS(baseclass) + +// make the type visible in the docs even if not a QML_ELEMENT +#define QSDOC_ELEMENT +#define QSDOC_NAMED_ELEMENT(name) diff --git a/src/core/floatingwindow.cpp b/src/core/floatingwindow.cpp new file mode 100644 index 0000000..4bcd74b --- /dev/null +++ b/src/core/floatingwindow.cpp @@ -0,0 +1,62 @@ +#include "floatingwindow.hpp" + +#include +#include +#include +#include + +#include "proxywindow.hpp" +#include "windowinterface.hpp" + +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); + } +} + +// FloatingWindowInterface + +FloatingWindowInterface::FloatingWindowInterface(QObject* parent) + : WindowInterface(parent) + , window(new ProxyFloatingWindow(this)) { + // clang-format off + QObject::connect(this->window, &ProxyWindowBase::windowConnected, this, &FloatingWindowInterface::windowConnected); + QObject::connect(this->window, &ProxyWindowBase::visibleChanged, this, &FloatingWindowInterface::visibleChanged); + QObject::connect(this->window, &ProxyWindowBase::heightChanged, this, &FloatingWindowInterface::heightChanged); + QObject::connect(this->window, &ProxyWindowBase::widthChanged, this, &FloatingWindowInterface::widthChanged); + QObject::connect(this->window, &ProxyWindowBase::screenChanged, this, &FloatingWindowInterface::screenChanged); + QObject::connect(this->window, &ProxyWindowBase::colorChanged, this, &FloatingWindowInterface::colorChanged); + QObject::connect(this->window, &ProxyWindowBase::maskChanged, this, &FloatingWindowInterface::maskChanged); + // clang-format on +} + +void FloatingWindowInterface::onReload(QObject* oldInstance) { + auto* old = qobject_cast(oldInstance); + this->window->onReload(old != nullptr ? old->window : nullptr); +} + +QQmlListProperty FloatingWindowInterface::data() { return this->window->data(); } +QQuickItem* FloatingWindowInterface::contentItem() const { return this->window->contentItem(); } + +// NOLINTBEGIN +#define proxyPair(type, get, set) \ + type FloatingWindowInterface::get() const { return this->window->get(); } \ + void FloatingWindowInterface::set(type value) { this->window->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); + +#undef proxyPair +#undef proxySet +#undef proxyGet +// NOLINTEND diff --git a/src/core/floatingwindow.hpp b/src/core/floatingwindow.hpp new file mode 100644 index 0000000..66fcc7e --- /dev/null +++ b/src/core/floatingwindow.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +#include "proxywindow.hpp" + +class ProxyFloatingWindow: public ProxyWindowBase { + Q_OBJECT; + +public: + explicit ProxyFloatingWindow(QObject* parent = nullptr): ProxyWindowBase(parent) {} + + // Setting geometry while the window is visible makes the content item shrinks but not the window + // which is awful so we disable it for floating windows. + void setWidth(qint32 width) override; + void setHeight(qint32 height) override; +}; + +///! Standard floating window. +class FloatingWindowInterface: public WindowInterface { + Q_OBJECT; + QML_NAMED_ELEMENT(FloatingWindow); + +public: + explicit FloatingWindowInterface(QObject* parent = nullptr); + + void onReload(QObject* oldInstance) override; + + [[nodiscard]] QQuickItem* contentItem() const override; + + // NOLINTBEGIN + [[nodiscard]] bool isVisible() 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; + // NOLINTEND + +private: + ProxyFloatingWindow* window; +}; diff --git a/src/core/layershell.cpp b/src/core/layershell.cpp deleted file mode 100644 index 0503906..0000000 --- a/src/core/layershell.cpp +++ /dev/null @@ -1,300 +0,0 @@ -#include "layershell.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "proxywindow.hpp" -#include "shellwindow.hpp" - -WaylandShellWindow::WaylandShellWindow(QObject* parent): - ProxyShellWindow(parent), mWayland(new WaylandShellWindowExtensions(this)) {} - -void WaylandShellWindow::setupWindow() { - this->shellWindow = LayerShellQt::Window::get(this->window); - - this->ProxyShellWindow::setupWindow(); - - // clang-format off - QObject::connect(this->shellWindow, &LayerShellQt::Window::anchorsChanged, this, &ProxyShellWindow::anchorsChanged); - QObject::connect(this->shellWindow, &LayerShellQt::Window::marginsChanged, this, &ProxyShellWindow::marginsChanged); - - QObject::connect( - this->shellWindow, &LayerShellQt::Window::layerChanged, - this->mWayland, &WaylandShellWindowExtensions::layerChanged - ); - QObject::connect( - this->shellWindow, &LayerShellQt::Window::keyboardInteractivityChanged, - this->mWayland, &WaylandShellWindowExtensions::keyboardFocusChanged - ); - - QObject::connect(this->window, &QWindow::widthChanged, this, &WaylandShellWindow::updateExclusionZone); - QObject::connect(this->window, &QWindow::heightChanged, this, &WaylandShellWindow::updateExclusionZone); - QObject::connect(this, &ProxyShellWindow::anchorsChanged, this, &WaylandShellWindow::updateExclusionZone); - QObject::connect(this, &ProxyShellWindow::marginsChanged, this, &WaylandShellWindow::updateExclusionZone); - // clang-format on - - this->setAnchors(this->mAnchors); - this->setMargins(this->mMargins); - this->setExclusionMode(this->mExclusionMode); // also sets exclusion zone - this->mWayland->setLayer(this->mLayer); - this->shellWindow->setScope(this->mScope); - this->mWayland->setKeyboardFocus(this->mKeyboardFocus); - - this->connected = true; -} - -QQuickWindow* WaylandShellWindow::disownWindow() { - QObject::disconnect(this->shellWindow, nullptr, this, nullptr); - return this->ProxyWindowBase::disownWindow(); -} - -void WaylandShellWindow::setWidth(qint32 width) { - this->mWidth = width; - - // only update the actual size if not blocked by anchors - auto anchors = this->anchors(); - if (!anchors.mLeft || !anchors.mRight) this->ProxyShellWindow::setWidth(width); -} - -void WaylandShellWindow::setHeight(qint32 height) { - this->mHeight = height; - - // only update the actual size if not blocked by anchors - auto anchors = this->anchors(); - if (!anchors.mTop || !anchors.mBottom) this->ProxyShellWindow::setHeight(height); -} - -void WaylandShellWindow::setAnchors(Anchors anchors) { - if (this->window == nullptr) { - this->mAnchors = anchors; - return; - } - - auto lsAnchors = LayerShellQt::Window::Anchors(); - if (anchors.mLeft) lsAnchors |= LayerShellQt::Window::AnchorLeft; - if (anchors.mRight) lsAnchors |= LayerShellQt::Window::AnchorRight; - if (anchors.mTop) lsAnchors |= LayerShellQt::Window::AnchorTop; - if (anchors.mBottom) lsAnchors |= LayerShellQt::Window::AnchorBottom; - - 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 WaylandShellWindow::anchors() const { - if (this->window == nullptr) return this->mAnchors; - - auto lsAnchors = this->shellWindow->anchors(); - - Anchors anchors; - anchors.mLeft = lsAnchors.testFlag(LayerShellQt::Window::AnchorLeft); - anchors.mRight = lsAnchors.testFlag(LayerShellQt::Window::AnchorRight); - anchors.mTop = lsAnchors.testFlag(LayerShellQt::Window::AnchorTop); - anchors.mBottom = lsAnchors.testFlag(LayerShellQt::Window::AnchorBottom); - - return anchors; -} - -void WaylandShellWindow::setExclusiveZone(qint32 zone) { - if (zone < 0) zone = 0; - if (this->connected && zone == this->mExclusionZone) return; - this->mExclusionZone = zone; - - if (this->window != nullptr && this->exclusionMode() == ExclusionMode::Normal) { - this->shellWindow->setExclusiveZone(zone); - emit this->exclusionZoneChanged(); - } -} - -qint32 WaylandShellWindow::exclusiveZone() const { - if (this->window == nullptr) return this->mExclusionZone; - else return this->shellWindow->exclusionZone(); -} - -ExclusionMode::Enum WaylandShellWindow::exclusionMode() const { return this->mExclusionMode; } - -void WaylandShellWindow::setExclusionMode(ExclusionMode::Enum exclusionMode) { - if (this->connected && exclusionMode == this->mExclusionMode) return; - this->mExclusionMode = exclusionMode; - - 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 WaylandShellWindow::setMargins(Margins margins) { - if (this->window == nullptr) this->mMargins = margins; - else { - auto lsMargins = QMargins(margins.mLeft, margins.mTop, margins.mRight, margins.mBottom); - this->shellWindow->setMargins(lsMargins); - } -} - -Margins WaylandShellWindow::margins() const { - if (this->window == nullptr) return this->mMargins; - auto lsMargins = this->shellWindow->margins(); - - auto margins = Margins(); - margins.mLeft = lsMargins.left(); - margins.mRight = lsMargins.right(); - margins.mTop = lsMargins.top(); - margins.mBottom = lsMargins.bottom(); - - return margins; -} - -void WaylandShellWindowExtensions::setLayer(Layer::Enum layer) { - if (this->window->window == nullptr) { - this->window->mLayer = layer; - return; - } - - auto lsLayer = LayerShellQt::Window::LayerBackground; - - // clang-format off - switch (layer) { - case Layer::Background: lsLayer = LayerShellQt::Window::LayerBackground; break; - case Layer::Bottom: lsLayer = LayerShellQt::Window::LayerBottom; break; - case Layer::Top: lsLayer = LayerShellQt::Window::LayerTop; break; - case Layer::Overlay: lsLayer = LayerShellQt::Window::LayerOverlay; break; - } - // clang-format on - - this->window->shellWindow->setLayer(lsLayer); -} - -Layer::Enum WaylandShellWindowExtensions::layer() const { - if (this->window->window == nullptr) return this->window->mLayer; - - auto layer = Layer::Top; - auto lsLayer = this->window->shellWindow->layer(); - - // clang-format off - switch (lsLayer) { - case LayerShellQt::Window::LayerBackground: layer = Layer::Background; break; - case LayerShellQt::Window::LayerBottom: layer = Layer::Bottom; break; - case LayerShellQt::Window::LayerTop: layer = Layer::Top; break; - case LayerShellQt::Window::LayerOverlay: layer = Layer::Overlay; break; - } - // clang-format on - - return layer; -} - -void WaylandShellWindowExtensions::setScope(const QString& scope) { - if (this->window->window == nullptr) this->window->mScope = scope; - else this->window->shellWindow->setScope(scope); -} - -QString WaylandShellWindowExtensions::scope() const { - if (this->window->window == nullptr) return this->window->mScope; - else return this->window->shellWindow->scope(); -} - -void WaylandShellWindowExtensions::setKeyboardFocus(KeyboardFocus::Enum focus) { - if (this->window->window == nullptr) { - this->window->mKeyboardFocus = focus; - return; - } - - auto lsFocus = LayerShellQt::Window::KeyboardInteractivityNone; - - // clang-format off - switch (focus) { - case KeyboardFocus::None: lsFocus = LayerShellQt::Window::KeyboardInteractivityNone; break; - case KeyboardFocus::Exclusive: lsFocus = LayerShellQt::Window::KeyboardInteractivityExclusive; break; - case KeyboardFocus::OnDemand: lsFocus = LayerShellQt::Window::KeyboardInteractivityOnDemand; break; - } - // clang-format on - - this->window->shellWindow->setKeyboardInteractivity(lsFocus); -} - -KeyboardFocus::Enum WaylandShellWindowExtensions::keyboardFocus() const { - if (this->window->window == nullptr) return this->window->mKeyboardFocus; - - auto focus = KeyboardFocus::None; - auto lsFocus = this->window->shellWindow->keyboardInteractivity(); - - // clang-format off - switch (lsFocus) { - case LayerShellQt::Window::KeyboardInteractivityNone: focus = KeyboardFocus::None; break; - case LayerShellQt::Window::KeyboardInteractivityExclusive: focus = KeyboardFocus::Exclusive; break; - case LayerShellQt::Window::KeyboardInteractivityOnDemand: focus = KeyboardFocus::OnDemand; break; - } - // clang-format on - - return focus; -} - -void WaylandShellWindowExtensions::setScreenConfiguration(ScreenConfiguration::Enum configuration) { - if (this->window->window == nullptr) { - this->window->mScreenConfiguration = configuration; - return; - } - - auto lsConfiguration = LayerShellQt::Window::ScreenFromQWindow; - - // clang-format off - switch (configuration) { - case ScreenConfiguration::Window: lsConfiguration = LayerShellQt::Window::ScreenFromQWindow; break; - case ScreenConfiguration::Compositor: lsConfiguration = LayerShellQt::Window::ScreenFromCompositor; break; - } - // clang-format on - - this->window->shellWindow->setScreenConfiguration(lsConfiguration); -} - -ScreenConfiguration::Enum WaylandShellWindowExtensions::screenConfiguration() const { - if (this->window->window == nullptr) return this->window->mScreenConfiguration; - - auto configuration = ScreenConfiguration::Window; - auto lsConfiguration = this->window->shellWindow->screenConfiguration(); - - // clang-format off - switch (lsConfiguration) { - case LayerShellQt::Window::ScreenFromQWindow: configuration = ScreenConfiguration::Window; break; - case LayerShellQt::Window::ScreenFromCompositor: configuration = ScreenConfiguration::Compositor; break; - } - // clang-format on - - return configuration; -} - -void WaylandShellWindow::updateExclusionZone() { - if (this->window != nullptr && this->exclusionMode() == ExclusionMode::Auto) { - auto anchors = this->anchors(); - - auto zone = -1; - - if (anchors.mTop && anchors.mBottom) { - if (anchors.mLeft) zone = this->width() + this->margins().mLeft; - else if (anchors.mRight) zone = this->width() + this->margins().mRight; - } else if (anchors.mLeft && anchors.mRight) { - if (anchors.mTop) zone = this->height() + this->margins().mTop; - else if (anchors.mBottom) zone = this->height() + this->margins().mBottom; - } - - if (zone != -1) { - this->shellWindow->setExclusiveZone(zone); - emit this->exclusionZoneChanged(); - } - } -} diff --git a/src/core/layershell.hpp b/src/core/layershell.hpp deleted file mode 100644 index 6f0c0d7..0000000 --- a/src/core/layershell.hpp +++ /dev/null @@ -1,144 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "shellwindow.hpp" - -namespace Layer { // NOLINT -Q_NAMESPACE; -QML_ELEMENT; - -enum Enum { - Background = 0, - Bottom = 1, - Top = 2, - Overlay = 3, -}; -Q_ENUM_NS(Enum); - -} // namespace Layer - -/// Type of keyboard focus that will be accepted by a [ShellWindow] -/// -/// [ShellWindow]: ../shellwindow -namespace KeyboardFocus { // NOLINT -Q_NAMESPACE; -QML_ELEMENT; - -enum Enum { - /// No keyboard input will be accepted. - None = 0, - /// Exclusive access to the keyboard, locking out all other windows. - Exclusive = 1, - /// Access to the keyboard as determined by the operating system. - /// - /// > [!WARNING] On some systems, `OnDemand` may cause the shell window to - /// > retain focus over another window unexpectedly. - /// > You should try `None` if you experience issues. - OnDemand = 2, -}; -Q_ENUM_NS(Enum); - -} // namespace KeyboardFocus - -namespace ScreenConfiguration { // NOLINT -Q_NAMESPACE; -QML_ELEMENT; - -enum Enum { - Window = 0, - Compositor = 1, -}; -Q_ENUM_NS(Enum); - -} // namespace ScreenConfiguration - -class WaylandShellWindowExtensions; - -class WaylandShellWindow: public ProxyShellWindow { - Q_OBJECT; - Q_PROPERTY(WaylandShellWindowExtensions* wayland MEMBER mWayland CONSTANT); - QML_NAMED_ELEMENT(ShellWindow); - -public: - explicit WaylandShellWindow(QObject* parent = nullptr); - - WaylandShellWindowExtensions* wayland(); - - void setupWindow() override; - QQuickWindow* disownWindow() override; - - void setWidth(qint32 width) override; - void setHeight(qint32 height) override; - - void setAnchors(Anchors anchors) override; - [[nodiscard]] Anchors anchors() const override; - - void setExclusiveZone(qint32 zone) override; - [[nodiscard]] qint32 exclusiveZone() const override; - - void setExclusionMode(ExclusionMode::Enum exclusionMode) override; - [[nodiscard]] ExclusionMode::Enum exclusionMode() const override; - - void setMargins(Margins margins) override; - [[nodiscard]] Margins margins() const override; - -protected slots: - void updateExclusionZone(); - -private: - WaylandShellWindowExtensions* mWayland = nullptr; - - LayerShellQt::Window* shellWindow = nullptr; - Layer::Enum mLayer = Layer::Top; - QString mScope; - KeyboardFocus::Enum mKeyboardFocus = KeyboardFocus::None; - ScreenConfiguration::Enum mScreenConfiguration = ScreenConfiguration::Window; - - bool connected = false; - - friend class WaylandShellWindowExtensions; -}; - -class WaylandShellWindowExtensions: public QObject { - Q_OBJECT; - /// The shell layer the window sits in. Defaults to `Layer.Top`. - Q_PROPERTY(Layer::Enum layer READ layer WRITE setLayer NOTIFY layerChanged); - Q_PROPERTY(QString scope READ scope WRITE setScope); - /// 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); - QML_ELEMENT; - QML_UNCREATABLE("WaylandShellWindowExtensions cannot be created"); - -public: - explicit WaylandShellWindowExtensions(WaylandShellWindow* window): - QObject(window), window(window) {} - - void setLayer(Layer::Enum layer); - [[nodiscard]] Layer::Enum layer() const; - - void setScope(const QString& scope); - [[nodiscard]] QString scope() const; - - void setKeyboardFocus(KeyboardFocus::Enum focus); - [[nodiscard]] KeyboardFocus::Enum keyboardFocus() const; - - void setScreenConfiguration(ScreenConfiguration::Enum configuration); - [[nodiscard]] ScreenConfiguration::Enum screenConfiguration() const; - -signals: - void layerChanged(); - void keyboardFocusChanged(); - -private: - WaylandShellWindow* window; - - friend class WaylandShellWindow; -}; diff --git a/src/core/main.cpp b/src/core/main.cpp index edeeb86..8d4a5e3 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -8,12 +8,9 @@ #include #include +#include "plugin.hpp" #include "rootwrapper.hpp" -#ifdef CONF_LAYERSHELL -#include -#endif - int main(int argc, char** argv) { const auto app = QGuiApplication(argc, argv); QGuiApplication::setApplicationName("quickshell"); @@ -42,9 +39,7 @@ int main(int argc, char** argv) { return -1; } -#if CONF_LAYERSHELL - LayerShellQt::Shell::useLayerShell(); -#endif + QuickshellPlugin::initPlugins(); // Base window transparency appears to be additive. // Use a fully transparent window with a colored rect. diff --git a/src/core/module.md b/src/core/module.md index e55da91..5d4cf26 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -6,10 +6,11 @@ headers = [ "reload.hpp", "shell.hpp", "variants.hpp", - "proxywindow.hpp", - "layershell.hpp", "region.hpp", + "proxywindow.hpp", "persistentprops.hpp", + "windowinterface.hpp", + "panelinterface.hpp", + "floatingwindow.hpp", ] ----- -The core types provided by QuickShell diff --git a/src/core/panelinterface.cpp b/src/core/panelinterface.cpp new file mode 100644 index 0000000..c0157c9 --- /dev/null +++ b/src/core/panelinterface.cpp @@ -0,0 +1 @@ +#include "panelinterface.hpp" // NOLINT diff --git a/src/core/shellwindow.hpp b/src/core/panelinterface.hpp similarity index 70% rename from src/core/shellwindow.hpp rename to src/core/panelinterface.hpp index 25a123a..f25e2b9 100644 --- a/src/core/shellwindow.hpp +++ b/src/core/panelinterface.hpp @@ -1,16 +1,9 @@ #pragma once -#include -#include -#include -#include -#include #include -#include -#include -#include -#include "proxywindow.hpp" +#include "doc.hpp" +#include "windowinterface.hpp" class Anchors { Q_GADGET; @@ -20,6 +13,18 @@ class Anchors { Q_PROPERTY(bool bottom MEMBER mBottom); public: + [[nodiscard]] bool horizontalConstraint() const noexcept { return this->mLeft && this->mRight; } + [[nodiscard]] bool verticalConstraint() const noexcept { return this->mTop && this->mBottom; } + + [[nodiscard]] bool operator==(const Anchors& other) const noexcept { + // clang-format off + return this->mLeft == other.mLeft + && this->mRight == other.mRight + && this->mTop == other.mTop + && this->mBottom == other.mBottom; + // clang-format on + } + bool mLeft = false; bool mRight = false; bool mTop = false; @@ -34,6 +39,15 @@ class Margins { Q_PROPERTY(qint32 bottom MEMBER mBottom); public: + [[nodiscard]] bool operator==(const Margins& other) const noexcept { + // clang-format off + return this->mLeft == other.mLeft + && this->mRight == other.mRight + && this->mTop == other.mTop + && this->mBottom == other.mBottom; + // clang-format on + } + qint32 mLeft = 0; qint32 mRight = 0; qint32 mTop = 0; @@ -66,7 +80,7 @@ Q_ENUM_NS(Enum); /// The following snippet creates a white bar attached to the bottom of the screen. /// /// ```qml -/// ShellWindow { +/// PanelWindow { /// anchors { /// left: true /// bottom: true @@ -80,7 +94,8 @@ Q_ENUM_NS(Enum); /// } /// } /// ``` -class ProxyShellWindow: public ProxyWindowBase { +class PanelWindowInterface: public WindowInterface { + QSDOC_NAMED_ELEMENT(PanelWindow); // clang-format off Q_OBJECT; /// Anchors attach a shell window to the sides of the screen. @@ -90,45 +105,37 @@ class ProxyShellWindow: public ProxyWindowBase { /// > (width or height) will be forced to equal the screen width/height. /// > Margins can be used to create anchored windows that are also disconnected from the monitor sides. Q_PROPERTY(Anchors anchors READ anchors WRITE setAnchors NOTIFY anchorsChanged); - /// The amount of space reserved for the shell layer relative to its anchors. - /// - /// > [!INFO] Some systems will require exactly 3 anchors to be attached for the exclusion zone to take - /// > effect. - Q_PROPERTY(qint32 exclusionZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusionZoneChanged); - /// Defaults to `ExclusionMode.Normal`. - Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged); /// Offsets from the sides of the screen. /// /// > [!INFO] Only applies to edges with anchors Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged); + /// The amount of space reserved for the shell layer relative to its anchors. + /// Setting this property sets `exclusionMode` to `Normal`. + /// + /// > [!INFO] Either 1 or 3 anchors are required for the zone to take effect. + 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); // clang-format on public: - explicit ProxyShellWindow(QObject* parent = nullptr): ProxyWindowBase(parent) {} + explicit PanelWindowInterface(QObject* parent = nullptr): WindowInterface(parent) {} - QQmlListProperty data(); - - virtual void setAnchors(Anchors anchors) = 0; [[nodiscard]] virtual Anchors anchors() const = 0; + virtual void setAnchors(Anchors anchors) = 0; - virtual void setExclusiveZone(qint32 zone) = 0; - [[nodiscard]] virtual qint32 exclusiveZone() const = 0; - - virtual void setExclusionMode(ExclusionMode::Enum exclusionMode) = 0; - [[nodiscard]] virtual ExclusionMode::Enum exclusionMode() const = 0; - - virtual void setMargins(Margins margins) = 0; [[nodiscard]] virtual Margins margins() const = 0; + virtual void setMargins(Margins margins) = 0; + + [[nodiscard]] virtual qint32 exclusiveZone() const = 0; + virtual void setExclusiveZone(qint32 exclusiveZone) = 0; + + [[nodiscard]] virtual ExclusionMode::Enum exclusionMode() const = 0; + virtual void setExclusionMode(ExclusionMode::Enum exclusionMode) = 0; signals: void anchorsChanged(); void marginsChanged(); - void exclusionZoneChanged(); + void exclusiveZoneChanged(); void exclusionModeChanged(); - -protected: - ExclusionMode::Enum mExclusionMode = ExclusionMode::Normal; - qint32 mExclusionZone = 0; - Anchors mAnchors; - Margins mMargins; }; diff --git a/src/core/plugin.cpp b/src/core/plugin.cpp new file mode 100644 index 0000000..1485208 --- /dev/null +++ b/src/core/plugin.cpp @@ -0,0 +1,27 @@ +#include "plugin.hpp" +#include + +#include // NOLINT (what??) + +static QVector plugins; // NOLINT + +void QuickshellPlugin::registerPlugin(QuickshellPlugin& plugin) { plugins.push_back(&plugin); } + +void QuickshellPlugin::initPlugins() { + plugins.erase( + std::remove_if( + plugins.begin(), + plugins.end(), + [](QuickshellPlugin* plugin) { return !plugin->applies(); } + ), + plugins.end() + ); + + for (QuickshellPlugin* plugin: plugins) { + plugin->init(); + } + + for (QuickshellPlugin* plugin: plugins) { + plugin->registerTypes(); + } +} diff --git a/src/core/plugin.hpp b/src/core/plugin.hpp new file mode 100644 index 0000000..92f1bc2 --- /dev/null +++ b/src/core/plugin.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +class QuickshellPlugin { +public: + QuickshellPlugin() = default; + virtual ~QuickshellPlugin() = default; + QuickshellPlugin(QuickshellPlugin&&) = delete; + QuickshellPlugin(const QuickshellPlugin&) = delete; + void operator=(QuickshellPlugin&&) = delete; + void operator=(const QuickshellPlugin&) = delete; + + virtual bool applies() { return true; } + virtual void init() {} + virtual void registerTypes() {} + + static void registerPlugin(QuickshellPlugin& plugin); + static void initPlugins(); +}; + +// NOLINTBEGIN +#define QS_REGISTER_PLUGIN(clazz) \ + [[gnu::constructor]] void qsInitPlugin() { \ + static clazz plugin; \ + QuickshellPlugin::registerPlugin(plugin); \ + } +// NOLINTEND diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index c6d5206..b8dc617 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -28,14 +28,7 @@ ProxyWindowBase::~ProxyWindowBase() { } void ProxyWindowBase::onReload(QObject* oldInstance) { - auto* old = qobject_cast(oldInstance); - - if (old == nullptr || old->window == nullptr) { - this->window = new QQuickWindow(); - } else { - this->window = old->disownWindow(); - } - + this->window = this->createWindow(oldInstance); this->setupWindow(); Reloadable::reloadRecursive(this->mContentItem, oldInstance); @@ -52,6 +45,16 @@ void ProxyWindowBase::onReload(QObject* oldInstance) { this->window->setVisible(this->mVisible); } +QQuickWindow* ProxyWindowBase::createWindow(QObject* oldInstance) { + auto* old = qobject_cast(oldInstance); + + if (old == nullptr || old->window == nullptr) { + return new QQuickWindow(); + } else { + return old->disownWindow(); + } +} + void ProxyWindowBase::setupWindow() { // clang-format off QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged); @@ -203,14 +206,4 @@ QQmlListProperty ProxyWindowBase::data() { } void ProxyWindowBase::onWidthChanged() { this->mContentItem->setWidth(this->width()); } - void ProxyWindowBase::onHeightChanged() { this->mContentItem->setHeight(this->height()); } - -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/core/proxywindow.hpp b/src/core/proxywindow.hpp index e28311e..c6d81a9 100644 --- a/src/core/proxywindow.hpp +++ b/src/core/proxywindow.hpp @@ -15,12 +15,12 @@ #include "qmlscreen.hpp" #include "region.hpp" #include "reload.hpp" +#include "windowinterface.hpp" // Proxy to an actual window exposing a limited property set with the ability to // transfer it to a new window. ///! Base class for reloadable windows -/// Base class for reloadable windows. See [ShellWindow] and [FloatingWindow] /// /// [ShellWindow]: ../shellwindow /// [FloatingWindow]: ../floatingwindow @@ -35,76 +35,14 @@ class ProxyWindowBase: public Reloadable { /// > Use **only** if you know what you are doing. Q_PROPERTY(QQuickWindow* _backingWindow READ backingWindow); Q_PROPERTY(QQuickItem* contentItem READ contentItem); - /// If the window is shown or hidden. Defaults to true. Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged); Q_PROPERTY(qint32 width READ width WRITE setWidth NOTIFY widthChanged); Q_PROPERTY(qint32 height READ height WRITE setHeight NOTIFY heightChanged); - /// The screen that the window currently occupies. - /// - /// > [!INFO] This cannot be changed while the window is visible. Q_PROPERTY(QuickShellScreenInfo* screen READ screen WRITE setScreen NOTIFY screenChanged); - /// The background color of the window. Defaults to white. - /// - /// > [!WARNING] This seems to behave weirdly when using transparent colors on some systems. - /// > Using a colored content item over a transparent window is the recommended way to work around this: - /// > ```qml - /// > ProxyWindow { - /// > Rectangle { - /// > anchors.fill: parent - /// > color: "#20ffffff" - /// > - /// > // your content here - /// > } - /// > } - /// > ``` Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged); - /// The clickthrough mask. Defaults to null. - /// - /// If non null then the clickable areas of the window will be determined by the provided region. - /// - /// ```qml - /// ShellWindow { - /// // The mask region is set to `rect`, meaning only `rect` is clickable. - /// // All other clicks pass through the window to ones behind it. - /// mask: Region { item: rect } - /// - /// Rectangle { - /// id: rect - /// - /// anchors.centerIn: parent - /// width: 100 - /// height: 100 - /// } - /// } - /// ``` - /// - /// If the provided region's intersection mode is `Combine` (the default), - /// then the region will be used as is. Otherwise it will be applied on top of the window region. - /// - /// For example, setting the intersection mode to `Xor` will invert the mask and make everything in - /// the mask region not clickable and pass through clicks inside it through the window. - /// - /// ```qml - /// ShellWindow { - /// // The mask region is set to `rect`, but the intersection mode is set to `Xor`. - /// // This inverts the mask causing all clicks inside `rect` to be passed to the window - /// // behind this one. - /// mask: Region { item: rect; intersection: Intersection.Xor } - /// - /// Rectangle { - /// id: rect - /// - /// anchors.centerIn: parent - /// width: 100 - /// height: 100 - /// } - /// } - /// ``` Q_PROPERTY(PendingRegion* mask READ mask WRITE setMask NOTIFY maskChanged); Q_PROPERTY(QQmlListProperty data READ data); Q_CLASSINFO("DefaultProperty", "data"); - QML_ELEMENT; - QML_UNCREATABLE("use ShellWindow or FloatingWindow"); public: explicit ProxyWindowBase(QObject* parent = nullptr); @@ -117,6 +55,7 @@ public: void onReload(QObject* oldInstance) override; + virtual QQuickWindow* createWindow(QObject* oldInstance); virtual void setupWindow(); // Disown the backing window and delete all its children. @@ -134,16 +73,16 @@ public: [[nodiscard]] virtual qint32 height() const; virtual void setHeight(qint32 height); + [[nodiscard]] virtual QuickShellScreenInfo* screen() const; virtual void setScreen(QuickShellScreenInfo* screen); - [[nodiscard]] QuickShellScreenInfo* screen() const; [[nodiscard]] QColor color() const; - void setColor(QColor color); + virtual void setColor(QColor color); [[nodiscard]] PendingRegion* mask() const; - void setMask(PendingRegion* mask); + virtual void setMask(PendingRegion* mask); - QQmlListProperty data(); + [[nodiscard]] QQmlListProperty data(); signals: void windowConnected(); @@ -154,10 +93,10 @@ signals: void colorChanged(); void maskChanged(); -private slots: +protected slots: + virtual void onWidthChanged(); + virtual void onHeightChanged(); void onMaskChanged(); - void onWidthChanged(); - void onHeightChanged(); void onScreenDestroyed(); protected: @@ -173,18 +112,3 @@ protected: private: void updateMask(); }; - -// qt attempts to resize the window but fails because wayland -// and only resizes the graphics context which looks terrible. - -///! Standard floating window. -class ProxyFloatingWindow: public ProxyWindowBase { - Q_OBJECT; - QML_NAMED_ELEMENT(FloatingWindow); - -public: - // 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/core/qmlscreen.cpp b/src/core/qmlscreen.cpp index b72d4c4..29a4629 100644 --- a/src/core/qmlscreen.cpp +++ b/src/core/qmlscreen.cpp @@ -7,8 +7,9 @@ #include #include -QuickShellScreenInfo::QuickShellScreenInfo(QObject* parent, QScreen* screen): - QObject(parent), screen(screen) { +QuickShellScreenInfo::QuickShellScreenInfo(QObject* parent, QScreen* screen) + : QObject(parent) + , screen(screen) { if (this->screen != nullptr) { // clang-format off diff --git a/src/core/reload.hpp b/src/core/reload.hpp index 8533309..ff15fad 100644 --- a/src/core/reload.hpp +++ b/src/core/reload.hpp @@ -13,7 +13,9 @@ /// /// [ProxyWindowBase]: ../proxywindowbase /// [PersistentProperties]: ../persistentproperties -class Reloadable: public QObject, public QQmlParserStatus { +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 diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index 17dd549..a5050b4 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -12,8 +12,10 @@ #include "shell.hpp" #include "watcher.hpp" -RootWrapper::RootWrapper(QString rootPath): - QObject(nullptr), rootPath(std::move(rootPath)), engine(this) { +RootWrapper::RootWrapper(QString rootPath) + : QObject(nullptr) + , rootPath(std::move(rootPath)) + , engine(this) { this->reloadGraph(true); if (this->root == nullptr) { diff --git a/src/core/shellwindow.cpp b/src/core/shellwindow.cpp deleted file mode 100644 index 6b36f7e..0000000 --- a/src/core/shellwindow.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "shellwindow.hpp" // NOLINT diff --git a/src/core/windowinterface.cpp b/src/core/windowinterface.cpp new file mode 100644 index 0000000..a29bd59 --- /dev/null +++ b/src/core/windowinterface.cpp @@ -0,0 +1 @@ +#include "windowinterface.hpp" // NOLINT diff --git a/src/core/windowinterface.hpp b/src/core/windowinterface.hpp new file mode 100644 index 0000000..d61dea2 --- /dev/null +++ b/src/core/windowinterface.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "qmlscreen.hpp" +#include "region.hpp" +#include "reload.hpp" + +class WindowInterface: public Reloadable { + Q_OBJECT; + // clang-format off + Q_PROPERTY(QQuickItem* contentItem READ contentItem); + /// If the window is shown or hidden. Defaults to true. + Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged); + Q_PROPERTY(qint32 width READ width WRITE setWidth NOTIFY widthChanged); + Q_PROPERTY(qint32 height READ height WRITE setHeight NOTIFY heightChanged); + /// The screen that the window currently occupies. + /// + /// > [!INFO] This cannot be changed after windowConnected. + Q_PROPERTY(QuickShellScreenInfo* screen READ screen WRITE setScreen NOTIFY screenChanged); + /// The background color of the window. Defaults to white. + /// + /// > [!WARNING] This seems to behave weirdly when using transparent colors on some systems. + /// > Using a colored content item over a transparent window is the recommended way to work around this: + /// > ```qml + /// > ProxyWindow { + /// > Rectangle { + /// > anchors.fill: parent + /// > color: "#20ffffff" + /// > + /// > // your content here + /// > } + /// > } + /// > ``` + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged); + /// The clickthrough mask. Defaults to null. + /// + /// If non null then the clickable areas of the window will be determined by the provided region. + /// + /// ```qml + /// ShellWindow { + /// // The mask region is set to `rect`, meaning only `rect` is clickable. + /// // All other clicks pass through the window to ones behind it. + /// mask: Region { item: rect } + /// + /// Rectangle { + /// id: rect + /// + /// anchors.centerIn: parent + /// width: 100 + /// height: 100 + /// } + /// } + /// ``` + /// + /// If the provided region's intersection mode is `Combine` (the default), + /// then the region will be used as is. Otherwise it will be applied on top of the window region. + /// + /// For example, setting the intersection mode to `Xor` will invert the mask and make everything in + /// the mask region not clickable and pass through clicks inside it through the window. + /// + /// ```qml + /// ShellWindow { + /// // The mask region is set to `rect`, but the intersection mode is set to `Xor`. + /// // This inverts the mask causing all clicks inside `rect` to be passed to the window + /// // behind this one. + /// mask: Region { item: rect; intersection: Intersection.Xor } + /// + /// Rectangle { + /// id: rect + /// + /// anchors.centerIn: parent + /// width: 100 + /// height: 100 + /// } + /// } + /// ``` + Q_PROPERTY(PendingRegion* mask READ mask WRITE setMask NOTIFY maskChanged); + Q_PROPERTY(QQmlListProperty data READ data); + // clang-format on + Q_CLASSINFO("DefaultProperty", "data"); + QML_NAMED_ELEMENT(QSWindow); + QML_UNCREATABLE("uncreatable base class"); + +public: + explicit WindowInterface(QObject* parent = nullptr): Reloadable(parent) {} + + [[nodiscard]] virtual QQuickItem* contentItem() const = 0; + + [[nodiscard]] virtual bool isVisible() const = 0; + virtual void setVisible(bool visible) = 0; + + [[nodiscard]] virtual qint32 width() const = 0; + virtual void setWidth(qint32 width) = 0; + + [[nodiscard]] virtual qint32 height() const = 0; + virtual void setHeight(qint32 height) = 0; + + [[nodiscard]] virtual QuickShellScreenInfo* screen() const = 0; + virtual void setScreen(QuickShellScreenInfo* screen) = 0; + + [[nodiscard]] virtual QColor color() const = 0; + virtual void setColor(QColor color) = 0; + + [[nodiscard]] virtual PendingRegion* mask() const = 0; + virtual void setMask(PendingRegion* mask) = 0; + + [[nodiscard]] virtual QQmlListProperty data() = 0; + +signals: + void windowConnected(); + void visibleChanged(); + void widthChanged(); + void heightChanged(); + void screenChanged(); + void colorChanged(); + void maskChanged(); +}; diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt new file mode 100644 index 0000000..e1f6a5d --- /dev/null +++ b/src/wayland/CMakeLists.txt @@ -0,0 +1,64 @@ +qt_add_library(quickshell-wayland STATIC + shell_integration.cpp + layer_surface.cpp + layershell.cpp + waylandlayershell.cpp +) + +# required to make sure the constructor is linked +add_library(quickshell-wayland-init OBJECT init.cpp) + +target_link_libraries(quickshell PRIVATE ${QT_DEPS} quickshell-waylandplugin quickshell-wayland-init) + +qt_add_qml_module(quickshell-wayland URI QuickShell.Wayland) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(wayland REQUIRED IMPORTED_TARGET wayland-client wayland-protocols) + +find_package(Qt6 REQUIRED COMPONENTS WaylandClient) + +target_link_libraries(quickshell-wayland PRIVATE ${QT_DEPS} wayland-client) +target_link_libraries(quickshell-wayland-init PRIVATE ${QT_DEPS} wayland-client) + +# wayland protocols + +if (DEFINED ENV{QTWAYLANDSCANNER}) + set(qtwaylandscanner $ENV{QTWAYLANDSCANNER}) +else() + find_program(qtwaylandscanner NAMES qtwaylandscanner) +endif() + +if (qtwaylandscanner STREQUAL "qtwaylandscanner-NOTFOUND") + message(FATAL_ERROR "qtwaylandscanner not found. Set the QTWAYLANDSCANNER environment variable to specify its path explicity.") +endif() + +message(STATUS "Found qtwaylandscanner at ${qtwaylandscanner}") + +find_program(waylandscanner NAMES wayland-scanner) +message(STATUS "Found wayland-scanner at ${waylandscanner}") + +execute_process( + COMMAND pkg-config --variable=pkgdatadir wayland-protocols + OUTPUT_VARIABLE WAYLAND_PROTOCOLS_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}") + +set(PROTO_SRC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/wl-proto) +set(PROTO_BUILD_PATH ${CMAKE_CURRENT_BINARY_DIR}/wl-proto) +make_directory(${PROTO_BUILD_PATH}) + +function (wl_proto name path) + execute_process(COMMAND ${waylandscanner} client-header ${path} ${PROTO_BUILD_PATH}/wayland-${name}-client-protocol.h) + execute_process(COMMAND ${waylandscanner} private-code ${path} ${PROTO_BUILD_PATH}/wayland-${name}.c) + execute_process(COMMAND ${qtwaylandscanner} client-header ${path} OUTPUT_FILE ${PROTO_BUILD_PATH}/qwayland-${name}.h) + execute_process(COMMAND ${qtwaylandscanner} client-code ${path} OUTPUT_FILE ${PROTO_BUILD_PATH}/qwayland-${name}.cpp) + + target_sources(quickshell-wayland PRIVATE ${PROTO_BUILD_PATH}/wayland-${name}.c) + target_sources(quickshell-wayland PRIVATE ${PROTO_BUILD_PATH}/qwayland-${name}.cpp) +endfunction() + +wl_proto("wlr-layer-shell-unstable-v1" "${PROTO_SRC_PATH}/wlr-layer-shell-unstable-v1.xml") + +target_include_directories(quickshell-wayland PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/wl-proto) diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp new file mode 100644 index 0000000..928f523 --- /dev/null +++ b/src/wayland/init.cpp @@ -0,0 +1,29 @@ +#include +#include + +#include "../core/plugin.hpp" +#include "waylandlayershell.hpp" + +namespace { + +class WaylandPlugin: public QuickshellPlugin { + bool applies() override { return QGuiApplication::platformName() == "wayland"; } + + void registerTypes() override { + qmlRegisterType("QuickShell._WaylandOverlay", 1, 0, "PanelWindow"); + + // If any types are defined inside a module using QML_ELEMENT then all QML_ELEMENT types + // will not be registered. This can be worked around with a module import which makes + // the QML_ELMENT module import the old register-type style module. + qmlRegisterModuleImport( + "QuickShell", + QQmlModuleImportModuleAny, + "QuickShell._WaylandOverlay", + QQmlModuleImportLatest + ); + } +}; + +QS_REGISTER_PLUGIN(WaylandPlugin); + +} // namespace diff --git a/src/wayland/layer_surface.cpp b/src/wayland/layer_surface.cpp new file mode 100644 index 0000000..777a38f --- /dev/null +++ b/src/wayland/layer_surface.cpp @@ -0,0 +1,165 @@ +#include "layer_surface.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/panelinterface.hpp" +#include "layershell.hpp" +#include "shell_integration.hpp" + +// clang-format off +[[nodiscard]] QtWayland::zwlr_layer_shell_v1::layer toWaylandLayer(const Layer::Enum& layer) noexcept; +[[nodiscard]] QtWayland::zwlr_layer_surface_v1::anchor toWaylandAnchors(const Anchors& anchors) noexcept; +[[nodiscard]] QtWayland::zwlr_layer_surface_v1::keyboard_interactivity toWaylandKeyboardFocus(const KeyboardFocus::Enum& focus) noexcept; +[[nodiscard]] QSize constrainedSize(const Anchors& anchors, const QSize& size) noexcept; +// clang-format on + +// clang-format off +QSWaylandLayerSurface::QSWaylandLayerSurface( + QSWaylandLayerShellIntegration* shell, + QtWaylandClient::QWaylandWindow* window +): QtWaylandClient::QWaylandShellSurface(window) { + // clang-format on + + auto* qwindow = window->window(); + this->ext = LayershellWindowExtension::get(qwindow); + + if (this->ext == nullptr) { + throw "QSWaylandLayerSurface created with null LayershellWindowExtension"; + } + + wl_output* output = nullptr; // NOLINT (import) + if (this->ext->useWindowScreen) { + auto* waylandScreen = + dynamic_cast(qwindow->screen()->handle()); + + if (waylandScreen != nullptr) { + output = waylandScreen->output(); + } else { + qWarning() << "Layershell screen is set but does not corrospond to a real screen. Letting " + "the compositor pick."; + } + } + + this->init(shell->get_layer_surface( + window->waylandSurface()->object(), + output, + toWaylandLayer(this->ext->mLayer), + this->ext->mNamespace + )); + + this->updateAnchors(); + this->updateLayer(); + this->updateMargins(); + this->updateExclusiveZone(); + this->updateKeyboardFocus(); + + // new updates will be sent from the extension + this->ext->surface = this; + + auto size = constrainedSize(this->ext->mAnchors, qwindow->size()); + this->set_size(size.width(), size.height()); +} + +QSWaylandLayerSurface::~QSWaylandLayerSurface() { this->destroy(); } + +void QSWaylandLayerSurface::zwlr_layer_surface_v1_configure( + quint32 serial, + quint32 width, + quint32 height +) { + this->ack_configure(serial); + + this->size = QSize(static_cast(width), static_cast(height)); + if (!this->configured) { + this->configured = true; + this->window()->resizeFromApplyConfigure(this->size); + this->window()->handleExpose(QRect(QPoint(), this->size)); + } else { + this->window()->applyConfigureWhenPossible(); + } +} + +void QSWaylandLayerSurface::zwlr_layer_surface_v1_closed() { this->window()->window()->close(); } + +bool QSWaylandLayerSurface::isExposed() const { return this->configured; } + +void QSWaylandLayerSurface::applyConfigure() { + this->window()->resizeFromApplyConfigure(this->size); +} + +void QSWaylandLayerSurface::setWindowGeometry(const QRect& geometry) { + auto size = constrainedSize(this->ext->mAnchors, geometry.size()); + this->set_size(size.width(), size.height()); +} + +QWindow* QSWaylandLayerSurface::qwindow() { return this->window()->window(); } + +void QSWaylandLayerSurface::updateLayer() { this->set_layer(toWaylandLayer(this->ext->mLayer)); } + +void QSWaylandLayerSurface::updateAnchors() { + this->set_anchor(toWaylandAnchors(this->ext->mAnchors)); + this->setWindowGeometry(this->window()->windowContentGeometry()); +} + +void QSWaylandLayerSurface::updateMargins() { + auto& margins = this->ext->mMargins; + this->set_margin(margins.mTop, margins.mRight, margins.mBottom, margins.mLeft); +} + +void QSWaylandLayerSurface::updateExclusiveZone() { + this->set_exclusive_zone(this->ext->mExclusiveZone); +} + +void QSWaylandLayerSurface::updateKeyboardFocus() { + this->set_keyboard_interactivity(toWaylandKeyboardFocus(this->ext->mKeyboardFocus)); +} + +QtWayland::zwlr_layer_shell_v1::layer toWaylandLayer(const Layer::Enum& layer) noexcept { + switch (layer) { + case Layer::Background: return QtWayland::zwlr_layer_shell_v1::layer_background; + case Layer::Bottom: return QtWayland::zwlr_layer_shell_v1::layer_bottom; + case Layer::Top: return QtWayland::zwlr_layer_shell_v1::layer_top; + case Layer::Overlay: return QtWayland::zwlr_layer_shell_v1::layer_overlay; + } + + return QtWayland::zwlr_layer_shell_v1::layer_top; +} + +QtWayland::zwlr_layer_surface_v1::anchor toWaylandAnchors(const Anchors& anchors) noexcept { + quint32 wl = 0; + if (anchors.mLeft) wl |= QtWayland::zwlr_layer_surface_v1::anchor_left; + if (anchors.mRight) wl |= QtWayland::zwlr_layer_surface_v1::anchor_right; + if (anchors.mTop) wl |= QtWayland::zwlr_layer_surface_v1::anchor_top; + if (anchors.mBottom) wl |= QtWayland::zwlr_layer_surface_v1::anchor_bottom; + return static_cast(wl); +} + +QtWayland::zwlr_layer_surface_v1::keyboard_interactivity +toWaylandKeyboardFocus(const KeyboardFocus::Enum& focus) noexcept { + switch (focus) { + case KeyboardFocus::None: return QtWayland::zwlr_layer_surface_v1::keyboard_interactivity_none; + case KeyboardFocus::Exclusive: + return QtWayland::zwlr_layer_surface_v1::keyboard_interactivity_exclusive; + case KeyboardFocus::OnDemand: + return QtWayland::zwlr_layer_surface_v1::keyboard_interactivity_on_demand; + } + + return QtWayland::zwlr_layer_surface_v1::keyboard_interactivity_none; +} + +QSize constrainedSize(const Anchors& anchors, const QSize& size) noexcept { + return QSize( + anchors.horizontalConstraint() ? 0 : size.width(), + anchors.verticalConstraint() ? 0 : size.height() + ); +} diff --git a/src/wayland/layer_surface.hpp b/src/wayland/layer_surface.hpp new file mode 100644 index 0000000..2518e74 --- /dev/null +++ b/src/wayland/layer_surface.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "layershell.hpp" +#include "shell_integration.hpp" + +class QSWaylandLayerSurface + : public QtWaylandClient::QWaylandShellSurface + , public QtWayland::zwlr_layer_surface_v1 { +public: + QSWaylandLayerSurface( + QSWaylandLayerShellIntegration* shell, + QtWaylandClient::QWaylandWindow* window + ); + + ~QSWaylandLayerSurface() override; + QSWaylandLayerSurface(QSWaylandLayerSurface&&) = delete; + QSWaylandLayerSurface(const QSWaylandLayerSurface&) = delete; + void operator=(QSWaylandLayerSurface&&) = delete; + void operator=(const QSWaylandLayerSurface&) = delete; + + [[nodiscard]] bool isExposed() const override; + void applyConfigure() override; + void setWindowGeometry(const QRect& geometry) override; + +private: + void zwlr_layer_surface_v1_configure(quint32 serial, quint32 width, quint32 height) override; + void zwlr_layer_surface_v1_closed() override; + + QWindow* qwindow(); + void updateLayer(); + void updateAnchors(); + void updateMargins(); + void updateExclusiveZone(); + void updateKeyboardFocus(); + + LayershellWindowExtension* ext; + QSize size; + bool configured = false; + + friend class LayershellWindowExtension; +}; diff --git a/src/wayland/layershell.cpp b/src/wayland/layershell.cpp new file mode 100644 index 0000000..1fa9711 --- /dev/null +++ b/src/wayland/layershell.cpp @@ -0,0 +1,145 @@ +#include "layershell.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../core/panelinterface.hpp" +#include "layer_surface.hpp" +#include "shell_integration.hpp" + +LayershellWindowExtension* LayershellWindowExtension::get(QWindow* window) { + auto v = window->property("layershell_ext"); + + if (v.canConvert()) { + return v.value(); + } else { + return nullptr; + } +} + +bool LayershellWindowExtension::attach(QWindow* window) { + if (this->surface != nullptr) + throw "Cannot change the attached window of a LayershellWindowExtension"; + + auto* current = LayershellWindowExtension::get(window); + + bool hasSurface = false; + + if (current != nullptr) { + if (current->mNamespace != this->mNamespace) return false; + + if (current->surface != nullptr) { + if (current->surface->qwindow()->screen() != window->screen()) return false; + this->surface = current->surface; + + // update window with current settings, leveraging old extension's cached values + current->setAnchors(this->mAnchors); + current->setMargins(this->mMargins); + current->setExclusiveZone(this->mExclusiveZone); + current->setLayer(this->mLayer); + current->setKeyboardFocus(this->mKeyboardFocus); + + this->surface->ext = this; + current->surface = nullptr; + current->deleteLater(); + + hasSurface = true; + } + } + + if (!hasSurface) { + window->create(); + + auto* waylandWindow = dynamic_cast(window->handle()); + if (waylandWindow == nullptr) { + qWarning() << window << "is not a wayland window. Cannot create layershell surface."; + return false; + } + + static QSWaylandLayerShellIntegration* layershellIntegration = nullptr; // NOLINT + if (layershellIntegration == nullptr) { + layershellIntegration = new QSWaylandLayerShellIntegration(); + if (!layershellIntegration->initialize(waylandWindow->display())) { + delete layershellIntegration; + layershellIntegration = nullptr; + qWarning() << "Failed to initialize layershell integration"; + } + } + + waylandWindow->setShellIntegration(layershellIntegration); + } + + this->setParent(window); + window->setProperty("layershell_ext", QVariant::fromValue(this)); + return true; +} + +void LayershellWindowExtension::setAnchors(Anchors anchors) { + if (anchors != this->mAnchors) { + this->mAnchors = anchors; + if (this->surface != nullptr) this->surface->updateAnchors(); + emit this->anchorsChanged(); + } +} + +Anchors LayershellWindowExtension::anchors() const { return this->mAnchors; } + +void LayershellWindowExtension::setMargins(Margins margins) { + if (margins != this->mMargins) { + this->mMargins = margins; + if (this->surface != nullptr) this->surface->updateMargins(); + emit this->marginsChanged(); + } +} + +Margins LayershellWindowExtension::margins() const { return this->mMargins; } + +void LayershellWindowExtension::setExclusiveZone(qint32 exclusiveZone) { + if (exclusiveZone != this->mExclusiveZone) { + this->mExclusiveZone = exclusiveZone; + if (this->surface != nullptr) this->surface->updateExclusiveZone(); + emit this->exclusiveZoneChanged(); + } +} + +qint32 LayershellWindowExtension::exclusiveZone() const { return this->mExclusiveZone; } + +void LayershellWindowExtension::setLayer(Layer::Enum layer) { + if (layer != this->mLayer) { + this->mLayer = layer; + if (this->surface != nullptr) this->surface->updateLayer(); + emit this->layerChanged(); + } +} + +Layer::Enum LayershellWindowExtension::layer() const { return this->mLayer; } + +void LayershellWindowExtension::setKeyboardFocus(KeyboardFocus::Enum focus) { + if (focus != this->mKeyboardFocus) { + this->mKeyboardFocus = focus; + if (this->surface != nullptr) this->surface->updateKeyboardFocus(); + emit this->keyboardFocusChanged(); + } +} + +KeyboardFocus::Enum LayershellWindowExtension::keyboardFocus() const { + return this->mKeyboardFocus; +} + +void LayershellWindowExtension::setUseWindowScreen(bool value) { + this->useWindowScreen = value; // has no effect post configure +} + +void LayershellWindowExtension::setNamespace(QString ns) { + if (!this->isConfigured()) this->mNamespace = std::move(ns); +} + +QString LayershellWindowExtension::ns() const { return this->mNamespace; } + +bool LayershellWindowExtension::isConfigured() const { return this->surface != nullptr; } diff --git a/src/wayland/layershell.hpp b/src/wayland/layershell.hpp new file mode 100644 index 0000000..5ff1c62 --- /dev/null +++ b/src/wayland/layershell.hpp @@ -0,0 +1,114 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../core/panelinterface.hpp" + +namespace Layer { // NOLINT +Q_NAMESPACE; +QML_ELEMENT; + +enum Enum { + Background = 0, + /// Above background, usually below windows + Bottom = 1, + /// Commonly used for panels, app launchers, and docks. + /// Usually renders over normal windows and below fullscreen windows. + Top = 2, + /// Usually renders over fullscreen windows + Overlay = 3, +}; +Q_ENUM_NS(Enum); + +} // namespace Layer + +/// Type of keyboard focus that will be accepted by a [ShellWindow] +/// +/// [ShellWindow]: ../shellwindow +namespace KeyboardFocus { // NOLINT +Q_NAMESPACE; +QML_ELEMENT; + +enum Enum { + /// No keyboard input will be accepted. + None = 0, + /// Exclusive access to the keyboard, locking out all other windows. + Exclusive = 1, + /// Access to the keyboard as determined by the operating system. + /// + /// > [!WARNING] On some systems, `OnDemand` may cause the shell window to + /// > retain focus over another window unexpectedly. + /// > You should try `None` if you experience issues. + OnDemand = 2, +}; +Q_ENUM_NS(Enum); + +} // namespace KeyboardFocus + +class QSWaylandLayerSurface; + +class LayershellWindowExtension: public QObject { + Q_OBJECT; + +public: + LayershellWindowExtension(QObject* parent = nullptr): QObject(parent) {} + ~LayershellWindowExtension() override = default; + LayershellWindowExtension(LayershellWindowExtension&&) = delete; + LayershellWindowExtension(const LayershellWindowExtension&) = delete; + void operator=(LayershellWindowExtension&&) = delete; + void operator=(const LayershellWindowExtension&) = delete; + + // returns the layershell extension if attached, otherwise nullptr + static LayershellWindowExtension* get(QWindow* window); + + // Attach this layershell extension to the given window. + // The extension is reparented to the window and replaces any existing extensions. + // Returns false if the window cannot be used. + bool attach(QWindow* window); + + void setAnchors(Anchors anchors); + [[nodiscard]] Anchors anchors() const; + + void setMargins(Margins margins); + [[nodiscard]] Margins margins() const; + + void setExclusiveZone(qint32 exclusiveZone); + [[nodiscard]] qint32 exclusiveZone() const; + + void setLayer(Layer::Enum layer); + [[nodiscard]] Layer::Enum layer() const; + + void setKeyboardFocus(KeyboardFocus::Enum focus); + [[nodiscard]] KeyboardFocus::Enum keyboardFocus() const; + + // no effect if configured + void setUseWindowScreen(bool value); + void setNamespace(QString ns); + [[nodiscard]] QString ns() const; + [[nodiscard]] bool isConfigured() const; + +signals: + void anchorsChanged(); + void marginsChanged(); + void exclusiveZoneChanged(); + void layerChanged(); + void keyboardFocusChanged(); + +private: + // if configured the screen cannot be changed + QSWaylandLayerSurface* surface = nullptr; + + bool useWindowScreen = false; + Anchors mAnchors; + Margins mMargins; + qint32 mExclusiveZone = 0; + Layer::Enum mLayer = Layer::Top; + QString mNamespace = "quickshell"; + KeyboardFocus::Enum mKeyboardFocus = KeyboardFocus::None; + + friend class QSWaylandLayerSurface; +}; diff --git a/src/wayland/module.md b/src/wayland/module.md new file mode 100644 index 0000000..6abcee2 --- /dev/null +++ b/src/wayland/module.md @@ -0,0 +1,7 @@ +name = "QuickShell.Wayland" +description = "Wayland specific QuickShell types" +headers = [ + "layershell.hpp", + "waylandlayershell.hpp", +] +----- diff --git a/src/wayland/shell_integration.cpp b/src/wayland/shell_integration.cpp new file mode 100644 index 0000000..134d14d --- /dev/null +++ b/src/wayland/shell_integration.cpp @@ -0,0 +1,22 @@ +#include "shell_integration.hpp" + +#include +#include +#include + +#include "layer_surface.hpp" +#include "wayland-wlr-layer-shell-unstable-v1-client-protocol.h" + +QSWaylandLayerShellIntegration::QSWaylandLayerShellIntegration() + : QtWaylandClient::QWaylandShellIntegrationTemplate(4) {} + +QSWaylandLayerShellIntegration::~QSWaylandLayerShellIntegration() { + if (this->object() != nullptr) { + zwlr_layer_shell_v1_destroy(this->object()); + } +} + +QtWaylandClient::QWaylandShellSurface* +QSWaylandLayerShellIntegration::createShellSurface(QtWaylandClient::QWaylandWindow* window) { + return new QSWaylandLayerSurface(this, window); +} diff --git a/src/wayland/shell_integration.hpp b/src/wayland/shell_integration.hpp new file mode 100644 index 0000000..1004e43 --- /dev/null +++ b/src/wayland/shell_integration.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include +#include + +class QSWaylandLayerShellIntegration + : public QtWaylandClient::QWaylandShellIntegrationTemplate + , public QtWayland::zwlr_layer_shell_v1 { +public: + QSWaylandLayerShellIntegration(); + ~QSWaylandLayerShellIntegration() override; + QSWaylandLayerShellIntegration(QSWaylandLayerShellIntegration&&) = delete; + QSWaylandLayerShellIntegration(const QSWaylandLayerShellIntegration&) = delete; + void operator=(QSWaylandLayerShellIntegration&&) = delete; + void operator=(const QSWaylandLayerShellIntegration&) = delete; + + QtWaylandClient::QWaylandShellSurface* createShellSurface(QtWaylandClient::QWaylandWindow* window + ) override; +}; diff --git a/src/wayland/waylandlayershell.cpp b/src/wayland/waylandlayershell.cpp new file mode 100644 index 0000000..4e4f7a5 --- /dev/null +++ b/src/wayland/waylandlayershell.cpp @@ -0,0 +1,209 @@ +#include "waylandlayershell.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../core/panelinterface.hpp" +#include "../core/proxywindow.hpp" +#include "../core/qmlscreen.hpp" +#include "layershell.hpp" + +WaylandLayershell::WaylandLayershell(QObject* parent) + : ProxyWindowBase(parent) + , ext(new LayershellWindowExtension(this)) {} + +QQuickWindow* WaylandLayershell::createWindow(QObject* oldInstance) { + auto* old = qobject_cast(oldInstance); + QQuickWindow* window = nullptr; + + if (old == nullptr || old->window == nullptr) { + window = new QQuickWindow(); + } else { + window = old->disownWindow(); + + if (this->ext->attach(window)) { + return window; + } else { + window->deleteLater(); + window = new QQuickWindow(); + } + } + + if (!this->ext->attach(window)) { + qWarning() << "Could not attach Layershell extension to new QQUickWindow. Layer will not " + "behave correctly."; + } + + return window; +} + +void WaylandLayershell::setupWindow() { + this->ProxyWindowBase::setupWindow(); + + // clang-format off + QObject::connect(this->ext, &LayershellWindowExtension::layerChanged, this, &WaylandLayershell::layerChanged); + QObject::connect(this->ext, &LayershellWindowExtension::keyboardFocusChanged, this, &WaylandLayershell::keyboardFocusChanged); + QObject::connect(this->ext, &LayershellWindowExtension::anchorsChanged, this, &WaylandLayershell::anchorsChanged); + QObject::connect(this->ext, &LayershellWindowExtension::exclusiveZoneChanged, this, &WaylandLayershell::exclusiveZoneChanged); + QObject::connect(this->ext, &LayershellWindowExtension::marginsChanged, this, &WaylandLayershell::marginsChanged); + + QObject::connect(this, &ProxyWindowBase::widthChanged, this, &WaylandLayershell::updateAutoExclusion); + QObject::connect(this, &ProxyWindowBase::heightChanged, this, &WaylandLayershell::updateAutoExclusion); + QObject::connect(this, &WaylandLayershell::anchorsChanged, this, &WaylandLayershell::updateAutoExclusion); + // clang-format on + + this->updateAutoExclusion(); +} + +void WaylandLayershell::setWidth(qint32 width) { + this->mWidth = width; + + // only update the actual size if not blocked by anchors + if (!this->ext->anchors().horizontalConstraint()) { + this->ProxyWindowBase::setWidth(width); + } +} + +void WaylandLayershell::setHeight(qint32 height) { + this->mHeight = height; + + // only update the actual size if not blocked by anchors + if (!this->ext->anchors().verticalConstraint()) { + this->ProxyWindowBase::setHeight(height); + } +} + +void WaylandLayershell::setScreen(QuickShellScreenInfo* screen) { + this->ProxyWindowBase::setScreen(screen); + this->ext->setUseWindowScreen(screen != nullptr); +} + +// NOLINTBEGIN +#define extPair(type, get, set) \ + type WaylandLayershell::get() const { return this->ext->get(); } \ + void WaylandLayershell::set(type value) { this->ext->set(value); } + +extPair(Layer::Enum, layer, setLayer); +extPair(KeyboardFocus::Enum, keyboardFocus, setKeyboardFocus); +extPair(Margins, margins, setMargins); +// NOLINTEND + +Anchors WaylandLayershell::anchors() const { return this->ext->anchors(); } + +void WaylandLayershell::setAnchors(Anchors anchors) { + this->ext->setAnchors(anchors); + + // explicitly set width values are tracked so the entire screen isn't covered if an anchor is removed. + if (!anchors.horizontalConstraint()) this->ProxyWindowBase::setWidth(this->mWidth); + if (!anchors.verticalConstraint()) this->ProxyWindowBase::setHeight(this->mHeight); +} + +QString WaylandLayershell::ns() const { return this->ext->ns(); } + +void WaylandLayershell::setNamespace(QString ns) { + this->ext->setNamespace(std::move(ns)); + emit this->namespaceChanged(); +} + +qint32 WaylandLayershell::exclusiveZone() const { return this->ext->exclusiveZone(); } + +void WaylandLayershell::setExclusiveZone(qint32 exclusiveZone) { + this->mExclusiveZone = exclusiveZone; + this->setExclusionMode(ExclusionMode::Normal); + this->ext->setExclusiveZone(exclusiveZone); +} + +ExclusionMode::Enum WaylandLayershell::exclusionMode() const { return this->mExclusionMode; } + +void WaylandLayershell::setExclusionMode(ExclusionMode::Enum exclusionMode) { + this->mExclusionMode = exclusionMode; + if (exclusionMode == this->mExclusionMode) return; + + if (exclusionMode == ExclusionMode::Normal) { + this->ext->setExclusiveZone(this->mExclusiveZone); + } else if (exclusionMode == ExclusionMode::Ignore) { + this->ext->setExclusiveZone(-1); + } else { + this->setAutoExclusion(); + } +} + +void WaylandLayershell::setAutoExclusion() { + const auto anchors = this->anchors(); + auto zone = 0; + + if (anchors.horizontalConstraint()) zone = this->height(); + else if (anchors.verticalConstraint()) zone = this->width(); + + this->ext->setExclusiveZone(zone); +} + +void WaylandLayershell::updateAutoExclusion() { + if (this->mExclusionMode == ExclusionMode::Auto) { + this->setAutoExclusion(); + } +} + +WaylandLayershell* WaylandLayershell::qmlAttachedProperties(QObject* object) { + if (auto* obj = qobject_cast(object)) { + return obj->layer; + } else { + return nullptr; + } +} + +// WaylandPanelInterface + +WaylandPanelInterface::WaylandPanelInterface(QObject* parent) + : PanelWindowInterface(parent) + , layer(new WaylandLayershell(this)) { + + // clang-format off + QObject::connect(this->layer, &ProxyWindowBase::windowConnected, this, &WaylandPanelInterface::windowConnected); + QObject::connect(this->layer, &ProxyWindowBase::visibleChanged, this, &WaylandPanelInterface::visibleChanged); + QObject::connect(this->layer, &ProxyWindowBase::heightChanged, this, &WaylandPanelInterface::heightChanged); + QObject::connect(this->layer, &ProxyWindowBase::widthChanged, this, &WaylandPanelInterface::widthChanged); + QObject::connect(this->layer, &ProxyWindowBase::screenChanged, this, &WaylandPanelInterface::screenChanged); + QObject::connect(this->layer, &ProxyWindowBase::colorChanged, this, &WaylandPanelInterface::colorChanged); + QObject::connect(this->layer, &ProxyWindowBase::maskChanged, this, &WaylandPanelInterface::maskChanged); + + // panel specific + QObject::connect(this->layer, &WaylandLayershell::anchorsChanged, this, &WaylandPanelInterface::anchorsChanged); + QObject::connect(this->layer, &WaylandLayershell::marginsChanged, this, &WaylandPanelInterface::marginsChanged); + QObject::connect(this->layer, &WaylandLayershell::exclusiveZoneChanged, this, &WaylandPanelInterface::exclusiveZoneChanged); + QObject::connect(this->layer, &WaylandLayershell::exclusionModeChanged, this, &WaylandPanelInterface::exclusionModeChanged); + // clang-format on +} + +void WaylandPanelInterface::onReload(QObject* oldInstance) { + auto* old = qobject_cast(oldInstance); + this->layer->onReload(old != nullptr ? old->layer : nullptr); +} + +QQmlListProperty WaylandPanelInterface::data() { return this->layer->data(); } +QQuickItem* WaylandPanelInterface::contentItem() const { return this->layer->contentItem(); } + +// NOLINTBEGIN +#define proxyPair(type, get, set) \ + type WaylandPanelInterface::get() const { return this->layer->get(); } \ + void WaylandPanelInterface::set(type value) { this->layer->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); +// NOLINTEND diff --git a/src/wayland/waylandlayershell.hpp b/src/wayland/waylandlayershell.hpp new file mode 100644 index 0000000..793969c --- /dev/null +++ b/src/wayland/waylandlayershell.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../core/doc.hpp" +#include "../core/proxywindow.hpp" +#include "layershell.hpp" + +class WaylandLayershell: public ProxyWindowBase { + QSDOC_BASECLASS(PanelWindowInterface); + // clang-format off + Q_OBJECT; + /// The shell layer the window sits in. Defaults to `Layer.Top`. + Q_PROPERTY(Layer::Enum layer READ layer WRITE setLayer NOTIFY layerChanged); + /// Similar to the class property of windows. Can be used to identify the window to external tools. + /// + /// Cannot be set after windowConnected. + Q_PROPERTY(QString namespace READ ns WRITE setNamespace NOTIFY namespaceChanged); + /// The degree of keyboard focus taken. Defaults to `KeyboardFocus.None`. + Q_PROPERTY(KeyboardFocus::Enum keyboardFocus READ keyboardFocus WRITE setKeyboardFocus NOTIFY keyboardFocusChanged); + + 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); + QML_ATTACHED(WaylandLayershell); + QML_ELEMENT; + // clang-format on + +public: + explicit WaylandLayershell(QObject* parent = nullptr); + + QQuickWindow* createWindow(QObject* oldInstance) override; + void setupWindow() override; + + void setWidth(qint32 width) override; + void setHeight(qint32 height) override; + + void setScreen(QuickShellScreenInfo* screen) override; + + [[nodiscard]] Layer::Enum layer() const; + void setLayer(Layer::Enum layer); // NOLINT + + [[nodiscard]] QString ns() const; + void setNamespace(QString ns); + + [[nodiscard]] KeyboardFocus::Enum keyboardFocus() const; + void setKeyboardFocus(KeyboardFocus::Enum focus); // NOLINT + + [[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); // NOLINT + + static WaylandLayershell* qmlAttachedProperties(QObject* object); + +signals: + void layerChanged(); + void namespaceChanged(); + void keyboardFocusChanged(); + QSDOC_HIDE void anchorsChanged(); + QSDOC_HIDE void exclusiveZoneChanged(); + QSDOC_HIDE void exclusionModeChanged(); + QSDOC_HIDE void marginsChanged(); + +private slots: + void updateAutoExclusion(); + +private: + void setAutoExclusion(); + + LayershellWindowExtension* ext; + + ExclusionMode::Enum mExclusionMode = ExclusionMode::Auto; + qint32 mExclusiveZone = 0; +}; + +class WaylandPanelInterface: public PanelWindowInterface { + Q_OBJECT; + +public: + explicit WaylandPanelInterface(QObject* parent = nullptr); + + void onReload(QObject* oldInstance) override; + + [[nodiscard]] QQuickItem* contentItem() const override; + + // NOLINTBEGIN + [[nodiscard]] bool isVisible() 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; + // NOLINTEND + +private: + WaylandLayershell* layer; + + friend class WaylandLayershell; +}; diff --git a/src/wayland/wl-proto/wlr-layer-shell-unstable-v1.xml b/src/wayland/wl-proto/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 0000000..283f391 --- /dev/null +++ b/src/wayland/wl-proto/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,407 @@ + + + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + Attaching a null buffer to a layer surface unmaps it. + + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Types of keyboard interaction possible for layer shell surfaces. The + rationale for this is twofold: (1) some applications are not interested + in keyboard events and not allowing them to be focused can improve the + desktop experience; (2) some applications will want to take exclusive + keyboard focus. + + + + + This value indicates that this surface is not interested in keyboard + events and the compositor should never assign it the keyboard focus. + + This is the default value, set for newly created layer shell surfaces. + + This is useful for e.g. desktop widgets that display information or + only have interaction with non-keyboard input devices. + + + + + Request exclusive keyboard focus if this surface is above the shell surface layer. + + For the top and overlay layers, the seat will always give + exclusive keyboard focus to the top-most layer which has keyboard + interactivity set to exclusive. If this layer contains multiple + surfaces with keyboard interactivity set to exclusive, the compositor + determines the one receiving keyboard events in an implementation- + defined manner. In this case, no guarantee is made when this surface + will receive keyboard focus (if ever). + + For the bottom and background layers, the compositor is allowed to use + normal focus semantics. + + This setting is mainly intended for applications that need to ensure + they receive all keyboard events, such as a lock screen or a password + prompt. + + + + + This requests the compositor to allow this surface to be focused and + unfocused by the user in an implementation-defined manner. The user + should be able to unfocus this surface even regardless of the layer + it is on. + + Typically, the compositor will want to use its normal mechanism to + manage keyboard focus between layer shell surfaces with this setting + and regular toplevels on the desktop layer (e.g. click to focus). + Nevertheless, it is possible for a compositor to require a special + interaction to focus or unfocus layer shell surfaces (e.g. requiring + a click even if focus follows the mouse normally, or providing a + keybinding to switch focus between layers). + + This setting is mainly intended for desktop shell components (e.g. + panels) that allow keyboard interaction. Using this option can allow + implementing a desktop shell that can be fully usable without the + mouse. + + + + + + + Set how keyboard events are delivered to this surface. By default, + layer shell surfaces do not receive keyboard events; this request can + be used to change this. + + This setting is inherited by child surfaces set by the get_popup + request. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Keyboard interactivity is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + + + + + + + Change the layer that the surface is rendered on. + + Layer is double-buffered, see wl_surface.commit. + + + + + + + + + Requests an edge for the exclusive zone to apply. The exclusive + edge will be automatically deduced from anchor points when possible, + but when the surface is anchored to a corner, it will be necessary + to set it explicitly to disambiguate, as it is not possible to deduce + which one of the two corner edges should be used. + + The edge must be one the surface is anchored to, otherwise the + invalid_exclusive_edge protocol error will be raised. + + + + +