diff --git a/.clang-tidy b/.clang-tidy index 6362e662..1da445cd 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -5,6 +5,7 @@ Checks: > -*, bugprone-*, -bugprone-easily-swappable-parameters, + -bugprone-forward-declararion-namespace, concurrency-*, cppcoreguidelines-*, -cppcoreguidelines-owning-memory, diff --git a/.gitignore b/.gitignore index 1933837e..dcdefe39 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# related repos +/docs +/examples + # build files /result /build/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 74013769..00000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "docs"] - path = docs - url = https://git.outfoxxed.me/outfoxxed/quickshell-docs -[submodule "examples"] - path = examples - url = https://git.outfoxxed.me/outfoxxed/quickshell-examples diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 00000000..3c3e7125 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,165 @@ +# Build instructions +Instructions for building from source and distro packagers. We highly recommend +distro packagers read through this page fully. + +## Dependencies +Quickshell has a set of base dependencies you will always need, names vary by distro: + +- `cmake` +- `qt6base` +- `qt6declarative` +- `pkg-config` + +We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and +svg icons will not work, including system ones. + +At least Qt 6.6 is required. + +All features are enabled by default and some have their own dependencies. + +##### Additional note to packagers: +If your package manager supports enabling some features but not others, +we recommend not exposing the subfeatures and just the main ones that introduce +new dependencies: `wayland`, `x11`, `pipewire`, `hyprland` + +### Jemalloc +We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused +by the QML engine, which results in much lower memory usage. Without this you +will get a perceived memory leak. + +To disable: `-DUSE_JEMALLOC=OFF` + +Dependencies: `jemalloc` + +### Unix Sockets +This feature allows interaction with unix sockets and creating socket servers +which is useful for IPC and has no additional dependencies. + +WARNING: Disabling unix sockets will NOT make it safe to run arbitrary code using quickshell. +There are many vectors which mallicious code can use to escape into your system. + +To disable: `-DSOCKETS=OFF` + +### Wayland +This feature enables wayland support. Subfeatures exist for each particular wayland integration. + +WARNING: Wayland integration relies on features that are not part of the public Qt API and which +may break in minor releases. Updating quickshell's dependencies without ensuring without ensuring +that the current Qt version is supported WILL result in quickshell failing to build or misbehaving +at runtime. + +Currently supported Qt versions: `6.6`, `6.7`. + +To disable: `-DWAYLAND=OFF` + +Dependencies: + - `qt6wayland` + - `wayland` (libwayland-client) + - `wayland-scanner` (may be part of your distro's wayland package) + - `wayland-protocols` + +#### Wlroots Layershell +Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol, +enabling use cases such as bars overlays and backgrounds. +This feature has no extra dependencies. + +To disable: `-DWAYLAND_WLR_LAYERSHELL=OFF` + +[zwlr-layer-shell-v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1 + +#### Session Lock +Enables session lock support through the [ext-session-lock-v1] protocol, +which allows quickshell to be used as a session lock under compatible wayland compositors. + +To disable: `-DWAYLAND_SESSION_LOCK=OFF` + +[ext-session-lock-v1]: https://wayland.app/protocols/ext-session-lock-v1 + + +#### Foreign Toplevel Management +Enables management of windows of other clients through the [zwlr-foreign-toplevel-management-v1] protocol, +which allows quickshell to be used as a session lock under compatible wayland compositors. + +[zwlr-foreign-toplevel-management-v1]: https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1 + +To disable: `-DWAYLAND_TOPLEVEL_MANAGEMENT=OFF` + +### X11 +This feature enables x11 support. Currently this implements panel windows for X11 similarly +to the wlroots layershell above. + +To disable: `-DX11=OFF` + +Dependencies: `libxcb` + +### Pipewire +This features enables viewing and management of pipewire nodes. + +To disable: `-DSERVICE_PIPEWIRE=OFF` + +Dependencies: `libpipewire` + +### StatusNotifier / System Tray +This feature enables system tray support using the status notifier dbus protocol. + +To disable: `-DSERVICE_STATUS_NOTIFIER=OFF` + +Dependencies: `qt6dbus` (usually part of qt6base) + +### MPRIS +This feature enables access to MPRIS compatible media players using its dbus protocol. + +To disable: `-DSERVICE_MPRIS=OFF` + +Dependencies: `qt6dbus` (usually part of qt6base) + +### Hyprland +This feature enables hyprland specific integrations. It requires wayland support +but has no extra dependencies. + +To disable: `-DHYPRLAND=OFF` + +#### Hyprland Global Shortcuts +Enables creation of global shortcuts under hyprland through the [hyprland-global-shortcuts-v1] +protocol. Generally a much nicer alternative to using unix sockets to implement the same thing. +This feature has no extra dependencies. + +To disable: `-DHYPRLAND_GLOBAL_SHORTCUTS=OFF` + +[hyprland-global-shortcuts-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml + +#### Hyprland Focus Grab +Enables windows to grab focus similarly to a context menu undr hyprland through the +[hyprland-focus-grab-v1] protocol. This feature has no extra dependencies. + +To disable: `-DHYPRLAND_FOCUS_GRAB=OFF` + +[hyprland-focus-grab-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-focus-grab-v1.xml + +## Building +*For developers and prospective contributors: See [CONTRIBUTING.md](CONTRIBUTING.md).* + +We highly recommend using `ninja` to run the build, but you can use makefiles if you must. + +#### Configuring the build +```sh +$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here] +``` + +Note that features you do not supply dependencies for MUST be disabled with their associated flags +or quickshell will fail to build. + +Additionally, note that clang builds much faster than gcc if you care. + +You may disable debug information but it's only a couple megabytes and is extremely helpful +for helping us fix problems when they do arise. + +#### Building +```sh +$ cmake --build build +``` + +#### Installing +```sh +$ cmake --install build +``` diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d17758b..7af6b6cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,13 +9,15 @@ option(BUILD_TESTING "Build tests" OFF) option(ASAN "Enable ASAN" OFF) option(FRAME_POINTERS "Always keep frame pointers" ${ASAN}) -option(NVIDIA_COMPAT "Workarounds for nvidia gpus" OFF) +option(USE_JEMALLOC "Use jemalloc over the system malloc implementation" ON) 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(WAYLAND_TOPLEVEL_MANAGEMENT "Support the zwlr_foreign_toplevel_management_v1 wayland protocol" ON) option(X11 "Enable X11 support" ON) option(HYPRLAND "Support hyprland specific features" ON) +option(HYPRLAND_IPC "Hyprland IPC" ON) option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON) option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON) option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) @@ -23,13 +25,14 @@ option(SERVICE_PIPEWIRE "PipeWire service" ON) option(SERVICE_MPRIS "Mpris service" ON) message(STATUS "Quickshell configuration") -message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}") +message(STATUS " Jemalloc: ${USE_JEMALLOC}") message(STATUS " Build tests: ${BUILD_TESTING}") message(STATUS " Sockets: ${SOCKETS}") message(STATUS " Wayland: ${WAYLAND}") if (WAYLAND) message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}") message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}") + message(STATUS " Toplevel Management: ${WAYLAND_TOPLEVEL_MANAGEMENT}") endif () message(STATUS " X11: ${X11}") message(STATUS " Services") @@ -38,6 +41,7 @@ message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") message(STATUS " Mpris: ${SERVICE_MPRIS}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) + message(STATUS " IPC: ${HYPRLAND_IPC}") message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}") message(STATUS " Global Shortcuts: ${HYPRLAND_GLOBAL_SHORTCUTS}") endif() @@ -132,8 +136,11 @@ function (qs_pch target) endif() endfunction() -if (NVIDIA_COMPAT) - add_compile_definitions(NVIDIA_COMPAT) -endif() - add_subdirectory(src) + +if (USE_JEMALLOC) + find_package(PkgConfig REQUIRED) + # IMPORTED_TARGET not working for some reason + pkg_check_modules(JEMALLOC REQUIRED jemalloc) + target_link_libraries(quickshell PRIVATE ${JEMALLOC_LIBRARIES}) +endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6fdef09c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,99 @@ +# Contributing / Development +Instructions for development setup and upstreaming patches. + +If you just want to build or package quickshell see [BUILD.md](BUILD.md). + +## Development + +Install the dependencies listed in [BUILD.md](BUILD.md). +You probably want all of them even if you don't use all of them +to ensure tests work correctly and avoid passing a bunch of configure +flags when you need to wipe the build directory. + +Quickshell also uses `just` for common development command aliases. + +The dependencies are also available as a nix shell or nix flake which we recommend +using with nix-direnv. + +Common aliases: +- `just configure [ [extra cmake args]]` (note that you must specify debug/release to specify extra args) +- `just build` - runs the build, configuring if not configured already. +- `just run [args]` - runs quickshell with the given arguments +- `just clean` - clean up build artifacts. `just clean build` is somewhat common. + +### Formatting +All contributions should be formatted similarly to what already exists. +Group related functionality together. + +Run the formatter using `just fmt`. +If the results look stupid, fix the clang-format file if possible, +or disable clang-format in the affected area +using `// clang-format off` and `// clang-format on`. + +### Linter +All contributions should pass the linter. + +Note that running the linter requires disabling precompiled +headers and including the test codepaths: +```sh +$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON +$ just lint +``` + +If the linter is complaining about something that you think it should not, +please disable the lint in your MR and explain your reasoning. + +### Tests +If you feel like the feature you are working on is very complex or likely to break, +please write some tests. We will ask you to directly if you send in an MR for an +overly complex or breakable feature. + +At least all tests that passed before your changes should still be passing +by the time your contribution is ready. + +You can run the tests using `just test` but you must enable them first +using `-DBUILD_TESTING=ON`. + +### Documentation +Most of quickshell's documentation is automatically generated from the source code. +You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser +cannot handle random line breaks and will usually require you to disable clang-format if the +lines are too long. + +Before submitting an MR, if adding new features please make sure the documentation is generated +reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo. + +Doc comments take the form `///` or `///!` (summary) and work with markdown. +Look at existing code for how it works. + +Quickshell modules additionally have a `module.md` file which contains a summary, description, +and list of headers to scan for documentation. + +## Contributing + +### Commits +Please structure your commit messages as `scope[!]: commit` where +the scope is something like `core` or `service/mpris`. (pick what has been +used historically or what makes sense if new.) Add `!` for changes that break +existing APIs or functionality. + +Commit descriptions should contain a summary of the changes if they are not +sufficiently addressed in the commit message. + +Please squash/rebase additions or edits to previous changes and follow the +commit style to keep the history easily searchable at a glance. +Depending on the change, it is often reasonable to squash it into just +a single commit. (If you do not follow this we will squash your changes +for you.) + +### Sending patches +You may contribute by submitting a pull request on github, asking for +an account on our git server, or emailing patches / git bundles +directly to `outfoxxed@outfoxxed.me`. + +### Getting help +If you're getting stuck, you can come talk to us in the +[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me) +for help on implementation, conventions, etc. +Feel free to ask for advice early in your implementation if you are +unsure. diff --git a/README.md b/README.md index c17af3a8..4def09ed 100644 --- a/README.md +++ b/README.md @@ -11,22 +11,9 @@ Hosted on: [outfoxxed's gitea], [github] Documentation available at [quickshell.outfoxxed.me](https://quickshell.outfoxxed.me) or can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quickshell-docs) repo. -Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples) +Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples) repo. -Both the documentation and examples are included as submodules with revisions that work with the current -version of quickshell. - -You can clone everything with -``` -$ git clone --recursive https://git.outfoxxed.me/outfoxxed/quickshell.git -``` - -Or clone missing submodules later with -``` -$ git submodule update --init --recursive -``` - # Installation ## Nix @@ -48,81 +35,33 @@ This repo has a nix flake you can use to install the package directly: Quickshell's binary is available at `quickshell.packages..default` to be added to lists such as `environment.systemPackages` or `home.packages`. -`quickshell.packages..nvidia` is also available for nvidia users which fixes some -common crashes. +The package contains several features detailed in [BUILD.md](BUILD.md) which can be enabled +or disabled with overrides: + +```nix +quickshell.packages..default.override { + withJemalloc = true; + withQtSvg = true; + withWayland = true; + withX11 = true; + withPipewire = true; + withHyprland = true; +} +``` Note: by default this package is built with clang as it is significantly faster. -## Manual +## Arch (AUR) +Quickshell has a third party [AUR package] available under the same name. +As is usual with the AUR it is not maintained by us and should be looked over before use. -If not using nix, you'll have to build from source. +[AUR package]: https://aur.archlinux.org/packages/quickshell -### Dependencies -To build quickshell at all, you will need the following packages (names may vary by distro) +## Anything else +See [BUILD.md](BUILD.md) for instructions on building and packaging quickshell. -- just -- cmake -- 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: -```sh -$ just release -``` - -If running an nvidia GPU, instead run: -```sh -$ just configure release -DNVIDIA_COMPAT=ON -$ just build -``` - -(These commands are just aliases for cmake commands you can run directly, -see the Justfile for more information.) - -If you have all the dependencies installed and they are in expected -locations this will build correctly. - -To install to /usr/local/bin run as root (usually `sudo`) in the same folder: -``` -$ just install -``` - -### Building (Nix) - -You can build directly using the provided nix flake or nix package. -``` -nix build -nix build -f package.nix # calls default.nix with a basic callPackage expression -``` - -# Development - -For nix there is a devshell available from `shell.nix` and as a devShell -output from the flake. - -The Justfile contains various useful aliases: -- `just configure [ [extra cmake args]]` -- `just build` (runs configure for debug mode) -- `just run [args]` -- `just clean` -- `just test [args]` (configure with `-DBUILD_TESTING=ON` first) -- `just fmt` -- `just lint` +# Contributing / Development +See [CONTRIBUTING.md](CONTRIBUTING.md) for details. #### License diff --git a/default.nix b/default.nix index 0985d843..01624c4a 100644 --- a/default.nix +++ b/default.nix @@ -8,6 +8,7 @@ cmake, ninja, qt6, + jemalloc, wayland, wayland-protocols, xorg, @@ -25,11 +26,12 @@ else "unknown"), debug ? false, - enableWayland ? true, - enableX11 ? true, - enablePipewire ? true, - nvidiaCompat ? false, - svgSupport ? true, # you almost always want this + withJemalloc ? true, # masks heap fragmentation + withQtSvg ? true, + withWayland ? true, + withX11 ? true, + withPipewire ? true, + withHyprland ? true, }: buildStdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; version = "0.1.0"; @@ -39,8 +41,8 @@ cmake ninja qt6.wrapQtAppsHook - ] ++ (lib.optionals enableWayland [ pkg-config + ] ++ (lib.optionals withWayland [ wayland-protocols wayland-scanner ]); @@ -49,12 +51,13 @@ qt6.qtbase qt6.qtdeclarative ] - ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ]) - ++ (lib.optionals enableX11 [ xorg.libxcb ]) - ++ (lib.optionals svgSupport [ qt6.qtsvg ]) - ++ (lib.optionals enablePipewire [ pipewire ]); + ++ (lib.optional withJemalloc jemalloc) + ++ (lib.optional withQtSvg qt6.qtsvg) + ++ (lib.optionals withWayland [ qt6.qtwayland wayland ]) + ++ (lib.optional withX11 xorg.libxcb) + ++ (lib.optional withPipewire pipewire); - QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; + QTWAYLANDSCANNER = lib.optionalString withWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; configurePhase = let cmakeBuildType = if debug @@ -67,9 +70,11 @@ cmakeFlags = [ "-DGIT_REVISION=${gitRev}" - ] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF" - ++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON" - ++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF"; + ] + ++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF" + ++ lib.optional (!withWayland) "-DWAYLAND=OFF" + ++ lib.optional (!withPipewire) "-DSERVICE_PIPEWIRE=OFF" + ++ lib.optional (!withHyprland) "-DHYPRLAND=OFF"; buildPhase = "ninjaBuildPhase"; enableParallelBuilding = true; diff --git a/docs b/docs deleted file mode 160000 index ff5da84a..00000000 --- a/docs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ff5da84a8b258a9b2caaf978ddb6de23635d8903 diff --git a/examples b/examples deleted file mode 160000 index b9e744b5..00000000 --- a/examples +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b9e744b50673304dfddb68f3da2a2e906d028b96 diff --git a/flake.nix b/flake.nix index 5bb5069e..a0bc18d4 100644 --- a/flake.nix +++ b/flake.nix @@ -12,10 +12,8 @@ quickshell = pkgs.callPackage ./default.nix { gitRev = self.rev or self.dirtyRev; }; - quickshell-nvidia = quickshell.override { nvidiaCompat = true; }; default = quickshell; - nvidia = quickshell-nvidia; }); devShells = forEachSystem (system: pkgs: rec { diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b40b807f..24d2e685 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -26,6 +26,8 @@ qt_add_library(quickshell-core STATIC imageprovider.cpp transformwatcher.cpp boundcomponent.cpp + model.cpp + elapsedtimer.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/doc.hpp b/src/core/doc.hpp index b619b0a6..e1f2ee4c 100644 --- a/src/core/doc.hpp +++ b/src/core/doc.hpp @@ -10,5 +10,8 @@ #define QSDOC_ELEMENT #define QSDOC_NAMED_ELEMENT(name) +// change the cname used for this type +#define QSDOC_CNAME(name) + // overridden properties #define QSDOC_PROPERTY_OVERRIDE(...) diff --git a/src/core/elapsedtimer.cpp b/src/core/elapsedtimer.cpp new file mode 100644 index 00000000..91321122 --- /dev/null +++ b/src/core/elapsedtimer.cpp @@ -0,0 +1,22 @@ +#include "elapsedtimer.hpp" + +#include + +ElapsedTimer::ElapsedTimer() { this->timer.start(); } + +qreal ElapsedTimer::elapsed() { return static_cast(this->elapsedNs()) / 1000000000.0; } + +qreal ElapsedTimer::restart() { return static_cast(this->restartNs()) / 1000000000.0; } + +qint64 ElapsedTimer::elapsedMs() { return this->timer.elapsed(); } + +qint64 ElapsedTimer::restartMs() { return this->timer.restart(); } + +qint64 ElapsedTimer::elapsedNs() { return this->timer.nsecsElapsed(); } + +qint64 ElapsedTimer::restartNs() { + // see qelapsedtimer.cpp + auto old = this->timer; + this->timer.start(); + return old.durationTo(this->timer).count(); +} diff --git a/src/core/elapsedtimer.hpp b/src/core/elapsedtimer.hpp new file mode 100644 index 00000000..85850963 --- /dev/null +++ b/src/core/elapsedtimer.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include + +///! Measures time between events +/// The ElapsedTimer measures time since its last restart, and is useful +/// for determining the time between events that don't supply it. +class ElapsedTimer: public QObject { + Q_OBJECT; + QML_ELEMENT; + +public: + explicit ElapsedTimer(); + + /// Return the number of seconds since the timer was last + /// started or restarted, with nanosecond precision. + Q_INVOKABLE qreal elapsed(); + + /// Restart the timer, returning the number of seconds since + /// the timer was last started or restarted, with nanosecond precision. + Q_INVOKABLE qreal restart(); + + /// Return the number of milliseconds since the timer was last + /// started or restarted. + Q_INVOKABLE qint64 elapsedMs(); + + /// Restart the timer, returning the number of milliseconds since + /// the timer was last started or restarted. + Q_INVOKABLE qint64 restartMs(); + + /// Return the number of nanoseconds since the timer was last + /// started or restarted. + Q_INVOKABLE qint64 elapsedNs(); + + /// Restart the timer, returning the number of nanoseconds since + /// the timer was last started or restarted. + Q_INVOKABLE qint64 restartNs(); + +private: + QElapsedTimer timer; +}; diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 77e4a9cb..e43db6ee 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -12,7 +14,6 @@ #include #include #include -#include #include #include "iconimageprovider.hpp" @@ -25,8 +26,10 @@ static QHash g_generations; // NOLINT -EngineGeneration::EngineGeneration(QmlScanner scanner) - : scanner(std::move(scanner)) +EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) + : rootPath(rootPath) + , scanner(std::move(scanner)) + , urlInterceptor(this->rootPath) , interceptNetFactory(this->scanner.qmldirIntercepts) , engine(new QQmlEngine()) { g_generations.insert(this->engine, this); @@ -43,36 +46,41 @@ EngineGeneration::EngineGeneration(QmlScanner scanner) } EngineGeneration::~EngineGeneration() { - g_generations.remove(this->engine); - delete this->engine; + if (this->engine != nullptr) { + qFatal() << this << "destroyed without calling destroy()"; + } } void EngineGeneration::destroy() { - // Multiple generations can detect a reload at the same time. - delete this->watcher; - this->watcher = nullptr; + if (this->watcher != nullptr) { + // Multiple generations can detect a reload at the same time. + QObject::disconnect(this->watcher, nullptr, this, nullptr); + this->watcher->deleteLater(); + this->watcher = nullptr; + } - // Yes all of this is actually necessary. - if (this->engine != nullptr && this->root != nullptr) { + if (this->root != nullptr) { QObject::connect(this->root, &QObject::destroyed, this, [this]() { - // The timer seems to fix *one* of the possible qml item destructor crashes. - QTimer::singleShot(0, [this]() { - // Garbage is not collected during engine destruction. - this->engine->collectGarbage(); + // prevent further js execution between garbage collection and engine destruction. + this->engine->setInterrupted(true); - QObject::connect(this->engine, &QObject::destroyed, this, [this]() { delete this; }); + g_generations.remove(this->engine); - // Even after all of that there's still multiple failing assertions and segfaults. - // Pray you don't hit one. - // Note: it appeats *some* of the crashes are related to values owned by the generation. - // Test by commenting the connect() above. - this->engine->deleteLater(); - this->engine = nullptr; - }); + // Garbage is not collected during engine destruction. + this->engine->collectGarbage(); + + delete this->engine; + this->engine = nullptr; + delete this; }); this->root->deleteLater(); this->root = nullptr; + } else { + // the engine has never been used, no need to clean up + delete this->engine; + this->engine = nullptr; + delete this; } } @@ -117,13 +125,21 @@ void EngineGeneration::setWatchingFiles(bool watching) { for (auto& file: this->scanner.scannedFiles) { this->watcher->addPath(file); + this->watcher->addPath(QFileInfo(file).dir().absolutePath()); } QObject::connect( this->watcher, &QFileSystemWatcher::fileChanged, this, - &EngineGeneration::filesChanged + &EngineGeneration::onFileChanged + ); + + QObject::connect( + this->watcher, + &QFileSystemWatcher::directoryChanged, + this, + &EngineGeneration::onDirectoryChanged ); } } else { @@ -134,6 +150,24 @@ void EngineGeneration::setWatchingFiles(bool watching) { } } +void EngineGeneration::onFileChanged(const QString& name) { + if (!this->watcher->files().contains(name)) { + this->deletedWatchedFiles.push_back(name); + } else { + emit this->filesChanged(); + } +} + +void EngineGeneration::onDirectoryChanged() { + // try to find any files that were just deleted from a replace operation + for (auto& file: this->deletedWatchedFiles) { + if (QFileInfo(file).exists()) { + emit this->filesChanged(); + break; + } + } +} + void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) { auto* obj = dynamic_cast(controller); @@ -235,12 +269,16 @@ void EngineGeneration::assignIncubationController() { this->engine->setIncubationController(controller); } +EngineGeneration* EngineGeneration::findEngineGeneration(QQmlEngine* engine) { + return g_generations.value(engine); +} + EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) { while (object != nullptr) { auto* context = QQmlEngine::contextForObject(object); if (context != nullptr) { - if (auto* generation = g_generations.value(context->engine())) { + if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) { return generation; } } diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 11ebf0be..f757113e 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -1,9 +1,11 @@ #pragma once #include +#include #include #include #include +#include #include #include @@ -14,12 +16,13 @@ #include "singleton.hpp" class RootWrapper; +class QuickshellGlobal; class EngineGeneration: public QObject { Q_OBJECT; public: - explicit EngineGeneration(QmlScanner scanner); + explicit EngineGeneration(const QDir& rootPath, QmlScanner scanner); ~EngineGeneration() override; Q_DISABLE_COPY_MOVE(EngineGeneration); @@ -30,9 +33,11 @@ public: void registerIncubationController(QQmlIncubationController* controller); void deregisterIncubationController(QQmlIncubationController* controller); + static EngineGeneration* findEngineGeneration(QQmlEngine* engine); static EngineGeneration* findObjectGeneration(QObject* object); RootWrapper* wrapper = nullptr; + QDir rootPath; QmlScanner scanner; QsUrlInterceptor urlInterceptor; QsInterceptNetworkAccessManagerFactory interceptNetFactory; @@ -40,8 +45,10 @@ public: ShellRoot* root = nullptr; SingletonRegistry singletonRegistry; QFileSystemWatcher* watcher = nullptr; + QVector deletedWatchedFiles; DelayedQmlIncubationController delayedIncubationController; bool reloadComplete = false; + QuickshellGlobal* qsgInstance = nullptr; void destroy(); @@ -50,6 +57,8 @@ signals: void reloadFinished(); private slots: + void onFileChanged(const QString& name); + void onDirectoryChanged(); void incubationControllerDestroyed(); private: diff --git a/src/core/main.cpp b/src/core/main.cpp index 2cfd4d9c..220bde30 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,9 @@ int qs_main(int argc, char** argv) { auto desktopSettingsAware = true; QHash envOverrides; + int debugPort = -1; + bool waitForDebug = false; + { const auto app = QCoreApplication(argc, argv); QCoreApplication::setApplicationName("quickshell"); @@ -44,6 +48,8 @@ int qs_main(int argc, char** argv) { auto configOption = QCommandLineOption({"c", "config"}, "Name of a configuration in the manifest.", "name"); auto pathOption = QCommandLineOption({"p", "path"}, "Path to a configuration file.", "path"); auto workdirOption = QCommandLineOption({"d", "workdir"}, "Initial working directory.", "path"); + auto debugPortOption = QCommandLineOption("debugport", "Enable the QML debugger.", "port"); + auto debugWaitOption = QCommandLineOption("waitfordebug", "Wait for debugger connection before launching."); // clang-format on parser.addOption(currentOption); @@ -51,8 +57,30 @@ int qs_main(int argc, char** argv) { parser.addOption(configOption); parser.addOption(pathOption); parser.addOption(workdirOption); + parser.addOption(debugPortOption); + parser.addOption(debugWaitOption); parser.process(app); + auto debugPortStr = parser.value(debugPortOption); + if (!debugPortStr.isEmpty()) { + auto ok = false; + debugPort = debugPortStr.toInt(&ok); + + if (!ok) { + qCritical() << "Debug port must be a valid port number."; + return -1; + } + } + + if (parser.isSet(debugWaitOption)) { + if (debugPort == -1) { + qCritical() << "Cannot wait for debugger without a debug port set."; + return -1; + } + + waitForDebug = true; + } + { auto printCurrent = parser.isSet(currentOption); @@ -298,6 +326,13 @@ int qs_main(int argc, char** argv) { qputenv(var.toUtf8(), val.toUtf8()); } + // The simple animation driver seems to work far better than the default one + // when more than one window is in use, and even with a single window appears + // to improve animation quality. + if (!qEnvironmentVariableIsSet("QSG_USE_SIMPLE_ANIMATION_DRIVER")) { + qputenv("QSG_USE_SIMPLE_ANIMATION_DRIVER", "1"); + } + QGuiApplication::setDesktopSettingsAware(desktopSettingsAware); QGuiApplication* app = nullptr; @@ -308,6 +343,13 @@ int qs_main(int argc, char** argv) { app = new QGuiApplication(argc, argv); } + if (debugPort != -1) { + QQmlDebuggingEnabler::enableDebugging(true); + auto wait = waitForDebug ? QQmlDebuggingEnabler::WaitForClient + : QQmlDebuggingEnabler::DoNotWaitForClient; + QQmlDebuggingEnabler::startTcpDebugServer(debugPort, wait); + } + if (!workingDirectory.isEmpty()) { QDir::setCurrent(workingDirectory); } diff --git a/src/core/model.cpp b/src/core/model.cpp new file mode 100644 index 00000000..64f7d765 --- /dev/null +++ b/src/core/model.cpp @@ -0,0 +1,74 @@ +#include "model.hpp" + +#include +#include +#include +#include +#include +#include +#include + +qint32 UntypedObjectModel::rowCount(const QModelIndex& parent) const { + if (parent != QModelIndex()) return 0; + return static_cast(this->valuesList.length()); +} + +QVariant UntypedObjectModel::data(const QModelIndex& index, qint32 role) const { + if (role != 0) return QVariant(); + return QVariant::fromValue(this->valuesList.at(index.row())); +} + +QHash UntypedObjectModel::roleNames() const { return {{0, "modelData"}}; } + +QQmlListProperty UntypedObjectModel::values() { + return QQmlListProperty( + this, + nullptr, + &UntypedObjectModel::valuesCount, + &UntypedObjectModel::valueAt + ); +} + +qsizetype UntypedObjectModel::valuesCount(QQmlListProperty* property) { + return static_cast(property->object)->valuesList.count(); // NOLINT +} + +QObject* UntypedObjectModel::valueAt(QQmlListProperty* property, qsizetype index) { + return static_cast(property->object)->valuesList.at(index); // NOLINT +} + +void UntypedObjectModel::insertObject(QObject* object, qsizetype index) { + auto iindex = index == -1 ? this->valuesList.length() : index; + emit this->objectInsertedPre(object, index); + + auto intIndex = static_cast(iindex); + this->beginInsertRows(QModelIndex(), intIndex, intIndex); + this->valuesList.insert(iindex, object); + this->endInsertRows(); + + emit this->valuesChanged(); + emit this->objectInsertedPost(object, index); +} + +void UntypedObjectModel::removeAt(qsizetype index) { + auto* object = this->valuesList.at(index); + emit this->objectRemovedPre(object, index); + + auto intIndex = static_cast(index); + this->beginRemoveRows(QModelIndex(), intIndex, intIndex); + this->valuesList.removeAt(index); + this->endRemoveRows(); + + emit this->valuesChanged(); + emit this->objectRemovedPost(object, index); +} + +bool UntypedObjectModel::removeObject(const QObject* object) { + auto index = this->valuesList.indexOf(object); + if (index == -1) return false; + + this->removeAt(index); + return true; +} + +qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); } diff --git a/src/core/model.hpp b/src/core/model.hpp new file mode 100644 index 00000000..10465bba --- /dev/null +++ b/src/core/model.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "doc.hpp" + +///! View into a list of objets +/// Typed view into a list of objects. +/// +/// An ObjectModel works as a QML [Data Model], allowing efficient interaction with +/// components that act on models. It has a single role named `modelData`, to match the +/// behavior of lists. +/// The same information contained in the list model is available as a normal list +/// via the `values` property. +/// +/// #### Differences from a list +/// Unlike with a list, the following property binding will never be updated when `model[3]` changes. +/// ```qml +/// // will not update reactively +/// property var foo: model[3] +/// ``` +/// +/// You can work around this limitation using the `values` property of the model to view it as a list. +/// ```qml +/// // will update reactively +/// property var foo: model.values[3] +/// ``` +/// +/// [Data Model]: https://doc.qt.io/qt-6/qtquick-modelviewsdata-modelview.html#qml-data-models +class UntypedObjectModel: public QAbstractListModel { + QSDOC_CNAME(ObjectModel); + Q_OBJECT; + /// The content of the object model, as a QML list. + /// The values of this property will always be of the type of the model. + Q_PROPERTY(QQmlListProperty values READ values NOTIFY valuesChanged); + QML_NAMED_ELEMENT(ObjectModel); + QML_UNCREATABLE("ObjectModels cannot be created directly."); + +public: + explicit UntypedObjectModel(QObject* parent): QAbstractListModel(parent) {} + + [[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override; + [[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override; + [[nodiscard]] QHash roleNames() const override; + + [[nodiscard]] QQmlListProperty values(); + void removeAt(qsizetype index); + + Q_INVOKABLE qsizetype indexOf(QObject* object); + +signals: + void valuesChanged(); + /// Sent immediately before an object is inserted into the list. + void objectInsertedPre(QObject* object, qsizetype index); + /// Sent immediately after an object is inserted into the list. + void objectInsertedPost(QObject* object, qsizetype index); + /// Sent immediately before an object is removed from the list. + void objectRemovedPre(QObject* object, qsizetype index); + /// Sent immediately after an object is removed from the list. + void objectRemovedPost(QObject* object, qsizetype index); + +protected: + void insertObject(QObject* object, qsizetype index = -1); + bool removeObject(const QObject* object); + + QVector valuesList; + +private: + static qsizetype valuesCount(QQmlListProperty* property); + static QObject* valueAt(QQmlListProperty* property, qsizetype index); +}; + +template +class ObjectModel: public UntypedObjectModel { +public: + explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {} + + [[nodiscard]] const QVector& valueList() const { + return *reinterpret_cast*>(&this->valuesList); // NOLINT + } + + void insertObject(T* object, qsizetype index = -1) { + this->UntypedObjectModel::insertObject(object, index); + } + + void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); } +}; diff --git a/src/core/module.md b/src/core/module.md index 8eb9b638..13218610 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -18,5 +18,7 @@ headers = [ "easingcurve.hpp", "transformwatcher.hpp", "boundcomponent.hpp", + "model.hpp", + "elapsedtimer.hpp", ] ----- diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index 50370d9d..4eef5f38 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -156,16 +156,7 @@ void ProxyWindowBase::completeWindow() { emit this->screenChanged(); } -bool ProxyWindowBase::deleteOnInvisible() const { -#ifdef NVIDIA_COMPAT - // Nvidia drivers and Qt do not play nice when hiding and showing a window - // so for nvidia compatibility we can never reuse windows if they have been - // hidden. - return true; -#else - return false; -#endif -} +bool ProxyWindowBase::deleteOnInvisible() const { return false; } QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; } QQuickItem* ProxyWindowBase::contentItem() const { return this->mContentItem; } diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 70d7b416..05197f26 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -187,3 +187,14 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT return qEnvironmentVariable(vstr.data()); } + +QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) { + auto* qsg = new QuickshellGlobal(); + auto* generation = EngineGeneration::findEngineGeneration(engine); + + if (generation->qsgInstance == nullptr) { + generation->qsgInstance = qsg; + } + + return qsg; +} diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 83ef68d4..8de55fc2 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -110,8 +110,6 @@ class QuickshellGlobal: public QObject { public: [[nodiscard]] qint32 processId() const; - QuickshellGlobal(QObject* parent = nullptr); - QQmlListProperty screens(); /// Reload the shell from the [ShellRoot]. @@ -133,17 +131,25 @@ public: [[nodiscard]] bool watchFiles() const; void setWatchFiles(bool watchFiles); + static QuickshellGlobal* create(QQmlEngine* engine, QJSEngine* /*unused*/); + signals: /// Sent when the last window is closed. /// /// To make the application exit when the last window is closed run `Qt.quit()`. void lastWindowClosed(); + /// The reload sequence has completed successfully. + void reloadCompleted(); + /// The reload sequence has failed. + void reloadFailed(QString errorString); void screensChanged(); void workingDirectoryChanged(); void watchFilesChanged(); private: + QuickshellGlobal(QObject* parent = nullptr); + static qsizetype screensCount(QQmlListProperty* prop); static QuickshellScreenInfo* screenAt(QQmlListProperty* prop, qsizetype i); }; diff --git a/src/core/qsintercept.cpp b/src/core/qsintercept.cpp index 2eaf498e..ba46ab7b 100644 --- a/src/core/qsintercept.cpp +++ b/src/core/qsintercept.cpp @@ -16,7 +16,22 @@ Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg); -QUrl QsUrlInterceptor::intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) { +QUrl QsUrlInterceptor::intercept( + const QUrl& originalUrl, + QQmlAbstractUrlInterceptor::DataType type +) { + auto url = originalUrl; + + if (url.scheme() == "root") { + url.setScheme("qsintercept"); + + auto path = url.path(); + if (path.startsWith('/')) path = path.sliced(1); + url.setPath(this->configRoot.filePath(path)); + + qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url; + } + // Some types such as Image take into account where they are loading from, and force // asynchronous loading over a network. qsintercept is considered to be over a network. if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") { diff --git a/src/core/qsintercept.hpp b/src/core/qsintercept.hpp index d51b78e6..57923568 100644 --- a/src/core/qsintercept.hpp +++ b/src/core/qsintercept.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -13,7 +14,12 @@ Q_DECLARE_LOGGING_CATEGORY(logQsIntercept); class QsUrlInterceptor: public QQmlAbstractUrlInterceptor { public: - QUrl intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) override; + explicit QsUrlInterceptor(const QDir& configRoot): configRoot(configRoot) {} + + QUrl intercept(const QUrl& originalUrl, QQmlAbstractUrlInterceptor::DataType type) override; + +private: + QDir configRoot; }; class QsInterceptDataReply: public QNetworkReply { diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index ed2ef4b7..1afb30cf 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "generation.hpp" @@ -42,10 +43,11 @@ RootWrapper::~RootWrapper() { } void RootWrapper::reloadGraph(bool hard) { - auto scanner = QmlScanner(); + auto rootPath = QFileInfo(this->rootPath).dir(); + auto scanner = QmlScanner(rootPath); scanner.scanQmlFile(this->rootPath); - auto* generation = new EngineGeneration(std::move(scanner)); + auto* generation = new EngineGeneration(rootPath, std::move(scanner)); generation->wrapper = this; // todo: move into EngineGeneration @@ -63,17 +65,28 @@ void RootWrapper::reloadGraph(bool hard) { auto* obj = component.beginCreate(generation->engine->rootContext()); if (obj == nullptr) { - qWarning() << component.errorString().toStdString().c_str(); - qWarning() << "failed to create root component"; - delete generation; + const QString error = "failed to create root component\n" + component.errorString(); + qWarning().noquote() << error; + generation->destroy(); + + if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { + emit this->generation->qsgInstance->reloadFailed(error); + } + return; } auto* newRoot = qobject_cast(obj); if (newRoot == nullptr) { - qWarning() << "root component was not a Quickshell.ShellRoot"; + const QString error = "root component was not a Quickshell.ShellRoot"; + qWarning().noquote() << error; delete obj; - delete generation; + generation->destroy(); + + if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { + emit this->generation->qsgInstance->reloadFailed(error); + } + return; } @@ -81,8 +94,13 @@ void RootWrapper::reloadGraph(bool hard) { component.completeCreate(); + auto isReload = this->generation != nullptr; generation->onReload(hard ? nullptr : this->generation); - if (hard) delete this->generation; + + if (hard && this->generation != nullptr) { + this->generation->destroy(); + } + this->generation = generation; qInfo() << "Configuration Loaded"; @@ -95,6 +113,10 @@ void RootWrapper::reloadGraph(bool hard) { ); this->onWatchFilesChanged(); + + if (isReload && this->generation->qsgInstance != nullptr) { + emit this->generation->qsgInstance->reloadCompleted(); + } } void RootWrapper::onWatchFilesChanged() { diff --git a/src/core/scan.cpp b/src/core/scan.cpp index f5f078aa..59ec05b6 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -103,7 +103,15 @@ bool QmlScanner::scanQmlFile(const QString& path) { this->scanDir(currentdir.path()); for (auto& import: imports) { - auto ipath = currentdir.filePath(import); + QString ipath; + if (import.startsWith("root:")) { + auto path = import.sliced(5); + if (path.startsWith('/')) path = path.sliced(1); + ipath = this->rootPath.filePath(path); + } else { + ipath = currentdir.filePath(import); + } + auto cpath = QFileInfo(ipath).canonicalFilePath(); if (cpath.isEmpty()) { diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 32a6166d..e3071a88 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -10,6 +11,8 @@ Q_DECLARE_LOGGING_CATEGORY(logQmlScanner); // expects canonical paths class QmlScanner { public: + QmlScanner(const QDir& rootPath): rootPath(rootPath) {} + void scanDir(const QString& path); // returns if the file has a singleton bool scanQmlFile(const QString& path); @@ -17,4 +20,7 @@ public: QVector scannedDirs; QVector scannedFiles; QHash qmldirIntercepts; + +private: + QDir rootPath; }; diff --git a/src/core/transformwatcher.cpp b/src/core/transformwatcher.cpp index 697dfc56..2a33bad0 100644 --- a/src/core/transformwatcher.cpp +++ b/src/core/transformwatcher.cpp @@ -82,7 +82,10 @@ void TransformWatcher::linkItem(QQuickItem* item) const { QObject::connect(item, &QQuickItem::parentChanged, this, &TransformWatcher::recalcChains); QObject::connect(item, &QQuickItem::windowChanged, this, &TransformWatcher::recalcChains); - QObject::connect(item, &QObject::destroyed, this, &TransformWatcher::recalcChains); + + if (item != this->mA && item != this->mB) { + QObject::connect(item, &QObject::destroyed, this, &TransformWatcher::itemDestroyed); + } } void TransformWatcher::linkChains() { @@ -103,6 +106,18 @@ void TransformWatcher::unlinkChains() { for (auto* item: this->childChain) { QObject::disconnect(item, nullptr, this, nullptr); } + + // relink a and b destruction notifications + if (this->mA != nullptr) { + QObject::connect(this->mA, &QObject::destroyed, this, &TransformWatcher::aDestroyed); + } + + if (this->mB != nullptr) { + QObject::connect(this->mB, &QObject::destroyed, this, &TransformWatcher::bDestroyed); + } + + this->parentChain.clear(); + this->childChain.clear(); } void TransformWatcher::recalcChains() { @@ -111,26 +126,57 @@ void TransformWatcher::recalcChains() { this->linkChains(); } +void TransformWatcher::itemDestroyed() { + auto destroyed = + this->parentChain.removeOne(this->sender()) || this->childChain.removeOne(this->sender()); + + if (destroyed) this->recalcChains(); +} + QQuickItem* TransformWatcher::a() const { return this->mA; } void TransformWatcher::setA(QQuickItem* a) { if (this->mA == a) return; + if (this->mA != nullptr) QObject::disconnect(this->mA, nullptr, this, nullptr); this->mA = a; + + if (this->mA != nullptr) { + QObject::connect(this->mA, &QObject::destroyed, this, &TransformWatcher::aDestroyed); + } + this->recalcChains(); } +void TransformWatcher::aDestroyed() { + this->mA = nullptr; + this->unlinkChains(); + emit this->aChanged(); +} + QQuickItem* TransformWatcher::b() const { return this->mB; } void TransformWatcher::setB(QQuickItem* b) { if (this->mB == b) return; + if (this->mB != nullptr) QObject::disconnect(this->mB, nullptr, this, nullptr); this->mB = b; + + if (this->mB != nullptr) { + QObject::connect(this->mB, &QObject::destroyed, this, &TransformWatcher::bDestroyed); + } + this->recalcChains(); } +void TransformWatcher::bDestroyed() { + this->mB = nullptr; + this->unlinkChains(); + emit this->bChanged(); +} + QQuickItem* TransformWatcher::commonParent() const { return this->mCommonParent; } void TransformWatcher::setCommonParent(QQuickItem* commonParent) { if (this->mCommonParent == commonParent) return; this->mCommonParent = commonParent; - this->resolveChains(); + this->recalcChains(); } diff --git a/src/core/transformwatcher.hpp b/src/core/transformwatcher.hpp index d7174e4c..64bac4a1 100644 --- a/src/core/transformwatcher.hpp +++ b/src/core/transformwatcher.hpp @@ -60,6 +60,9 @@ signals: private slots: void recalcChains(); + void itemDestroyed(); + void aDestroyed(); + void bDestroyed(); private: void resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent); diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 3b0c7463..b659badf 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -60,6 +60,7 @@ MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(paren QObject::connect(&this->pCanRaise, &AbstractDBusProperty::changed, this, &MprisPlayer::canRaiseChanged); QObject::connect(&this->pCanSetFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::canSetFullscreenChanged); QObject::connect(&this->pIdentity, &AbstractDBusProperty::changed, this, &MprisPlayer::identityChanged); + QObject::connect(&this->pDesktopEntry, &AbstractDBusProperty::changed, this, &MprisPlayer::desktopEntryChanged); QObject::connect(&this->pFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::fullscreenChanged); QObject::connect(&this->pSupportedUriSchemes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedUriSchemesChanged); QObject::connect(&this->pSupportedMimeTypes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedMimeTypesChanged); @@ -155,6 +156,7 @@ bool MprisPlayer::canRaise() const { return this->pCanRaise.get(); } bool MprisPlayer::canSetFullscreen() const { return this->pCanSetFullscreen.get(); } QString MprisPlayer::identity() const { return this->pIdentity.get(); } +QString MprisPlayer::desktopEntry() const { return this->pDesktopEntry.get(); } qlonglong MprisPlayer::positionMs() const { if (!this->positionSupported()) return 0; // unsupported @@ -190,15 +192,15 @@ void MprisPlayer::setPosition(qreal position) { } auto target = static_cast(position * 1000) * 1000; - this->pPosition.set(target); if (!this->mTrackId.isEmpty()) { this->player->SetPosition(QDBusObjectPath(this->mTrackId), target); - return; } else { auto pos = this->positionMs() * 1000; this->player->Seek(target - pos); } + + this->pPosition.set(target); } void MprisPlayer::onPositionChanged() { @@ -245,6 +247,8 @@ void MprisPlayer::setVolume(qreal volume) { QVariantMap MprisPlayer::metadata() const { return this->pMetadata.get(); } void MprisPlayer::onMetadataChanged() { + emit this->metadataChanged(); + auto lengthVariant = this->pMetadata.get().value("mpris:length"); qlonglong length = -1; if (lengthVariant.isValid() && lengthVariant.canConvert()) { @@ -256,13 +260,34 @@ void MprisPlayer::onMetadataChanged() { emit this->lengthChanged(); } + auto trackChanged = false; + auto trackidVariant = this->pMetadata.get().value("mpris:trackid"); if (trackidVariant.isValid() && trackidVariant.canConvert()) { - this->mTrackId = trackidVariant.value(); - this->onSeek(0); + auto trackId = trackidVariant.value(); + + if (trackId != this->mTrackId) { + this->mTrackId = trackId; + trackChanged = true; + } } - emit this->metadataChanged(); + // Helps to catch players without trackid. + auto urlVariant = this->pMetadata.get().value("xesam:url"); + if (urlVariant.isValid() && urlVariant.canConvert()) { + auto url = urlVariant.value(); + + if (url != this->mUrl) { + this->mUrl = url; + trackChanged = true; + } + } + + if (trackChanged) { + // Some players don't seem to send position updates or seeks on track change. + this->pPosition.update(); + emit this->trackChanged(); + } } MprisPlaybackState::Enum MprisPlayer::playbackState() const { return this->mPlaybackState; } @@ -305,18 +330,25 @@ void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) { void MprisPlayer::onPlaybackStatusChanged() { const auto& status = this->pPlaybackStatus.get(); + auto state = MprisPlaybackState::Stopped; if (status == "Playing") { - this->mPlaybackState = MprisPlaybackState::Playing; + state = MprisPlaybackState::Playing; } else if (status == "Paused") { - this->mPlaybackState = MprisPlaybackState::Paused; + this->pausedTime = QDateTime::currentDateTimeUtc(); + state = MprisPlaybackState::Paused; } else if (status == "Stopped") { - this->mPlaybackState = MprisPlaybackState::Stopped; + state = MprisPlaybackState::Stopped; } else { - this->mPlaybackState = MprisPlaybackState::Stopped; + state = MprisPlaybackState::Stopped; qWarning() << "Received unexpected PlaybackStatus for" << this << status; } - emit this->playbackStateChanged(); + if (state != this->mPlaybackState) { + // make sure we're in sync at least on play/pause. Some players don't automatically send this. + this->pPosition.update(); + this->mPlaybackState = state; + emit this->playbackStateChanged(); + } } MprisLoopState::Enum MprisPlayer::loopState() const { return this->mLoopState; } diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 0b18d78c..1172505a 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -68,6 +68,8 @@ class MprisPlayer: public QObject { Q_PROPERTY(bool canSetFullscreen READ canSetFullscreen NOTIFY canSetFullscreenChanged); /// The human readable name of the media player. Q_PROPERTY(QString identity READ identity NOTIFY identityChanged); + /// The name of the desktop entry for the media player, or an empty string if not provided. + Q_PROPERTY(QString desktopEntry READ desktopEntry NOTIFY desktopEntryChanged); /// The current position in the playing track, as seconds, with millisecond precision, /// or `0` if `positionSupported` is false. /// @@ -204,6 +206,7 @@ public: [[nodiscard]] bool canSetFullscreen() const; [[nodiscard]] QString identity() const; + [[nodiscard]] QString desktopEntry() const; [[nodiscard]] qlonglong positionMs() const; [[nodiscard]] qreal position() const; @@ -242,6 +245,8 @@ public: [[nodiscard]] QList supportedMimeTypes() const; signals: + void trackChanged(); + QSDOC_HIDE void ready(); void canControlChanged(); void canPlayChanged(); @@ -253,6 +258,7 @@ signals: void canRaiseChanged(); void canSetFullscreenChanged(); void identityChanged(); + void desktopEntryChanged(); void positionChanged(); void positionSupportedChanged(); void lengthChanged(); @@ -285,6 +291,7 @@ private: // clang-format off dbus::DBusPropertyGroup appProperties; dbus::DBusProperty pIdentity {this->appProperties, "Identity"}; + dbus::DBusProperty pDesktopEntry {this->appProperties, "DesktopEntry", "", false}; dbus::DBusProperty pCanQuit {this->appProperties, "CanQuit"}; dbus::DBusProperty pCanRaise {this->appProperties, "CanRaise"}; dbus::DBusProperty pFullscreen {this->appProperties, "Fullscreen", false, false}; @@ -319,6 +326,7 @@ private: DBusMprisPlayerApp* app = nullptr; DBusMprisPlayer* player = nullptr; QString mTrackId; + QString mUrl; }; } // namespace qs::service::mpris diff --git a/src/services/mpris/watcher.cpp b/src/services/mpris/watcher.cpp index 1e107660..8a788933 100644 --- a/src/services/mpris/watcher.cpp +++ b/src/services/mpris/watcher.cpp @@ -4,21 +4,19 @@ #include #include #include -#include #include #include #include #include -#include -#include +#include "../../core/model.hpp" #include "player.hpp" namespace qs::service::mpris { Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg); -MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) { +MprisWatcher::MprisWatcher() { qCDebug(logMprisWatcher) << "Starting MprisWatcher"; auto bus = QDBusConnection::sessionBus(); @@ -74,34 +72,15 @@ void MprisWatcher::onServiceUnregistered(const QString& service) { void MprisWatcher::onPlayerReady() { auto* player = qobject_cast(this->sender()); - this->readyPlayers.push_back(player); - emit this->playersChanged(); + this->readyPlayers.insertObject(player); } void MprisWatcher::onPlayerDestroyed(QObject* object) { auto* player = static_cast(object); // NOLINT - - if (this->readyPlayers.removeOne(player)) { - emit this->playersChanged(); - } + this->readyPlayers.removeObject(player); } -QQmlListProperty MprisWatcher::players() { - return QQmlListProperty( - this, - nullptr, - &MprisWatcher::playersCount, - &MprisWatcher::playerAt - ); -} - -qsizetype MprisWatcher::playersCount(QQmlListProperty* property) { - return static_cast(property->object)->readyPlayers.count(); // NOLINT -} - -MprisPlayer* MprisWatcher::playerAt(QQmlListProperty* property, qsizetype index) { - return static_cast(property->object)->readyPlayers.at(index); // NOLINT -} +ObjectModel* MprisWatcher::players() { return &this->readyPlayers; } void MprisWatcher::registerPlayer(const QString& address) { if (this->mPlayers.contains(address)) { @@ -123,4 +102,13 @@ void MprisWatcher::registerPlayer(const QString& address) { qCDebug(logMprisWatcher) << "Registered MprisPlayer" << address; } +MprisWatcher* MprisWatcher::instance() { + static MprisWatcher* instance = new MprisWatcher(); // NOLINT + return instance; +} + +ObjectModel* MprisQml::players() { // NOLINT + return MprisWatcher::instance()->players(); +} + } // namespace qs::service::mpris diff --git a/src/services/mpris/watcher.hpp b/src/services/mpris/watcher.hpp index a1e4df7c..d60471cc 100644 --- a/src/services/mpris/watcher.hpp +++ b/src/services/mpris/watcher.hpp @@ -4,14 +4,13 @@ #include #include #include -#include #include #include #include #include #include -#include +#include "../../core/model.hpp" #include "player.hpp" namespace qs::service::mpris { @@ -19,18 +18,11 @@ namespace qs::service::mpris { ///! Provides access to MprisPlayers. class MprisWatcher: public QObject { Q_OBJECT; - QML_NAMED_ELEMENT(Mpris); - QML_SINGLETON; - /// All connected MPRIS players. - Q_PROPERTY(QQmlListProperty players READ players NOTIFY playersChanged); public: - explicit MprisWatcher(QObject* parent = nullptr); + [[nodiscard]] ObjectModel* players(); - [[nodiscard]] QQmlListProperty players(); - -signals: - void playersChanged(); + static MprisWatcher* instance(); private slots: void onServiceRegistered(const QString& service); @@ -39,15 +31,27 @@ private slots: void onPlayerDestroyed(QObject* object); private: - static qsizetype playersCount(QQmlListProperty* property); - static MprisPlayer* playerAt(QQmlListProperty* property, qsizetype index); + explicit MprisWatcher(); void registerExisting(); void registerPlayer(const QString& address); QDBusServiceWatcher serviceWatcher; QHash mPlayers; - QList readyPlayers; + ObjectModel readyPlayers {this}; +}; + +class MprisQml: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(Mpris); + QML_SINGLETON; + /// All connected MPRIS players. + Q_PROPERTY(ObjectModel* players READ players CONSTANT); + +public: + explicit MprisQml(QObject* parent = nullptr): QObject(parent) {}; + + [[nodiscard]] ObjectModel* players(); }; } // namespace qs::service::mpris diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index a6617d29..b40de687 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -8,6 +8,7 @@ #include #include +#include "../../core/model.hpp" #include "connection.hpp" #include "link.hpp" #include "metadata.hpp" @@ -65,88 +66,43 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) { // clang-format on } -QQmlListProperty Pipewire::nodes() { - return QQmlListProperty(this, nullptr, &Pipewire::nodesCount, &Pipewire::nodeAt); -} - -qsizetype Pipewire::nodesCount(QQmlListProperty* property) { - return static_cast(property->object)->mNodes.count(); // NOLINT -} - -PwNodeIface* Pipewire::nodeAt(QQmlListProperty* property, qsizetype index) { - return static_cast(property->object)->mNodes.at(index); // NOLINT -} +ObjectModel* Pipewire::nodes() { return &this->mNodes; } void Pipewire::onNodeAdded(PwNode* node) { auto* iface = PwNodeIface::instance(node); QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onNodeRemoved); - - this->mNodes.push_back(iface); - emit this->nodesChanged(); + this->mNodes.insertObject(iface); } void Pipewire::onNodeRemoved(QObject* object) { auto* iface = static_cast(object); // NOLINT - this->mNodes.removeOne(iface); - emit this->nodesChanged(); + this->mNodes.removeObject(iface); } -QQmlListProperty Pipewire::links() { - return QQmlListProperty(this, nullptr, &Pipewire::linksCount, &Pipewire::linkAt); -} - -qsizetype Pipewire::linksCount(QQmlListProperty* property) { - return static_cast(property->object)->mLinks.count(); // NOLINT -} - -PwLinkIface* Pipewire::linkAt(QQmlListProperty* property, qsizetype index) { - return static_cast(property->object)->mLinks.at(index); // NOLINT -} +ObjectModel* Pipewire::links() { return &this->mLinks; } void Pipewire::onLinkAdded(PwLink* link) { auto* iface = PwLinkIface::instance(link); QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkRemoved); - - this->mLinks.push_back(iface); - emit this->linksChanged(); + this->mLinks.insertObject(iface); } void Pipewire::onLinkRemoved(QObject* object) { auto* iface = static_cast(object); // NOLINT - this->mLinks.removeOne(iface); - emit this->linksChanged(); + this->mLinks.removeObject(iface); } -QQmlListProperty Pipewire::linkGroups() { - return QQmlListProperty( - this, - nullptr, - &Pipewire::linkGroupsCount, - &Pipewire::linkGroupAt - ); -} - -qsizetype Pipewire::linkGroupsCount(QQmlListProperty* property) { - return static_cast(property->object)->mLinkGroups.count(); // NOLINT -} - -PwLinkGroupIface* -Pipewire::linkGroupAt(QQmlListProperty* property, qsizetype index) { - return static_cast(property->object)->mLinkGroups.at(index); // NOLINT -} +ObjectModel* Pipewire::linkGroups() { return &this->mLinkGroups; } void Pipewire::onLinkGroupAdded(PwLinkGroup* linkGroup) { auto* iface = PwLinkGroupIface::instance(linkGroup); QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkGroupRemoved); - - this->mLinkGroups.push_back(iface); - emit this->linkGroupsChanged(); + this->mLinkGroups.insertObject(iface); } void Pipewire::onLinkGroupRemoved(QObject* object) { auto* iface = static_cast(object); // NOLINT - this->mLinkGroups.removeOne(iface); - emit this->linkGroupsChanged(); + this->mLinkGroups.removeObject(iface); } PwNodeIface* Pipewire::defaultAudioSink() const { // NOLINT diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 9b452727..8d456419 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -8,6 +8,7 @@ #include #include +#include "../../core/model.hpp" #include "link.hpp" #include "node.hpp" #include "registry.hpp" @@ -52,11 +53,11 @@ class Pipewire: public QObject { Q_OBJECT; // clang-format off /// All pipewire nodes. - Q_PROPERTY(QQmlListProperty nodes READ nodes NOTIFY nodesChanged); + Q_PROPERTY(ObjectModel* nodes READ nodes CONSTANT); /// All pipewire links. - Q_PROPERTY(QQmlListProperty links READ links NOTIFY linksChanged); + Q_PROPERTY(ObjectModel* links READ links CONSTANT); /// All pipewire link groups. - Q_PROPERTY(QQmlListProperty linkGroups READ linkGroups NOTIFY linkGroupsChanged); + Q_PROPERTY(ObjectModel* linkGroups READ linkGroups CONSTANT); /// The default audio sink or `null`. Q_PROPERTY(PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged); /// The default audio source or `null`. @@ -68,16 +69,13 @@ class Pipewire: public QObject { public: explicit Pipewire(QObject* parent = nullptr); - [[nodiscard]] QQmlListProperty nodes(); - [[nodiscard]] QQmlListProperty links(); - [[nodiscard]] QQmlListProperty linkGroups(); + [[nodiscard]] ObjectModel* nodes(); + [[nodiscard]] ObjectModel* links(); + [[nodiscard]] ObjectModel* linkGroups(); [[nodiscard]] PwNodeIface* defaultAudioSink() const; [[nodiscard]] PwNodeIface* defaultAudioSource() const; signals: - void nodesChanged(); - void linksChanged(); - void linkGroupsChanged(); void defaultAudioSinkChanged(); void defaultAudioSourceChanged(); @@ -90,17 +88,9 @@ private slots: void onLinkGroupRemoved(QObject* object); private: - static qsizetype nodesCount(QQmlListProperty* property); - static PwNodeIface* nodeAt(QQmlListProperty* property, qsizetype index); - static qsizetype linksCount(QQmlListProperty* property); - static PwLinkIface* linkAt(QQmlListProperty* property, qsizetype index); - static qsizetype linkGroupsCount(QQmlListProperty* property); - static PwLinkGroupIface* - linkGroupAt(QQmlListProperty* property, qsizetype index); - - QVector mNodes; - QVector mLinks; - QVector mLinkGroups; + ObjectModel mNodes {this}; + ObjectModel mLinks {this}; + ObjectModel mLinkGroups {this}; }; ///! Tracks all link connections to a given node. diff --git a/src/services/status_notifier/qml.cpp b/src/services/status_notifier/qml.cpp index cea5646e..f81a6381 100644 --- a/src/services/status_notifier/qml.cpp +++ b/src/services/status_notifier/qml.cpp @@ -9,6 +9,7 @@ #include #include +#include "../../core/model.hpp" #include "../../dbus/dbusmenu/dbusmenu.hpp" #include "../../dbus/properties.hpp" #include "host.hpp" @@ -106,46 +107,25 @@ SystemTray::SystemTray(QObject* parent): QObject(parent) { // clang-format on for (auto* item: host->items()) { - this->mItems.push_back(new SystemTrayItem(item, this)); + this->mItems.insertObject(new SystemTrayItem(item, this)); } } void SystemTray::onItemRegistered(StatusNotifierItem* item) { - this->mItems.push_back(new SystemTrayItem(item, this)); - emit this->itemsChanged(); + this->mItems.insertObject(new SystemTrayItem(item, this)); } void SystemTray::onItemUnregistered(StatusNotifierItem* item) { - SystemTrayItem* trayItem = nullptr; - - this->mItems.removeIf([item, &trayItem](SystemTrayItem* testItem) { - if (testItem->item == item) { - trayItem = testItem; - return true; - } else return false; - }); - - emit this->itemsChanged(); - - delete trayItem; + for (const auto* storedItem: this->mItems.valueList()) { + if (storedItem->item == item) { + this->mItems.removeObject(storedItem); + delete storedItem; + break; + } + } } -QQmlListProperty SystemTray::items() { - return QQmlListProperty( - this, - nullptr, - &SystemTray::itemsCount, - &SystemTray::itemAt - ); -} - -qsizetype SystemTray::itemsCount(QQmlListProperty* property) { - return reinterpret_cast(property->object)->mItems.count(); // NOLINT -} - -SystemTrayItem* SystemTray::itemAt(QQmlListProperty* property, qsizetype index) { - return reinterpret_cast(property->object)->mItems.at(index); // NOLINT -} +ObjectModel* SystemTray::items() { return &this->mItems; } SystemTrayItem* SystemTrayMenuWatcher::trayItem() const { return this->item; } diff --git a/src/services/status_notifier/qml.hpp b/src/services/status_notifier/qml.hpp index 01f6bb05..e55509df 100644 --- a/src/services/status_notifier/qml.hpp +++ b/src/services/status_notifier/qml.hpp @@ -2,10 +2,9 @@ #include #include -#include #include -#include +#include "../../core/model.hpp" #include "item.hpp" namespace SystemTrayStatus { // NOLINT @@ -108,27 +107,21 @@ signals: class SystemTray: public QObject { Q_OBJECT; /// List of all system tray icons. - Q_PROPERTY(QQmlListProperty items READ items NOTIFY itemsChanged); + Q_PROPERTY(ObjectModel* items READ items CONSTANT); QML_ELEMENT; QML_SINGLETON; public: explicit SystemTray(QObject* parent = nullptr); - [[nodiscard]] QQmlListProperty items(); - -signals: - void itemsChanged(); + [[nodiscard]] ObjectModel* items(); private slots: void onItemRegistered(qs::service::sni::StatusNotifierItem* item); void onItemUnregistered(qs::service::sni::StatusNotifierItem* item); private: - static qsizetype itemsCount(QQmlListProperty* property); - static SystemTrayItem* itemAt(QQmlListProperty* property, qsizetype index); - - QList mItems; + ObjectModel mItems {this}; }; ///! Accessor for SystemTrayItem menus. diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 48140a91..ac8f42bb 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -51,16 +51,19 @@ endfunction() # ----- qt_add_library(quickshell-wayland STATIC) -qt_add_qml_module(quickshell-wayland URI Quickshell.Wayland VERSION 0.1) # required to make sure the constructor is linked add_library(quickshell-wayland-init OBJECT init.cpp) +set(WAYLAND_MODULES) + if (WAYLAND_WLR_LAYERSHELL) target_sources(quickshell-wayland PRIVATE wlr_layershell.cpp) add_subdirectory(wlr_layershell) target_compile_definitions(quickshell-wayland PRIVATE QS_WAYLAND_WLR_LAYERSHELL) target_compile_definitions(quickshell-wayland-init PRIVATE QS_WAYLAND_WLR_LAYERSHELL) + + list(APPEND WAYLAND_MODULES Quickshell.Wayland._WlrLayerShell) endif() if (WAYLAND_SESSION_LOCK) @@ -68,6 +71,11 @@ if (WAYLAND_SESSION_LOCK) add_subdirectory(session_lock) endif() +if (WAYLAND_TOPLEVEL_MANAGEMENT) + add_subdirectory(toplevel_management) + list(APPEND WAYLAND_MODULES Quickshell.Wayland._ToplevelManagement) +endif() + if (HYPRLAND) add_subdirectory(hyprland) endif() @@ -75,6 +83,12 @@ endif() target_link_libraries(quickshell-wayland PRIVATE ${QT_DEPS}) target_link_libraries(quickshell-wayland-init PRIVATE ${QT_DEPS}) +qt_add_qml_module(quickshell-wayland + URI Quickshell.Wayland + VERSION 0.1 + IMPORTS ${WAYLAND_MODULES} +) + qs_pch(quickshell-wayland) qs_pch(quickshell-waylandplugin) qs_pch(quickshell-wayland-init) diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt index 06121a7e..be2f0c59 100644 --- a/src/wayland/hyprland/CMakeLists.txt +++ b/src/wayland/hyprland/CMakeLists.txt @@ -1,15 +1,29 @@ qt_add_library(quickshell-hyprland STATIC) -qt_add_qml_module(quickshell-hyprland URI Quickshell.Hyprland VERSION 0.1) + +target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS}) + +set(HYPRLAND_MODULES) + +if (HYPRLAND_IPC) + add_subdirectory(ipc) + list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._Ipc) +endif() if (HYPRLAND_FOCUS_GRAB) add_subdirectory(focus_grab) + list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._FocusGrab) endif() if (HYPRLAND_GLOBAL_SHORTCUTS) add_subdirectory(global_shortcuts) + list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._GlobalShortcuts) endif() -target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS}) +qt_add_qml_module(quickshell-hyprland + URI Quickshell.Hyprland + VERSION 0.1 + IMPORTS ${HYPRLAND_MODULES} +) qs_pch(quickshell-hyprland) qs_pch(quickshell-hyprlandplugin) diff --git a/src/wayland/hyprland/focus_grab/CMakeLists.txt b/src/wayland/hyprland/focus_grab/CMakeLists.txt index 587ae939..1e37c9fe 100644 --- a/src/wayland/hyprland/focus_grab/CMakeLists.txt +++ b/src/wayland/hyprland/focus_grab/CMakeLists.txt @@ -9,21 +9,14 @@ qt_add_qml_module(quickshell-hyprland-focus-grab VERSION 0.1 ) -add_library(quickshell-hyprland-focus-grab-init OBJECT init.cpp) - wl_proto(quickshell-hyprland-focus-grab hyprland-focus-grab-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml" ) target_link_libraries(quickshell-hyprland-focus-grab PRIVATE ${QT_DEPS} wayland-client) -target_link_libraries(quickshell-hyprland-focus-grab-init PRIVATE ${QT_DEPS}) qs_pch(quickshell-hyprland-focus-grab) qs_pch(quickshell-hyprland-focus-grabplugin) -qs_pch(quickshell-hyprland-focus-grab-init) -target_link_libraries(quickshell PRIVATE - quickshell-hyprland-focus-grabplugin - quickshell-hyprland-focus-grab-init -) +target_link_libraries(quickshell PRIVATE quickshell-hyprland-focus-grabplugin) diff --git a/src/wayland/hyprland/focus_grab/init.cpp b/src/wayland/hyprland/focus_grab/init.cpp deleted file mode 100644 index 784c7f26..00000000 --- a/src/wayland/hyprland/focus_grab/init.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include - -#include "../../../core/plugin.hpp" - -namespace { - -class HyprlandFocusGrabPlugin: public QuickshellPlugin { - void registerTypes() override { - qmlRegisterModuleImport( - "Quickshell.Hyprland", - QQmlModuleImportModuleAny, - "Quickshell.Hyprland._FocusGrab", - QQmlModuleImportLatest - ); - } -}; - -QS_REGISTER_PLUGIN(HyprlandFocusGrabPlugin); - -} // namespace diff --git a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt index 804c0a3c..2ccfb74d 100644 --- a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt +++ b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt @@ -9,21 +9,14 @@ qt_add_qml_module(quickshell-hyprland-global-shortcuts VERSION 0.1 ) -add_library(quickshell-hyprland-global-shortcuts-init OBJECT init.cpp) - wl_proto(quickshell-hyprland-global-shortcuts hyprland-global-shortcuts-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-global-shortcuts-v1.xml" ) target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE ${QT_DEPS} wayland-client) -target_link_libraries(quickshell-hyprland-global-shortcuts-init PRIVATE ${QT_DEPS}) qs_pch(quickshell-hyprland-global-shortcuts) qs_pch(quickshell-hyprland-global-shortcutsplugin) -qs_pch(quickshell-hyprland-global-shortcuts-init) -target_link_libraries(quickshell PRIVATE - quickshell-hyprland-global-shortcutsplugin - quickshell-hyprland-global-shortcuts-init -) +target_link_libraries(quickshell PRIVATE quickshell-hyprland-global-shortcutsplugin) diff --git a/src/wayland/hyprland/global_shortcuts/init.cpp b/src/wayland/hyprland/global_shortcuts/init.cpp deleted file mode 100644 index 12fed07f..00000000 --- a/src/wayland/hyprland/global_shortcuts/init.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include - -#include "../../../core/plugin.hpp" - -namespace { - -class HyprlandFocusGrabPlugin: public QuickshellPlugin { - void registerTypes() override { - qmlRegisterModuleImport( - "Quickshell.Hyprland", - QQmlModuleImportModuleAny, - "Quickshell.Hyprland._GlobalShortcuts", - QQmlModuleImportLatest - ); - } -}; - -QS_REGISTER_PLUGIN(HyprlandFocusGrabPlugin); - -} // namespace diff --git a/src/wayland/hyprland/ipc/CMakeLists.txt b/src/wayland/hyprland/ipc/CMakeLists.txt new file mode 100644 index 00000000..59200462 --- /dev/null +++ b/src/wayland/hyprland/ipc/CMakeLists.txt @@ -0,0 +1,18 @@ +qt_add_library(quickshell-hyprland-ipc STATIC + connection.cpp + monitor.cpp + workspace.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-hyprland-ipc + URI Quickshell.Hyprland._Ipc + VERSION 0.1 +) + +target_link_libraries(quickshell-hyprland-ipc PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-hyprland-ipc) +qs_pch(quickshell-hyprland-ipcplugin) + +target_link_libraries(quickshell PRIVATE quickshell-hyprland-ipcplugin) diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp new file mode 100644 index 00000000..5ee8fffe --- /dev/null +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -0,0 +1,561 @@ +#include "connection.hpp" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "monitor.hpp" +#include "workspace.hpp" + +namespace qs::hyprland::ipc { + +Q_LOGGING_CATEGORY(logHyprlandIpc, "quickshell.hyprland.ipc", QtWarningMsg); +Q_LOGGING_CATEGORY(logHyprlandIpcEvents, "quickshell.hyprland.ipc.events", QtWarningMsg); + +HyprlandIpc::HyprlandIpc() { + auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); + if (his.isEmpty()) { + qWarning() << "$HYPRLAND_INSTANCE_SIGNATURE is unset. Cannot connect to hyprland."; + return; + } + + auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR"); + auto hyprlandDir = runtimeDir + "/hypr/" + his; + + if (!QFileInfo(hyprlandDir).isDir()) { + hyprlandDir = "/tmp/hypr/" + his; + } + + if (!QFileInfo(hyprlandDir).isDir()) { + qWarning() << "Unable to find hyprland socket. Cannot connect to hyprland."; + return; + } + + this->mRequestSocketPath = hyprlandDir + "/.socket.sock"; + this->mEventSocketPath = hyprlandDir + "/.socket2.sock"; + + // clang-format off + QObject::connect(&this->eventSocket, &QLocalSocket::errorOccurred, this, &HyprlandIpc::eventSocketError); + QObject::connect(&this->eventSocket, &QLocalSocket::stateChanged, this, &HyprlandIpc::eventSocketStateChanged); + QObject::connect(&this->eventSocket, &QLocalSocket::readyRead, this, &HyprlandIpc::eventSocketReady); + // clang-format on + + // Sockets don't appear to be able to send data in the first event loop + // cycle of the program, so delay it by one. No idea why this is the case. + QTimer::singleShot(0, [this]() { + this->eventSocket.connectToServer(this->mEventSocketPath, QLocalSocket::ReadOnly); + this->refreshMonitors(true); + this->refreshWorkspaces(true); + }); +} + +QString HyprlandIpc::requestSocketPath() const { return this->mRequestSocketPath; } +QString HyprlandIpc::eventSocketPath() const { return this->mEventSocketPath; } + +void HyprlandIpc::eventSocketError(QLocalSocket::LocalSocketError error) const { + if (!this->valid) { + qWarning() << "Unable to connect to hyprland event socket:" << error; + } else { + qWarning() << "Hyprland event socket error:" << error; + } +} + +void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) { + if (state == QLocalSocket::ConnectedState) { + qCInfo(logHyprlandIpc) << "Hyprland event socket connected."; + emit this->connected(); + } else if (state == QLocalSocket::UnconnectedState && this->valid) { + qCWarning(logHyprlandIpc) << "Hyprland event socket disconnected."; + } + + this->valid = state == QLocalSocket::ConnectedState; +} + +void HyprlandIpc::eventSocketReady() { + while (true) { + auto rawEvent = this->eventSocket.readLine(); + if (rawEvent.isEmpty()) break; + + // remove trailing \n + rawEvent.truncate(rawEvent.length() - 1); + auto splitIdx = rawEvent.indexOf(">>"); + auto event = QByteArrayView(rawEvent.data(), splitIdx); + auto data = QByteArrayView( + rawEvent.data() + splitIdx + 2, // NOLINT + rawEvent.data() + rawEvent.length() // NOLINT + ); + qCDebug(logHyprlandIpcEvents) << "Received event:" << rawEvent << "parsed as" << event << data; + + this->event.name = event; + this->event.data = data; + this->onEvent(&this->event); + emit this->rawEvent(&this->event); + } +} + +void HyprlandIpc::makeRequest( + const QByteArray& request, + const std::function& callback +) { + auto* requestSocket = new QLocalSocket(this); + qCDebug(logHyprlandIpc) << "Making request:" << request; + + auto connectedCallback = [this, request, requestSocket, callback]() { + auto responseCallback = [requestSocket, callback]() { + auto response = requestSocket->readAll(); + callback(true, std::move(response)); + delete requestSocket; + }; + + QObject::connect(requestSocket, &QLocalSocket::readyRead, this, responseCallback); + + requestSocket->write(request); + }; + + auto errorCallback = [=](QLocalSocket::LocalSocketError error) { + qCWarning(logHyprlandIpc) << "Error making request:" << error << "request:" << request; + requestSocket->deleteLater(); + callback(false, {}); + }; + + QObject::connect(requestSocket, &QLocalSocket::connected, this, connectedCallback); + QObject::connect(requestSocket, &QLocalSocket::errorOccurred, this, errorCallback); + + requestSocket->connectToServer(this->mRequestSocketPath); +} + +void HyprlandIpc::dispatch(const QString& request) { + this->makeRequest( + ("dispatch " + request).toUtf8(), + [request](bool success, const QByteArray& response) { + if (!success) { + qCWarning(logHyprlandIpc) << "Failed to request dispatch of" << request; + return; + } + + if (response != "ok") { + qCWarning(logHyprlandIpc) + << "Dispatch request" << request << "failed with error" << response; + } + } + ); +} + +ObjectModel* HyprlandIpc::monitors() { return &this->mMonitors; } + +ObjectModel* HyprlandIpc::workspaces() { return &this->mWorkspaces; } + +QVector HyprlandIpc::parseEventArgs(QByteArrayView event, quint16 count) { + auto args = QVector(); + + for (auto i = 0; i < count - 1; i++) { + auto splitIdx = event.indexOf(','); + if (splitIdx == -1) break; + args.push_back(event.sliced(0, splitIdx)); + event = event.sliced(splitIdx + 1); + } + + if (!event.isEmpty()) { + args.push_back(event); + } + + return args; +} + +QVector HyprlandIpcEvent::parse(qint32 argumentCount) const { + auto args = QVector(); + + for (auto arg: this->parseView(argumentCount)) { + args.push_back(QString::fromUtf8(arg)); + } + + return args; +} + +QVector HyprlandIpcEvent::parseView(qint32 argumentCount) const { + return HyprlandIpc::parseEventArgs(this->data, argumentCount); +} + +QString HyprlandIpcEvent::nameStr() const { return QString::fromUtf8(this->name); } +QString HyprlandIpcEvent::dataStr() const { return QString::fromUtf8(this->data); } + +HyprlandIpc* HyprlandIpc::instance() { + static HyprlandIpc* instance = nullptr; // NOLINT + + if (instance == nullptr) { + instance = new HyprlandIpc(); + } + + return instance; +} + +void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { + if (event->name == "configreloaded") { + this->refreshMonitors(true); + this->refreshWorkspaces(true); + } else if (event->name == "monitoraddedv2") { + auto args = event->parseView(3); + + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + // hyprland will often reference the monitor before creation, in which case + // it will already exist. + auto* monitor = this->findMonitorByName(name, false); + auto existed = monitor != nullptr; + + if (monitor == nullptr) { + monitor = new HyprlandMonitor(this); + } + + qCDebug(logHyprlandIpc) << "Monitor added with id" << id << "name" << name + << "preemptively created:" << existed; + + monitor->updateInitial(id, name, QString::fromUtf8(args.at(2))); + + if (!existed) { + this->mMonitors.insertObject(monitor); + } + + // refresh even if it already existed because workspace focus might have changed. + this->refreshMonitors(false); + } else if (event->name == "monitorremoved") { + const auto& mList = this->mMonitors.valueList(); + auto name = QString::fromUtf8(event->data); + + auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { + return m->name() == name; + }); + + if (monitorIter == mList.end()) { + qCWarning(logHyprlandIpc) << "Got removal for monitor" << name + << "which was not previously tracked."; + return; + } + + auto index = monitorIter - mList.begin(); + auto* monitor = *monitorIter; + + qCDebug(logHyprlandIpc) << "Monitor removed with id" << monitor->id() << "name" + << monitor->name(); + this->mMonitors.removeAt(index); + + // delete the monitor object in the next event loop cycle so it's likely to + // still exist when future events reference it after destruction. + // If we get to the next cycle and things still reference it (unlikely), nulls + // can make it to the frontend. + monitor->deleteLater(); + } else if (event->name == "createworkspacev2") { + auto args = event->parseView(2); + + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + qCDebug(logHyprlandIpc) << "Workspace created with id" << id << "name" << name; + + auto* workspace = this->findWorkspaceByName(name, false); + auto existed = workspace != nullptr; + + if (workspace == nullptr) { + workspace = new HyprlandWorkspace(this); + } + + workspace->updateInitial(id, name); + + if (!existed) { + this->refreshWorkspaces(false); + this->mWorkspaces.insertObject(workspace); + } + } else if (event->name == "destroyworkspacev2") { + auto args = event->parseView(2); + + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + const auto& mList = this->mWorkspaces.valueList(); + + auto workspaceIter = std::find_if(mList.begin(), mList.end(), [id](const HyprlandWorkspace* m) { + return m->id() == id; + }); + + if (workspaceIter == mList.end()) { + qCWarning(logHyprlandIpc) << "Got removal for workspace id" << id << "name" << name + << "which was not previously tracked."; + return; + } + + auto index = workspaceIter - mList.begin(); + auto* workspace = *workspaceIter; + + qCDebug(logHyprlandIpc) << "Workspace removed with id" << id << "name" << name; + this->mWorkspaces.removeAt(index); + + // workspaces have not been observed to be referenced after deletion + delete workspace; + + for (auto* monitor: this->mMonitors.valueList()) { + if (monitor->activeWorkspace() == nullptr) { + // removing a monitor will cause a new workspace to be created and destroyed after removal, + // but it won't go back to a real workspace afterwards and just leaves a null, so we + // re-query monitors if this appears to be the case. + this->refreshMonitors(false); + break; + } + } + } else if (event->name == "focusedmon") { + auto args = event->parseView(2); + auto name = QString::fromUtf8(args.at(0)); + auto workspaceName = QString::fromUtf8(args.at(1)); + + HyprlandWorkspace* workspace = nullptr; + if (workspaceName != "?") { // what the fuck + workspace = this->findWorkspaceByName(workspaceName, false); + } + + auto* monitor = this->findMonitorByName(name, true); + this->setFocusedMonitor(monitor); + monitor->setActiveWorkspace(workspace); + } else if (event->name == "workspacev2") { + auto args = event->parseView(2); + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + if (this->mFocusedMonitor != nullptr) { + auto* workspace = this->findWorkspaceByName(name, true, id); + this->mFocusedMonitor->setActiveWorkspace(workspace); + } + } else if (event->name == "moveworkspacev2") { + auto args = event->parseView(3); + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + auto monitorName = QString::fromUtf8(args.at(2)); + + auto* workspace = this->findWorkspaceByName(name, true, id); + auto* monitor = this->findMonitorByName(monitorName, true); + + workspace->setMonitor(monitor); + } +} + +HyprlandWorkspace* +HyprlandIpc::findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id) { + const auto& mList = this->mWorkspaces.valueList(); + + auto workspaceIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { + return m->name() == name; + }); + + if (workspaceIter != mList.end()) { + return *workspaceIter; + } else if (createIfMissing) { + qCDebug(logHyprlandIpc) << "Workspace" << name + << "requested before creation, performing early init"; + auto* workspace = new HyprlandWorkspace(this); + workspace->updateInitial(id, name); + this->mWorkspaces.insertObject(workspace); + return workspace; + } else { + return nullptr; + } +} + +void HyprlandIpc::refreshWorkspaces(bool canCreate, bool tryAgain) { + if (this->requestingWorkspaces) return; + this->requestingWorkspaces = true; + + this->makeRequest( + "j/workspaces", + [this, canCreate, tryAgain](bool success, const QByteArray& resp) { + this->requestingWorkspaces = false; + if (!success) { + // sometimes fails randomly, so we give it another shot. + if (tryAgain) this->refreshWorkspaces(canCreate, false); + return; + } + + qCDebug(logHyprlandIpc) << "parsing workspaces response"; + auto json = QJsonDocument::fromJson(resp).array(); + + const auto& mList = this->mWorkspaces.valueList(); + auto names = QVector(); + + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + auto name = object.value("name").toString(); + + auto workspaceIter = + std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { + return m->name() == name; + }); + + auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; + auto existed = workspace != nullptr; + + if (workspace == nullptr) { + if (!canCreate) continue; + workspace = new HyprlandWorkspace(this); + } + + workspace->updateFromObject(object); + + if (!existed) { + this->mWorkspaces.insertObject(workspace); + } + + names.push_back(name); + } + + auto removedWorkspaces = QVector(); + + for (auto* workspace: mList) { + if (!names.contains(workspace->name())) { + removedWorkspaces.push_back(workspace); + } + } + + for (auto* workspace: removedWorkspaces) { + this->mWorkspaces.removeObject(workspace); + delete workspace; + } + } + ); +} + +HyprlandMonitor* +HyprlandIpc::findMonitorByName(const QString& name, bool createIfMissing, qint32 id) { + const auto& mList = this->mMonitors.valueList(); + + auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { + return m->name() == name; + }); + + if (monitorIter != mList.end()) { + return *monitorIter; + } else if (createIfMissing) { + qCDebug(logHyprlandIpc) << "Monitor" << name + << "requested before creation, performing early init"; + auto* monitor = new HyprlandMonitor(this); + monitor->updateInitial(id, name, ""); + this->mMonitors.insertObject(monitor); + return monitor; + } else { + return nullptr; + } +} + +HyprlandMonitor* HyprlandIpc::focusedMonitor() const { return this->mFocusedMonitor; } + +HyprlandMonitor* HyprlandIpc::monitorFor(QuickshellScreenInfo* screen) { + // Wayland monitors appear after hyprland ones are created and disappear after destruction + // so simply not doing any preemptive creation is enough, however if this call creates + // the HyprlandIpc singleton then monitors won't be initialized, in which case we + // preemptively create one. + + if (screen == nullptr) return nullptr; + return this->findMonitorByName(screen->name(), !this->monitorsRequested); +} + +void HyprlandIpc::setFocusedMonitor(HyprlandMonitor* monitor) { + if (monitor == this->mFocusedMonitor) return; + + if (this->mFocusedMonitor != nullptr) { + QObject::disconnect(this->mFocusedMonitor, nullptr, this, nullptr); + } + + this->mFocusedMonitor = monitor; + + if (monitor != nullptr) { + QObject::connect(monitor, &QObject::destroyed, this, &HyprlandIpc::onFocusedMonitorDestroyed); + } + emit this->focusedMonitorChanged(); +} + +void HyprlandIpc::onFocusedMonitorDestroyed() { + this->mFocusedMonitor = nullptr; + emit this->focusedMonitorChanged(); +} + +void HyprlandIpc::refreshMonitors(bool canCreate, bool tryAgain) { + if (this->requestingMonitors) return; + this->requestingMonitors = true; + + this->makeRequest( + "j/monitors", + [this, canCreate, tryAgain](bool success, const QByteArray& resp) { + this->requestingMonitors = false; + if (!success) { + // sometimes fails randomly, so we give it another shot. + if (tryAgain) this->refreshMonitors(canCreate, false); + return; + } + + this->monitorsRequested = true; + + qCDebug(logHyprlandIpc) << "parsing monitors response"; + auto json = QJsonDocument::fromJson(resp).array(); + + const auto& mList = this->mMonitors.valueList(); + auto names = QVector(); + + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + auto name = object.value("name").toString(); + + auto monitorIter = + std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { + return m->name() == name; + }); + + auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; + auto existed = monitor != nullptr; + + if (monitor == nullptr) { + if (!canCreate) continue; + monitor = new HyprlandMonitor(this); + } + + monitor->updateFromObject(object); + + if (!existed) { + this->mMonitors.insertObject(monitor); + } + + names.push_back(name); + } + + auto removedMonitors = QVector(); + + for (auto* monitor: mList) { + if (!names.contains(monitor->name())) { + removedMonitors.push_back(monitor); + } + } + + for (auto* monitor: removedMonitors) { + this->mMonitors.removeObject(monitor); + // see comment in onEvent + monitor->deleteLater(); + } + } + ); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/connection.hpp b/src/wayland/hyprland/ipc/connection.hpp new file mode 100644 index 00000000..1778460a --- /dev/null +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandMonitor; +class HyprlandWorkspace; + +} // namespace qs::hyprland::ipc + +Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandWorkspace*); +Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandMonitor*); + +namespace qs::hyprland::ipc { + +///! Live Hyprland IPC event. +/// Live Hyprland IPC event. Holding this object after the +/// signal handler exits is undefined as the event instance +/// is reused. +class HyprlandIpcEvent: public QObject { + Q_OBJECT; + /// The name of the event. + Q_PROPERTY(QString name READ nameStr CONSTANT); + /// The unparsed data of the event. + Q_PROPERTY(QString data READ dataStr CONSTANT); + QML_NAMED_ELEMENT(HyprlandEvent); + QML_UNCREATABLE("HyprlandIpcEvents cannot be created."); + +public: + HyprlandIpcEvent(QObject* parent): QObject(parent) {} + + /// Parse this event with a known number of arguments. + /// + /// Argument count is required as some events can contain commas + /// in the last argument, which can be ignored as long as the count is known. + Q_INVOKABLE [[nodiscard]] QVector parse(qint32 argumentCount) const; + [[nodiscard]] QVector parseView(qint32 argumentCount) const; + + [[nodiscard]] QString nameStr() const; + [[nodiscard]] QString dataStr() const; + + void reset(); + QByteArrayView name; + QByteArrayView data; +}; + +class HyprlandIpc: public QObject { + Q_OBJECT; + +public: + static HyprlandIpc* instance(); + + [[nodiscard]] QString requestSocketPath() const; + [[nodiscard]] QString eventSocketPath() const; + + void + makeRequest(const QByteArray& request, const std::function& callback); + void dispatch(const QString& request); + + [[nodiscard]] HyprlandMonitor* monitorFor(QuickshellScreenInfo* screen); + [[nodiscard]] HyprlandMonitor* focusedMonitor() const; + void setFocusedMonitor(HyprlandMonitor* monitor); + + [[nodiscard]] ObjectModel* monitors(); + [[nodiscard]] ObjectModel* workspaces(); + + // No byId because these preemptively create objects. The given id is set if created. + HyprlandWorkspace* findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id = 0); + HyprlandMonitor* findMonitorByName(const QString& name, bool createIfMissing, qint32 id = -1); + + // canCreate avoids making ghost workspaces when the connection races + void refreshWorkspaces(bool canCreate, bool tryAgain = true); + void refreshMonitors(bool canCreate, bool tryAgain = true); + + // The last argument may contain commas, so the count is required. + [[nodiscard]] static QVector parseEventArgs(QByteArrayView event, quint16 count); + +signals: + void connected(); + void rawEvent(HyprlandIpcEvent* event); + + void focusedMonitorChanged(); + +private slots: + void eventSocketError(QLocalSocket::LocalSocketError error) const; + void eventSocketStateChanged(QLocalSocket::LocalSocketState state); + void eventSocketReady(); + + void onFocusedMonitorDestroyed(); + +private: + explicit HyprlandIpc(); + + void onEvent(HyprlandIpcEvent* event); + + QLocalSocket eventSocket; + QString mRequestSocketPath; + QString mEventSocketPath; + bool valid = false; + bool requestingMonitors = false; + bool requestingWorkspaces = false; + bool monitorsRequested = false; + + ObjectModel mMonitors {this}; + ObjectModel mWorkspaces {this}; + HyprlandMonitor* mFocusedMonitor = nullptr; + //HyprlandWorkspace* activeWorkspace = nullptr; + + HyprlandIpcEvent event {this}; +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/monitor.cpp b/src/wayland/hyprland/ipc/monitor.cpp new file mode 100644 index 00000000..8ee5e207 --- /dev/null +++ b/src/wayland/hyprland/ipc/monitor.cpp @@ -0,0 +1,136 @@ +#include "monitor.hpp" +#include + +#include +#include +#include +#include + +#include "workspace.hpp" + +namespace qs::hyprland::ipc { + +qint32 HyprlandMonitor::id() const { return this->mId; } +QString HyprlandMonitor::name() const { return this->mName; } +QString HyprlandMonitor::description() const { return this->mDescription; } +qint32 HyprlandMonitor::x() const { return this->mX; } +qint32 HyprlandMonitor::y() const { return this->mY; } +qint32 HyprlandMonitor::width() const { return this->mWidth; } +qint32 HyprlandMonitor::height() const { return this->mHeight; } +qreal HyprlandMonitor::scale() const { return this->mScale; } +QVariantMap HyprlandMonitor::lastIpcObject() const { return this->mLastIpcObject; } + +void HyprlandMonitor::updateInitial(qint32 id, QString name, QString description) { + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } + + if (description != this->mDescription) { + this->mDescription = std::move(description); + emit this->descriptionChanged(); + } +} + +void HyprlandMonitor::updateFromObject(QVariantMap object) { + auto id = object.value("id").value(); + auto name = object.value("name").value(); + auto description = object.value("description").value(); + auto x = object.value("x").value(); + auto y = object.value("y").value(); + auto width = object.value("width").value(); + auto height = object.value("height").value(); + auto scale = object.value("height").value(); + auto activeWorkspaceObj = object.value("activeWorkspace").value(); + auto activeWorkspaceId = activeWorkspaceObj.value("id").value(); + auto activeWorkspaceName = activeWorkspaceObj.value("name").value(); + auto focused = object.value("focused").value(); + + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } + + if (description != this->mDescription) { + this->mDescription = std::move(description); + emit this->descriptionChanged(); + } + + if (x != this->mX) { + this->mX = x; + emit this->xChanged(); + } + + if (y != this->mY) { + this->mY = y; + emit this->yChanged(); + } + + if (width != this->mWidth) { + this->mWidth = width; + emit this->widthChanged(); + } + + if (height != this->mHeight) { + this->mHeight = height; + emit this->heightChanged(); + } + + if (scale != this->mScale) { + this->mScale = scale; + emit this->scaleChanged(); + } + + if (this->mActiveWorkspace == nullptr || this->mActiveWorkspace->name() != activeWorkspaceName) { + auto* workspace = this->ipc->findWorkspaceByName(activeWorkspaceName, true, activeWorkspaceId); + workspace->setMonitor(this); + this->setActiveWorkspace(workspace); + } + + this->mLastIpcObject = std::move(object); + emit this->lastIpcObjectChanged(); + + if (focused) { + this->ipc->setFocusedMonitor(this); + } +} + +HyprlandWorkspace* HyprlandMonitor::activeWorkspace() const { return this->mActiveWorkspace; } + +void HyprlandMonitor::setActiveWorkspace(HyprlandWorkspace* workspace) { + if (workspace == this->mActiveWorkspace) return; + + if (this->mActiveWorkspace != nullptr) { + QObject::disconnect(this->mActiveWorkspace, nullptr, this, nullptr); + } + + this->mActiveWorkspace = workspace; + + if (workspace != nullptr) { + QObject::connect( + workspace, + &QObject::destroyed, + this, + &HyprlandMonitor::onActiveWorkspaceDestroyed + ); + } + + emit this->activeWorkspaceChanged(); +} + +void HyprlandMonitor::onActiveWorkspaceDestroyed() { + this->mActiveWorkspace = nullptr; + emit this->activeWorkspaceChanged(); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/monitor.hpp b/src/wayland/hyprland/ipc/monitor.hpp new file mode 100644 index 00000000..6b5d2ecc --- /dev/null +++ b/src/wayland/hyprland/ipc/monitor.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "connection.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandMonitor: public QObject { + Q_OBJECT; + Q_PROPERTY(qint32 id READ id NOTIFY idChanged); + Q_PROPERTY(QString name READ name NOTIFY nameChanged); + Q_PROPERTY(QString description READ description NOTIFY descriptionChanged); + Q_PROPERTY(qint32 x READ x NOTIFY xChanged); + Q_PROPERTY(qint32 y READ y NOTIFY yChanged); + Q_PROPERTY(qint32 width READ width NOTIFY widthChanged); + Q_PROPERTY(qint32 height READ height NOTIFY heightChanged); + Q_PROPERTY(qreal scale READ scale NOTIFY scaleChanged); + /// Last json returned for this monitor, as a javascript object. + /// + /// > [!WARNING] This is *not* updated unless the monitor object is fetched again from + /// > Hyprland. If you need a value that is subject to change and does not have a dedicated + /// > property, run `HyprlandIpc.refreshMonitors()` and wait for this property to update. + Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); + /// The currently active workspace on this monitor. May be null. + Q_PROPERTY(HyprlandWorkspace* activeWorkspace READ activeWorkspace NOTIFY activeWorkspaceChanged); + QML_ELEMENT; + QML_UNCREATABLE("HyprlandMonitors must be retrieved from the HyprlandIpc object."); + +public: + explicit HyprlandMonitor(HyprlandIpc* ipc): QObject(ipc), ipc(ipc) {} + + void updateInitial(qint32 id, QString name, QString description); + void updateFromObject(QVariantMap object); + + [[nodiscard]] qint32 id() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QString description() const; + [[nodiscard]] qint32 x() const; + [[nodiscard]] qint32 y() const; + [[nodiscard]] qint32 width() const; + [[nodiscard]] qint32 height() const; + [[nodiscard]] qreal scale() const; + [[nodiscard]] QVariantMap lastIpcObject() const; + + void setActiveWorkspace(HyprlandWorkspace* workspace); + [[nodiscard]] HyprlandWorkspace* activeWorkspace() const; + +signals: + void idChanged(); + void nameChanged(); + void descriptionChanged(); + void xChanged(); + void yChanged(); + void widthChanged(); + void heightChanged(); + void scaleChanged(); + void lastIpcObjectChanged(); + void activeWorkspaceChanged(); + +private slots: + void onActiveWorkspaceDestroyed(); + +private: + HyprlandIpc* ipc; + + qint32 mId = -1; + QString mName; + QString mDescription; + qint32 mX = 0; + qint32 mY = 0; + qint32 mWidth = 0; + qint32 mHeight = 0; + qreal mScale = 0; + QVariantMap mLastIpcObject; + + HyprlandWorkspace* mActiveWorkspace = nullptr; +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/qml.cpp b/src/wayland/hyprland/ipc/qml.cpp new file mode 100644 index 00000000..1e75ee9c --- /dev/null +++ b/src/wayland/hyprland/ipc/qml.cpp @@ -0,0 +1,52 @@ +#include "qml.hpp" + +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" +#include "monitor.hpp" + +namespace qs::hyprland::ipc { + +HyprlandIpcQml::HyprlandIpcQml() { + auto* instance = HyprlandIpc::instance(); + + QObject::connect(instance, &HyprlandIpc::rawEvent, this, &HyprlandIpcQml::rawEvent); + QObject::connect( + instance, + &HyprlandIpc::focusedMonitorChanged, + this, + &HyprlandIpcQml::focusedMonitorChanged + ); +} + +void HyprlandIpcQml::dispatch(const QString& request) { + HyprlandIpc::instance()->dispatch(request); +} + +HyprlandMonitor* HyprlandIpcQml::monitorFor(QuickshellScreenInfo* screen) { + return HyprlandIpc::instance()->monitorFor(screen); +} + +void HyprlandIpcQml::refreshMonitors() { HyprlandIpc::instance()->refreshMonitors(false); } + +void HyprlandIpcQml::refreshWorkspaces() { HyprlandIpc::instance()->refreshWorkspaces(false); } + +QString HyprlandIpcQml::requestSocketPath() { return HyprlandIpc::instance()->requestSocketPath(); } + +QString HyprlandIpcQml::eventSocketPath() { return HyprlandIpc::instance()->eventSocketPath(); } + +HyprlandMonitor* HyprlandIpcQml::focusedMonitor() { + return HyprlandIpc::instance()->focusedMonitor(); +} + +ObjectModel* HyprlandIpcQml::monitors() { + return HyprlandIpc::instance()->monitors(); +} + +ObjectModel* HyprlandIpcQml::workspaces() { + return HyprlandIpc::instance()->workspaces(); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/qml.hpp b/src/wayland/hyprland/ipc/qml.hpp new file mode 100644 index 00000000..2d39623f --- /dev/null +++ b/src/wayland/hyprland/ipc/qml.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" +#include "monitor.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandIpcQml: public QObject { + Q_OBJECT; + /// Path to the request socket (.socket.sock) + Q_PROPERTY(QString requestSocketPath READ requestSocketPath CONSTANT); + /// Path to the event socket (.socket2.sock) + Q_PROPERTY(QString eventSocketPath READ eventSocketPath CONSTANT); + /// The currently focused hyprland monitor. May be null. + Q_PROPERTY(HyprlandMonitor* focusedMonitor READ focusedMonitor NOTIFY focusedMonitorChanged); + /// All hyprland monitors. + Q_PROPERTY(ObjectModel* monitors READ monitors CONSTANT); + /// All hyprland workspaces. + Q_PROPERTY(ObjectModel* workspaces READ workspaces CONSTANT); + QML_NAMED_ELEMENT(Hyprland); + QML_SINGLETON; + +public: + explicit HyprlandIpcQml(); + + /// Execute a hyprland [dispatcher](https://wiki.hyprland.org/Configuring/Dispatchers). + Q_INVOKABLE static void dispatch(const QString& request); + + /// Get the HyprlandMonitor object that corrosponds to a quickshell screen. + Q_INVOKABLE static HyprlandMonitor* monitorFor(QuickshellScreenInfo* screen); + + /// Refresh monitor information. + /// + /// Many actions that will invalidate monitor state don't send events, + /// so this function is available if required. + Q_INVOKABLE static void refreshMonitors(); + + /// Refresh workspace information. + /// + /// Many actions that will invalidate workspace state don't send events, + /// so this function is available if required. + Q_INVOKABLE static void refreshWorkspaces(); + + [[nodiscard]] static QString requestSocketPath(); + [[nodiscard]] static QString eventSocketPath(); + [[nodiscard]] static HyprlandMonitor* focusedMonitor(); + [[nodiscard]] static ObjectModel* monitors(); + [[nodiscard]] static ObjectModel* workspaces(); + +signals: + /// Emitted for every event that comes in through the hyprland event socket (socket2). + /// + /// See [Hyprland Wiki: IPC](https://wiki.hyprland.org/IPC/) for a list of events. + void rawEvent(HyprlandIpcEvent* event); + + void focusedMonitorChanged(); +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/workspace.cpp b/src/wayland/hyprland/ipc/workspace.cpp new file mode 100644 index 00000000..fbf8477f --- /dev/null +++ b/src/wayland/hyprland/ipc/workspace.cpp @@ -0,0 +1,79 @@ +#include "workspace.hpp" +#include + +#include +#include +#include +#include + +#include "monitor.hpp" + +namespace qs::hyprland::ipc { + +qint32 HyprlandWorkspace::id() const { return this->mId; } +QString HyprlandWorkspace::name() const { return this->mName; } +QVariantMap HyprlandWorkspace::lastIpcObject() const { return this->mLastIpcObject; } + +void HyprlandWorkspace::updateInitial(qint32 id, QString name) { + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } +} + +void HyprlandWorkspace::updateFromObject(QVariantMap object) { + auto id = object.value("id").value(); + auto name = object.value("name").value(); + auto monitorId = object.value("monitorID").value(); + auto monitorName = object.value("monitor").value(); + + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } + + if (!monitorName.isEmpty() + && (this->mMonitor == nullptr || this->mMonitor->name() != monitorName)) + { + auto* monitor = this->ipc->findMonitorByName(monitorName, true, monitorId); + this->setMonitor(monitor); + } + + this->mLastIpcObject = std::move(object); + emit this->lastIpcObjectChanged(); +} + +HyprlandMonitor* HyprlandWorkspace::monitor() const { return this->mMonitor; } + +void HyprlandWorkspace::setMonitor(HyprlandMonitor* monitor) { + if (monitor == this->mMonitor) return; + + if (this->mMonitor != nullptr) { + QObject::disconnect(this->mMonitor, nullptr, this, nullptr); + } + + this->mMonitor = monitor; + + if (monitor != nullptr) { + QObject::connect(monitor, &QObject::destroyed, this, &HyprlandWorkspace::onMonitorDestroyed); + } + + emit this->monitorChanged(); +} + +void HyprlandWorkspace::onMonitorDestroyed() { + this->mMonitor = nullptr; + emit this->monitorChanged(); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/workspace.hpp b/src/wayland/hyprland/ipc/workspace.hpp new file mode 100644 index 00000000..a63901e6 --- /dev/null +++ b/src/wayland/hyprland/ipc/workspace.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "connection.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandWorkspace: public QObject { + Q_OBJECT; + Q_PROPERTY(qint32 id READ id NOTIFY idChanged); + Q_PROPERTY(QString name READ name NOTIFY nameChanged); + /// Last json returned for this workspace, as a javascript object. + /// + /// > [!WARNING] This is *not* updated unless the workspace object is fetched again from + /// > Hyprland. If you need a value that is subject to change and does not have a dedicated + /// > property, run `HyprlandIpc.refreshWorkspaces()` and wait for this property to update. + Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); + Q_PROPERTY(HyprlandMonitor* monitor READ monitor NOTIFY monitorChanged); + QML_ELEMENT; + QML_UNCREATABLE("HyprlandWorkspaces must be retrieved from the HyprlandIpc object."); + +public: + explicit HyprlandWorkspace(HyprlandIpc* ipc): QObject(ipc), ipc(ipc) {} + + void updateInitial(qint32 id, QString name); + void updateFromObject(QVariantMap object); + + [[nodiscard]] qint32 id() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QVariantMap lastIpcObject() const; + + void setMonitor(HyprlandMonitor* monitor); + [[nodiscard]] HyprlandMonitor* monitor() const; + +signals: + void idChanged(); + void nameChanged(); + void lastIpcObjectChanged(); + void monitorChanged(); + +private slots: + void onMonitorDestroyed(); + +private: + HyprlandIpc* ipc; + + qint32 mId = -1; + QString mName; + QVariantMap mLastIpcObject; + HyprlandMonitor* mMonitor = nullptr; +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/module.md b/src/wayland/hyprland/module.md index 1b3e2fbf..6c2de249 100644 --- a/src/wayland/hyprland/module.md +++ b/src/wayland/hyprland/module.md @@ -1,6 +1,10 @@ name = "Quickshell.Hyprland" description = "Hyprland specific Quickshell types" headers = [ + "ipc/connection.hpp", + "ipc/monitor.hpp", + "ipc/workspace.hpp", + "ipc/qml.hpp", "focus_grab/qml.hpp", "global_shortcuts/qml.hpp", ] diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp index 194bad4c..95adb248 100644 --- a/src/wayland/init.cpp +++ b/src/wayland/init.cpp @@ -34,13 +34,6 @@ class WaylandPlugin: public QuickshellPlugin { // 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.Wayland", - QQmlModuleImportModuleAny, - "Quickshell.Wayland._WlrLayerShell", - QQmlModuleImportLatest - ); - qmlRegisterModuleImport( "Quickshell", QQmlModuleImportModuleAny, diff --git a/src/wayland/module.md b/src/wayland/module.md index 7a427df9..d6376e39 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -4,5 +4,6 @@ headers = [ "wlr_layershell/window.hpp", "wlr_layershell.hpp", "session_lock.hpp", + "toplevel_management/qml.hpp", ] ----- diff --git a/src/wayland/toplevel_management/CMakeLists.txt b/src/wayland/toplevel_management/CMakeLists.txt new file mode 100644 index 00000000..4537c201 --- /dev/null +++ b/src/wayland/toplevel_management/CMakeLists.txt @@ -0,0 +1,22 @@ +qt_add_library(quickshell-wayland-toplevel-management STATIC + manager.cpp + handle.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-wayland-toplevel-management + URI Quickshell.Wayland._ToplevelManagement + VERSION 0.1 +) + +wl_proto(quickshell-wayland-toplevel-management + wlr-foreign-toplevel-management-unstable-v1 + "${CMAKE_CURRENT_SOURCE_DIR}/wlr-foreign-toplevel-management-unstable-v1.xml" +) + +target_link_libraries(quickshell-wayland-toplevel-management PRIVATE ${QT_DEPS} wayland-client) + +qs_pch(quickshell-wayland-toplevel-management) +qs_pch(quickshell-wayland-toplevel-managementplugin) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-toplevel-managementplugin) diff --git a/src/wayland/toplevel_management/handle.cpp b/src/wayland/toplevel_management/handle.cpp new file mode 100644 index 00000000..8c2886b4 --- /dev/null +++ b/src/wayland/toplevel_management/handle.cpp @@ -0,0 +1,228 @@ +#include "handle.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" +#include "qwayland-wlr-foreign-toplevel-management-unstable-v1.h" +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +QString ToplevelHandle::appId() const { return this->mAppId; } +QString ToplevelHandle::title() const { return this->mTitle; } +QVector ToplevelHandle::visibleScreens() const { return this->mVisibleScreens; } +ToplevelHandle* ToplevelHandle::parent() const { return this->mParent; } +bool ToplevelHandle::activated() const { return this->mActivated; } +bool ToplevelHandle::maximized() const { return this->mMaximized; } +bool ToplevelHandle::minimized() const { return this->mMinimized; } +bool ToplevelHandle::fullscreen() const { return this->mFullscreen; } + +void ToplevelHandle::activate() { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* inputDevice = display->lastInputDevice(); + if (inputDevice == nullptr) return; + this->QtWayland::zwlr_foreign_toplevel_handle_v1::activate(inputDevice->object()); +} + +void ToplevelHandle::setMaximized(bool maximized) { + if (maximized) this->set_maximized(); + else this->unset_maximized(); +} + +void ToplevelHandle::setMinimized(bool minimized) { + if (minimized) this->set_minimized(); + else this->unset_minimized(); +} + +void ToplevelHandle::setFullscreen(bool fullscreen) { + if (fullscreen) this->set_fullscreen(nullptr); + else this->unset_fullscreen(); +} + +void ToplevelHandle::fullscreenOn(QScreen* screen) { + auto* waylandScreen = dynamic_cast(screen->handle()); + this->set_fullscreen(waylandScreen != nullptr ? waylandScreen->output() : nullptr); +} + +void ToplevelHandle::setRectangle(QWindow* window, QRect rect) { + if (window == nullptr) { + // will be cleared by the compositor if the surface is destroyed + if (this->rectWindow != nullptr) { + auto* waylandWindow = + dynamic_cast(this->rectWindow->handle()); + + if (waylandWindow != nullptr) { + this->set_rectangle(waylandWindow->surface(), 0, 0, 0, 0); + } + } + + QObject::disconnect(this->rectWindow, nullptr, this, nullptr); + this->rectWindow = nullptr; + return; + } + + if (this->rectWindow != window) { + if (this->rectWindow != nullptr) { + QObject::disconnect(this->rectWindow, nullptr, this, nullptr); + } + + this->rectWindow = window; + QObject::connect(window, &QObject::destroyed, this, &ToplevelHandle::onRectWindowDestroyed); + } + + if (auto* waylandWindow = dynamic_cast(window->handle())) { + this->set_rectangle(waylandWindow->surface(), rect.x(), rect.y(), rect.width(), rect.height()); + } else { + QObject::connect(window, &QWindow::visibleChanged, this, [this, window, rect]() { + if (window->isVisible()) { + if (window->handle() == nullptr) { + window->create(); + } + + auto* waylandWindow = dynamic_cast(window->handle()); + this->set_rectangle( + waylandWindow->surface(), + rect.x(), + rect.y(), + rect.width(), + rect.height() + ); + } + }); + } +} + +void ToplevelHandle::onRectWindowDestroyed() { this->rectWindow = nullptr; } + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_done() { + qCDebug(logToplevelManagement) << this << "got done"; + auto wasReady = this->isReady; + this->isReady = true; + + if (!wasReady) { + emit this->ready(); + } +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_closed() { + qCDebug(logToplevelManagement) << this << "closed"; + this->destroy(); + emit this->closed(); + delete this; +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_app_id(const QString& appId) { + qCDebug(logToplevelManagement) << this << "got appid" << appId; + this->mAppId = appId; + emit this->appIdChanged(); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_title(const QString& title) { + qCDebug(logToplevelManagement) << this << "got toplevel" << title; + this->mTitle = title; + emit this->titleChanged(); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_state(wl_array* stateArray) { + auto activated = false; + auto maximized = false; + auto minimized = false; + auto fullscreen = false; + + // wl_array_for_each is illegal in C++ so it is manually expanded. + auto* state = static_cast<::zwlr_foreign_toplevel_handle_v1_state*>(stateArray->data); + auto size = stateArray->size / sizeof(::zwlr_foreign_toplevel_handle_v1_state); + for (size_t i = 0; i < size; i++) { + auto flag = state[i]; // NOLINT + switch (flag) { + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED: activated = true; break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED: maximized = true; break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED: minimized = true; break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN: fullscreen = true; break; + } + } + + qCDebug(logToplevelManagement) << this << "got state update - activated:" << activated + << "maximized:" << maximized << "minimized:" << minimized + << "fullscreen:" << fullscreen; + + if (activated != this->mActivated) { + this->mActivated = activated; + emit this->activatedChanged(); + } + + if (maximized != this->mMaximized) { + this->mMaximized = maximized; + emit this->maximizedChanged(); + } + + if (minimized != this->mMinimized) { + this->mMinimized = minimized; + emit this->minimizedChanged(); + } + + if (fullscreen != this->mFullscreen) { + this->mFullscreen = fullscreen; + emit this->fullscreenChanged(); + } +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_output_enter(wl_output* output) { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* screen = display->screenForOutput(output)->screen(); + + qCDebug(logToplevelManagement) << this << "got output enter" << screen; + + this->mVisibleScreens.push_back(screen); + emit this->visibleScreenAdded(screen); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_output_leave(wl_output* output) { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* screen = display->screenForOutput(output)->screen(); + + qCDebug(logToplevelManagement) << this << "got output leave" << screen; + + emit this->visibleScreenRemoved(screen); + this->mVisibleScreens.removeOne(screen); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_parent( + ::zwlr_foreign_toplevel_handle_v1* parent +) { + auto* handle = ToplevelManager::instance()->handleFor(parent); + qCDebug(logToplevelManagement) << this << "got parent" << handle; + + if (handle != this->mParent) { + if (this->mParent != nullptr) { + QObject::disconnect(this->mParent, nullptr, this, nullptr); + } + + this->mParent = handle; + + if (handle != nullptr) { + QObject::connect(handle, &ToplevelHandle::closed, this, &ToplevelHandle::onParentClosed); + } + + emit this->parentChanged(); + } +} + +void ToplevelHandle::onParentClosed() { + this->mParent = nullptr; + emit this->parentChanged(); +} + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/handle.hpp b/src/wayland/toplevel_management/handle.hpp new file mode 100644 index 00000000..a49afe82 --- /dev/null +++ b/src/wayland/toplevel_management/handle.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +class ToplevelHandle + : public QObject + , public QtWayland::zwlr_foreign_toplevel_handle_v1 { + Q_OBJECT; + +public: + [[nodiscard]] QString appId() const; + [[nodiscard]] QString title() const; + [[nodiscard]] QVector visibleScreens() const; + [[nodiscard]] ToplevelHandle* parent() const; + [[nodiscard]] bool activated() const; + [[nodiscard]] bool maximized() const; + [[nodiscard]] bool minimized() const; + [[nodiscard]] bool fullscreen() const; + + void activate(); + void setMaximized(bool maximized); + void setMinimized(bool minimized); + void setFullscreen(bool fullscreen); + void fullscreenOn(QScreen* screen); + void setRectangle(QWindow* window, QRect rect); + +signals: + // sent after the first done event. + void ready(); + // sent right before delete this. + void closed(); + + void appIdChanged(); + void titleChanged(); + void visibleScreenAdded(QScreen* screen); + void visibleScreenRemoved(QScreen* screen); + void parentChanged(); + void activatedChanged(); + void maximizedChanged(); + void minimizedChanged(); + void fullscreenChanged(); + +private slots: + void onParentClosed(); + void onRectWindowDestroyed(); + +private: + void zwlr_foreign_toplevel_handle_v1_done() override; + void zwlr_foreign_toplevel_handle_v1_closed() override; + void zwlr_foreign_toplevel_handle_v1_app_id(const QString& appId) override; + void zwlr_foreign_toplevel_handle_v1_title(const QString& title) override; + void zwlr_foreign_toplevel_handle_v1_state(wl_array* stateArray) override; + void zwlr_foreign_toplevel_handle_v1_output_enter(wl_output* output) override; + void zwlr_foreign_toplevel_handle_v1_output_leave(wl_output* output) override; + void zwlr_foreign_toplevel_handle_v1_parent(::zwlr_foreign_toplevel_handle_v1* parent) override; + + bool isReady = false; + QString mAppId; + QString mTitle; + QVector mVisibleScreens; + ToplevelHandle* mParent = nullptr; + bool mActivated = false; + bool mMaximized = false; + bool mMinimized = false; + bool mFullscreen = false; + QWindow* rectWindow = nullptr; +}; + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/manager.cpp b/src/wayland/toplevel_management/manager.cpp new file mode 100644 index 00000000..bd477b49 --- /dev/null +++ b/src/wayland/toplevel_management/manager.cpp @@ -0,0 +1,67 @@ +#include "manager.hpp" + +#include +#include +#include +#include +#include +#include + +#include "handle.hpp" +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +Q_LOGGING_CATEGORY(logToplevelManagement, "quickshell.wayland.toplevelManagement", QtWarningMsg); + +ToplevelManager::ToplevelManager(): QWaylandClientExtensionTemplate(3) { this->initialize(); } + +bool ToplevelManager::available() const { return this->isActive(); } + +const QVector& ToplevelManager::readyToplevels() const { + return this->mReadyToplevels; +} + +ToplevelHandle* ToplevelManager::handleFor(::zwlr_foreign_toplevel_handle_v1* toplevel) { + if (toplevel == nullptr) return nullptr; + + for (auto* other: this->mToplevels) { + if (other->object() == toplevel) return other; + } + + return nullptr; +} + +ToplevelManager* ToplevelManager::instance() { + static auto* instance = new ToplevelManager(); // NOLINT + return instance; +} + +void ToplevelManager::zwlr_foreign_toplevel_manager_v1_toplevel( + ::zwlr_foreign_toplevel_handle_v1* toplevel +) { + auto* handle = new ToplevelHandle(); + QObject::connect(handle, &ToplevelHandle::closed, this, &ToplevelManager::onToplevelClosed); + QObject::connect(handle, &ToplevelHandle::ready, this, &ToplevelManager::onToplevelReady); + + qCDebug(logToplevelManagement) << "Toplevel handle created" << handle; + this->mToplevels.push_back(handle); + + // Not done in constructor as a close could technically be picked up immediately on init, + // making touching the handle a UAF. + handle->init(toplevel); +} + +void ToplevelManager::onToplevelReady() { + auto* handle = qobject_cast(this->sender()); + this->mReadyToplevels.push_back(handle); + emit this->toplevelReady(handle); +} + +void ToplevelManager::onToplevelClosed() { + auto* handle = qobject_cast(this->sender()); + this->mReadyToplevels.removeOne(handle); + this->mToplevels.removeOne(handle); +} + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/manager.hpp b/src/wayland/toplevel_management/manager.hpp new file mode 100644 index 00000000..41848de1 --- /dev/null +++ b/src/wayland/toplevel_management/manager.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +class ToplevelHandle; + +Q_DECLARE_LOGGING_CATEGORY(logToplevelManagement); + +class ToplevelManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwlr_foreign_toplevel_manager_v1 { + Q_OBJECT; + +public: + [[nodiscard]] bool available() const; + [[nodiscard]] const QVector& readyToplevels() const; + [[nodiscard]] ToplevelHandle* handleFor(::zwlr_foreign_toplevel_handle_v1* toplevel); + + static ToplevelManager* instance(); + +signals: + void toplevelReady(ToplevelHandle* toplevel); + +protected: + explicit ToplevelManager(); + + void zwlr_foreign_toplevel_manager_v1_toplevel(::zwlr_foreign_toplevel_handle_v1* toplevel + ) override; + +private slots: + void onToplevelReady(); + void onToplevelClosed(); + +private: + QVector mToplevels; + QVector mReadyToplevels; +}; + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/qml.cpp b/src/wayland/toplevel_management/qml.cpp new file mode 100644 index 00000000..2042262b --- /dev/null +++ b/src/wayland/toplevel_management/qml.cpp @@ -0,0 +1,153 @@ +#include "qml.hpp" + +#include +#include + +#include "../../core/model.hpp" +#include "../../core/proxywindow.hpp" +#include "../../core/qmlscreen.hpp" +#include "../../core/windowinterface.hpp" +#include "handle.hpp" +#include "manager.hpp" + +namespace qs::wayland::toplevel_management { + +Toplevel::Toplevel(impl::ToplevelHandle* handle, QObject* parent): QObject(parent), handle(handle) { + // clang-format off + QObject::connect(handle, &impl::ToplevelHandle::closed, this, &Toplevel::onClosed); + QObject::connect(handle, &impl::ToplevelHandle::appIdChanged, this, &Toplevel::appIdChanged); + QObject::connect(handle, &impl::ToplevelHandle::titleChanged, this, &Toplevel::titleChanged); + QObject::connect(handle, &impl::ToplevelHandle::parentChanged, this, &Toplevel::parentChanged); + QObject::connect(handle, &impl::ToplevelHandle::activatedChanged, this, &Toplevel::activatedChanged); + QObject::connect(handle, &impl::ToplevelHandle::maximizedChanged, this, &Toplevel::maximizedChanged); + QObject::connect(handle, &impl::ToplevelHandle::minimizedChanged, this, &Toplevel::minimizedChanged); + QObject::connect(handle, &impl::ToplevelHandle::fullscreenChanged, this, &Toplevel::fullscreenChanged); + // clang-format on +} + +void Toplevel::onClosed() { + emit this->closed(); + delete this; +} + +void Toplevel::activate() { this->handle->activate(); } + +QString Toplevel::appId() const { return this->handle->appId(); } +QString Toplevel::title() const { return this->handle->title(); } + +Toplevel* Toplevel::parent() const { + return ToplevelManager::instance()->forImpl(this->handle->parent()); +} + +bool Toplevel::activated() const { return this->handle->activated(); } + +bool Toplevel::maximized() const { return this->handle->maximized(); } +void Toplevel::setMaximized(bool maximized) { this->handle->setMaximized(maximized); } + +bool Toplevel::minimized() const { return this->handle->minimized(); } +void Toplevel::setMinimized(bool minimized) { this->handle->setMinimized(minimized); } + +bool Toplevel::fullscreen() const { return this->handle->fullscreen(); } +void Toplevel::setFullscreen(bool fullscreen) { this->handle->setFullscreen(fullscreen); } + +void Toplevel::fullscreenOn(QuickshellScreenInfo* screen) { + auto* qscreen = screen != nullptr ? screen->screen : nullptr; + this->handle->fullscreenOn(qscreen); +} + +void Toplevel::setRectangle(QObject* window, QRect rect) { + auto* proxyWindow = qobject_cast(window); + + if (proxyWindow == nullptr) { + if (auto* iface = qobject_cast(window)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (proxyWindow != this->rectWindow) { + if (this->rectWindow != nullptr) { + QObject::disconnect(this->rectWindow, nullptr, this, nullptr); + } + + this->rectWindow = proxyWindow; + + if (proxyWindow != nullptr) { + QObject::connect( + proxyWindow, + &QObject::destroyed, + this, + &Toplevel::onRectangleProxyDestroyed + ); + + QObject::connect( + proxyWindow, + &ProxyWindowBase::windowConnected, + this, + &Toplevel::onRectangleProxyConnected + ); + } + } + + this->rectangle = rect; + this->handle->setRectangle(proxyWindow->backingWindow(), rect); +} + +void Toplevel::unsetRectangle() { this->setRectangle(nullptr, QRect()); } + +void Toplevel::onRectangleProxyConnected() { + this->handle->setRectangle(this->rectWindow->backingWindow(), this->rectangle); +} + +void Toplevel::onRectangleProxyDestroyed() { + this->rectWindow = nullptr; + this->rectangle = QRect(); +} + +ToplevelManager::ToplevelManager() { + auto* manager = impl::ToplevelManager::instance(); + + QObject::connect( + manager, + &impl::ToplevelManager::toplevelReady, + this, + &ToplevelManager::onToplevelReady + ); + + for (auto* handle: manager->readyToplevels()) { + this->onToplevelReady(handle); + } +} + +Toplevel* ToplevelManager::forImpl(impl::ToplevelHandle* impl) const { + if (impl == nullptr) return nullptr; + + for (auto* toplevel: this->mToplevels.valueList()) { + if (toplevel->handle == impl) return toplevel; + } + + return nullptr; +} + +ObjectModel* ToplevelManager::toplevels() { return &this->mToplevels; } + +void ToplevelManager::onToplevelReady(impl::ToplevelHandle* handle) { + auto* toplevel = new Toplevel(handle, this); + QObject::connect(toplevel, &Toplevel::closed, this, &ToplevelManager::onToplevelClosed); + this->mToplevels.insertObject(toplevel); +} + +void ToplevelManager::onToplevelClosed() { + auto* toplevel = qobject_cast(this->sender()); + this->mToplevels.removeObject(toplevel); +} + +ToplevelManager* ToplevelManager::instance() { + static auto* instance = new ToplevelManager(); // NOLINT + return instance; +} + +ObjectModel* ToplevelManagerQml::toplevels() { + return ToplevelManager::instance()->toplevels(); +} + +} // namespace qs::wayland::toplevel_management diff --git a/src/wayland/toplevel_management/qml.hpp b/src/wayland/toplevel_management/qml.hpp new file mode 100644 index 00000000..8bb1d551 --- /dev/null +++ b/src/wayland/toplevel_management/qml.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include + +#include "../../core/model.hpp" +#include "../../core/proxywindow.hpp" +#include "../../core/qmlscreen.hpp" + +namespace qs::wayland::toplevel_management { + +namespace impl { +class ToplevelManager; +class ToplevelHandle; +} // namespace impl + +///! Window from another application. +/// A window/toplevel from another application, retrievable from +/// the [ToplevelManager](../toplevelmanager). +class Toplevel: public QObject { + Q_OBJECT; + Q_PROPERTY(QString appId READ appId NOTIFY appIdChanged); + Q_PROPERTY(QString title READ title NOTIFY titleChanged); + /// Parent toplevel if this toplevel is a modal/dialog, otherwise null. + Q_PROPERTY(Toplevel* parent READ parent NOTIFY parentChanged); + /// If the window is currently activated or focused. + /// + /// Activation can be requested with the `activate()` function. + Q_PROPERTY(bool activated READ activated NOTIFY activatedChanged); + /// If the window is currently maximized. + /// + /// Maximization can be requested by setting this property, though it may + /// be ignored by the compositor. + Q_PROPERTY(bool maximized READ maximized WRITE setMaximized NOTIFY maximizedChanged); + /// If the window is currently minimized. + /// + /// Minimization can be requested by setting this property, though it may + /// be ignored by the compositor. + Q_PROPERTY(bool minimized READ minimized WRITE setMinimized NOTIFY minimizedChanged); + /// If the window is currently fullscreen. + /// + /// Fullscreen can be requested by setting this property, though it may + /// be ignored by the compositor. + /// Fullscreen can be requested on a specific screen with the `fullscreenOn()` function. + Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged); + QML_ELEMENT; + QML_UNCREATABLE("Toplevels must be acquired from the ToplevelManager."); + +public: + explicit Toplevel(impl::ToplevelHandle* handle, QObject* parent); + + /// Request that this toplevel is activated. + /// The request may be ignored by the compositor. + Q_INVOKABLE void activate(); + + /// Request that this toplevel is fullscreened on a specific screen. + /// The request may be ignored by the compositor. + Q_INVOKABLE void fullscreenOn(QuickshellScreenInfo* screen); + + /// Provide a hint to the compositor where the visual representation + /// of this toplevel is relative to a quickshell window. + /// This hint can be used visually in operations like minimization. + Q_INVOKABLE void setRectangle(QObject* window, QRect rect); + Q_INVOKABLE void unsetRectangle(); + + [[nodiscard]] QString appId() const; + [[nodiscard]] QString title() const; + [[nodiscard]] Toplevel* parent() const; + [[nodiscard]] bool activated() const; + + [[nodiscard]] bool maximized() const; + void setMaximized(bool maximized); + + [[nodiscard]] bool minimized() const; + void setMinimized(bool minimized); + + [[nodiscard]] bool fullscreen() const; + void setFullscreen(bool fullscreen); + +signals: + void closed(); + void appIdChanged(); + void titleChanged(); + void parentChanged(); + void activatedChanged(); + void maximizedChanged(); + void minimizedChanged(); + void fullscreenChanged(); + +private slots: + void onClosed(); + void onRectangleProxyConnected(); + void onRectangleProxyDestroyed(); + +private: + impl::ToplevelHandle* handle; + ProxyWindowBase* rectWindow = nullptr; + QRect rectangle; + + friend class ToplevelManager; +}; + +class ToplevelManager: public QObject { + Q_OBJECT; + +public: + Toplevel* forImpl(impl::ToplevelHandle* impl) const; + + [[nodiscard]] ObjectModel* toplevels(); + + static ToplevelManager* instance(); + +private slots: + void onToplevelReady(impl::ToplevelHandle* handle); + void onToplevelClosed(); + +private: + explicit ToplevelManager(); + + ObjectModel mToplevels {this}; +}; + +///! Exposes a list of Toplevels. +/// Exposes a list of windows from other applications as [Toplevel](../toplevel)s via the +/// [zwlr-foreign-toplevel-management-v1](https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1) +/// wayland protocol. +class ToplevelManagerQml: public QObject { + Q_OBJECT; + Q_PROPERTY(ObjectModel* toplevels READ toplevels CONSTANT); + QML_NAMED_ELEMENT(ToplevelManager); + QML_SINGLETON; + +public: + explicit ToplevelManagerQml(QObject* parent = nullptr): QObject(parent) {} + + [[nodiscard]] static ObjectModel* toplevels(); +}; + +} // namespace qs::wayland::toplevel_management diff --git a/src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml b/src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml new file mode 100644 index 00000000..44505bbb --- /dev/null +++ b/src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml @@ -0,0 +1,270 @@ + + + + Copyright © 2018 Ilia Bozhinov + + 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. + + + + + The purpose of this protocol is to enable the creation of taskbars + and docks by providing them with a list of opened applications and + letting them request certain actions on them, like maximizing, etc. + + After a client binds the zwlr_foreign_toplevel_manager_v1, each opened + toplevel window will be sent via the toplevel event + + + + + This event is emitted whenever a new toplevel window is created. It + is emitted for all toplevels, regardless of the app that has created + them. + + All initial details of the toplevel(title, app_id, states, etc.) will + be sent immediately after this event via the corresponding events in + zwlr_foreign_toplevel_handle_v1. + + + + + + + Indicates the client no longer wishes to receive events for new toplevels. + However the compositor may emit further toplevel_created events, until + the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending events to the + zwlr_foreign_toplevel_manager_v1. The server will destroy the object + immediately after sending this request, so it will become invalid and + the client should free any resources associated with it. + + + + + + + A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel + window. Each app may have multiple opened toplevels. + + Each toplevel has a list of outputs it is visible on, conveyed to the + client with the output_enter and output_leave events. + + + + + This event is emitted whenever the title of the toplevel changes. + + + + + + + This event is emitted whenever the app-id of the toplevel changes. + + + + + + + This event is emitted whenever the toplevel becomes visible on + the given output. A toplevel may be visible on multiple outputs. + + + + + + + This event is emitted whenever the toplevel stops being visible on + the given output. It is guaranteed that an entered-output event + with the same output has been emitted before this event. + + + + + + + Requests that the toplevel be maximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unmaximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be minimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unminimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Request that this toplevel be activated on the given seat. + There is no guarantee the toplevel will be actually activated. + + + + + + + The different states that a toplevel can have. These have the same meaning + as the states with the same names defined in xdg-toplevel + + + + + + + + + + + This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 + is created and each time the toplevel state changes, either because of a + compositor action or because of a request in this protocol. + + + + + + + + This event is sent after all changes in the toplevel state have been + sent. + + This allows changes to the zwlr_foreign_toplevel_handle_v1 properties + to be seen as atomic, even if they happen via multiple events. + + + + + + Send a request to the toplevel to close itself. The compositor would + typically use a shell-specific method to carry out this request, for + example by sending the xdg_toplevel.close event. However, this gives + no guarantees the toplevel will actually be destroyed. If and when + this happens, the zwlr_foreign_toplevel_handle_v1.closed event will + be emitted. + + + + + + The rectangle of the surface specified in this request corresponds to + the place where the app using this protocol represents the given toplevel. + It can be used by the compositor as a hint for some operations, e.g + minimizing. The client is however not required to set this, in which + case the compositor is free to decide some default value. + + If the client specifies more than one rectangle, only the last one is + considered. + + The dimensions are given in surface-local coordinates. + Setting width=height=0 removes the already-set rectangle. + + + + + + + + + + + + + + + + This event means the toplevel has been destroyed. It is guaranteed there + won't be any more events for this zwlr_foreign_toplevel_handle_v1. The + toplevel itself becomes inert so any requests will be ignored except the + destroy request. + + + + + + Destroys the zwlr_foreign_toplevel_handle_v1 object. + + This request should be called either when the client does not want to + use the toplevel anymore or after the closed event to finalize the + destruction of the object. + + + + + + + + Requests that the toplevel be fullscreened on the given output. If the + fullscreen state and/or the outputs the toplevel is visible on actually + change, this will be indicated by the state and output_enter/leave + events. + + The output parameter is only a hint to the compositor. Also, if output + is NULL, the compositor should decide which output the toplevel will be + fullscreened on, if at all. + + + + + + + Requests that the toplevel be unfullscreened. If the fullscreen state + actually changes, this will be indicated by the state event. + + + + + + + + This event is emitted whenever the parent of the toplevel changes. + + No event is emitted when the parent handle is destroyed by the client. + + + + + diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index ac80ebd0..5c369f2b 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -18,6 +17,10 @@ #include "shell_integration.hpp" #include "window.hpp" +#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) +#include +#endif + // clang-format off [[nodiscard]] QtWayland::zwlr_layer_shell_v1::layer toWaylandLayer(const WlrLayer::Enum& layer) noexcept; [[nodiscard]] QtWayland::zwlr_layer_surface_v1::anchor toWaylandAnchors(const Anchors& anchors) noexcept; @@ -72,7 +75,10 @@ QSWaylandLayerSurface::QSWaylandLayerSurface( } QSWaylandLayerSurface::~QSWaylandLayerSurface() { - this->ext->surface = nullptr; + if (this->ext != nullptr) { + this->ext->surface = nullptr; + } + this->destroy(); } @@ -106,6 +112,7 @@ void QSWaylandLayerSurface::applyConfigure() { } void QSWaylandLayerSurface::setWindowGeometry(const QRect& geometry) { + if (this->ext == nullptr) return; auto size = constrainedSize(this->ext->mAnchors, geometry.size()); this->set_size(size.width(), size.height()); } diff --git a/src/wayland/wlr_layershell/window.cpp b/src/wayland/wlr_layershell/window.cpp index 035bae1d..a671d59e 100644 --- a/src/wayland/wlr_layershell/window.cpp +++ b/src/wayland/wlr_layershell/window.cpp @@ -13,6 +13,12 @@ #include "shell_integration.hpp" #include "surface.hpp" +LayershellWindowExtension::~LayershellWindowExtension() { + if (this->surface != nullptr) { + this->surface->ext = nullptr; + } +} + LayershellWindowExtension* LayershellWindowExtension::get(QWindow* window) { auto v = window->property("layershell_ext"); diff --git a/src/wayland/wlr_layershell/window.hpp b/src/wayland/wlr_layershell/window.hpp index 163f3aa7..37092a6a 100644 --- a/src/wayland/wlr_layershell/window.hpp +++ b/src/wayland/wlr_layershell/window.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -56,6 +57,8 @@ class LayershellWindowExtension: public QObject { public: LayershellWindowExtension(QObject* parent = nullptr): QObject(parent) {} + ~LayershellWindowExtension() override; + Q_DISABLE_COPY_MOVE(LayershellWindowExtension); // returns the layershell extension if attached, otherwise nullptr static LayershellWindowExtension* get(QWindow* window);