Compare commits

...

35 Commits

Author SHA1 Message Date
outfoxxed 67783ec24c
core/transformwatcher: fix crash when a or b is destroyed
Usually happens during reload.
2024-06-09 15:42:38 -07:00
outfoxxed b5b9c1f6c3
wayland/toplevel_management: add foreign toplevel management 2024-06-07 04:31:20 -07:00
outfoxxed 5d1def3e49
hyprland/ipc: fix monitorFor returning null during HyprlandIpc init 2024-06-06 00:59:17 -07:00
outfoxxed bc349998df
hyprland/ipc: match by name in refreshMonitors instead of id
Was causing ghost/duplicate monitors from usages where the id was not known.
2024-06-06 00:58:10 -07:00
outfoxxed ef1a4134f0
hyprland/ipc: re-request monitors and workspaces on fail 2024-06-06 00:46:38 -07:00
outfoxxed d14ca70984
hyprland/ipc: add hyprland ipc
Only monitors and workspaces are fully tracked for now.
2024-06-05 19:26:20 -07:00
outfoxxed be237b6ab5
core/elapsedtimer: add ElapsedTimer 2024-06-04 13:48:54 -07:00
outfoxxed 37fecfc990
docs: add commit style instructions 2024-06-03 00:38:22 -07:00
outfoxxed b1f5a5eb94
service/mpris: preserve mpris watcher and players across reload 2024-06-02 16:18:45 -07:00
outfoxxed 9d5dd402b9
docs: recommend packagers add a dependency on qtsvg 2024-06-02 15:37:47 -07:00
outfoxxed 29f02d837d
all: remove NVIDIA workarounds
They fixed the driver.
2024-06-02 15:36:33 -07:00
outfoxxed 7d20b472dd
misc: remove the docs and examples submodules
They have not been correctly updated in lock-step for a while now.
2024-06-02 15:23:19 -07:00
outfoxxed bd504daf56
docs: add build, packaging and development instructions 2024-06-02 14:50:23 -07:00
outfoxxed 238ca8cf0b
core/reloader: fix crashing on failed reload 2024-05-31 04:03:00 -07:00
outfoxxed a8506edbb9
build: link jemalloc by default to reduce heap fragmentation
The QML engine and the quickshell reloader both cause large amounts of
heap fragmentation that stacks up over time, leading to a perceived
memory leak. Jemalloc is able to handle the fragmentation much better,
leading to lower user facing memory usage.
2024-05-31 01:28:35 -07:00
outfoxxed d56c07ceb3
core/reloader: simplify generation teardown
The extra complexity previously masked the use after free in 6c95267.
2024-05-31 00:27:18 -07:00
outfoxxed 84bb4098ad
core/reloader: fix incorrect generation teardown on hard reload 2024-05-31 00:26:34 -07:00
outfoxxed 6c9526761c
wayland: fix UAF in layershell surface destructor 2024-05-31 00:24:58 -07:00
outfoxxed 7feae55ebe
core/reloader: add reload signals for visual notifications 2024-05-30 02:39:37 -07:00
outfoxxed 569c40494d
all: import module dependencies via qmldir
Improves compatibility with qml tooling.
2024-05-29 19:29:57 -07:00
outfoxxed 0519acf1d6
core: support `root:` and `root:/` paths for the config root
This works everywhere urls are accepted and rewrites them from the
config root as a qsintercept url.
2024-05-29 15:07:10 -07:00
outfoxxed 33fac67798
core: use the simple animation driver
Seems to provide much higher quality animations.
2024-05-28 20:22:01 -07:00
outfoxxed 7ad3671dd1
core/reloader: fix file watcher compatibility with vim 2024-05-28 15:36:25 -07:00
outfoxxed 4e92d82992
core: add options to enable QML debugging 2024-05-27 22:51:49 -07:00
outfoxxed 5a84e73442
core/objectmodel: add signals for changes to the list 2024-05-23 19:16:08 -07:00
outfoxxed 06240ccf80
service/mpris: improve compatibility with noncompliant players 2024-05-23 18:15:49 -07:00
outfoxxed 5016dbf0d4
all: replace list properties with ObjectModels 2024-05-23 17:28:07 -07:00
outfoxxed 6326f60ce2
service/mpris: re-query position on playback and metadata change 2024-05-23 02:38:26 -07:00
outfoxxed ac339cb23b
service/mpris: expose desktopEntry property 2024-05-22 05:40:03 -07:00
outfoxxed f2df3da596
service/mpris: fix position being incorrect after pausing 2024-05-22 04:34:56 -07:00
outfoxxed ed3708f5cb
service/mpris: add trackChanged signal 2024-05-21 05:07:24 -07:00
outfoxxed af45502913
service/mpris: add mpris module 2024-05-21 04:10:30 -07:00
outfoxxed 4ee9ac7f7c
service/mpris: finish mpris implementation 2024-05-21 04:09:19 -07:00
kossLAN 3b6d1c3bd8
feat: mpris 2024-05-21 04:09:19 -07:00
outfoxxed 73cfeba61b
x11: add XPanelWindow 2024-05-20 02:16:44 -07:00
87 changed files with 5021 additions and 363 deletions

View File

@ -5,6 +5,7 @@ Checks: >
-*,
bugprone-*,
-bugprone-easily-swappable-parameters,
-bugprone-forward-declararion-namespace,
concurrency-*,
cppcoreguidelines-*,
-cppcoreguidelines-owning-memory,

4
.gitignore vendored
View File

@ -1,3 +1,7 @@
# related repos
/docs
/examples
# build files
/result
/build/

6
.gitmodules vendored
View File

@ -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

165
BUILD.md Normal file
View File

@ -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
```

View File

@ -9,31 +9,39 @@ 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)
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")
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
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()
@ -87,7 +95,7 @@ if (WAYLAND)
list(APPEND QT_FPDEPS WaylandClient)
endif()
if (SERVICE_STATUS_NOTIFIER)
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS)
set(DBUS ON)
endif()
@ -128,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()

99
CONTRIBUTING.md Normal file
View File

@ -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 [<debug|release> [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.

View File

@ -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,75 +35,33 @@ This repo has a nix flake you can use to install the package directly:
Quickshell's binary is available at `quickshell.packages.<system>.default` to be added to
lists such as `environment.systemPackages` or `home.packages`.
`quickshell.packages.<system>.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.<system>.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
- pkg-config
- ninja
- Qt6 [ QtBase, QtDeclarative ]
To build with wayland support you will additionally need:
- wayland
- wayland-scanner (may be part of wayland on some distros)
- wayland-protocols
- Qt6 [ QtWayland ]
### 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 [<debug|release> [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

View File

@ -8,8 +8,11 @@
cmake,
ninja,
qt6,
jemalloc,
wayland,
wayland-protocols,
xorg,
pipewire,
gitRev ? (let
headExists = builtins.pathExists ./.git/HEAD;
@ -23,10 +26,12 @@
else "unknown"),
debug ? false,
enableWayland ? 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";
@ -36,21 +41,23 @@
cmake
ninja
qt6.wrapQtAppsHook
] ++ (lib.optionals enableWayland [
pkg-config
] ++ (lib.optionals withWayland [
wayland-protocols
wayland-scanner
]);
buildInputs = with pkgs; [
buildInputs = [
qt6.qtbase
qt6.qtdeclarative
]
++ (lib.optionals enableWayland [ qt6.qtwayland wayland ])
++ (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
@ -63,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;

1
docs

@ -1 +0,0 @@
Subproject commit ff5da84a8b258a9b2caaf978ddb6de23635d8903

@ -1 +0,0 @@
Subproject commit b9e744b50673304dfddb68f3da2a2e906d028b96

View File

@ -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 {

View File

@ -11,6 +11,10 @@ endif()
if (WAYLAND)
add_subdirectory(wayland)
endif ()
endif()
if (X11)
add_subdirectory(x11)
endif()
add_subdirectory(services)

View File

@ -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}")

View File

@ -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(...)

22
src/core/elapsedtimer.cpp Normal file
View File

@ -0,0 +1,22 @@
#include "elapsedtimer.hpp"
#include <qtypes.h>
ElapsedTimer::ElapsedTimer() { this->timer.start(); }
qreal ElapsedTimer::elapsed() { return static_cast<qreal>(this->elapsedNs()) / 1000000000.0; }
qreal ElapsedTimer::restart() { return static_cast<qreal>(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();
}

45
src/core/elapsedtimer.hpp Normal file
View File

@ -0,0 +1,45 @@
#pragma once
#include <qelapsedtimer.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
///! 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;
};

View File

@ -4,6 +4,8 @@
#include <qcontainerfwd.h>
#include <qcoreapplication.h>
#include <qdebug.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qhash.h>
#include <qlogging.h>
@ -12,7 +14,6 @@
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmlincubator.h>
#include <qtimer.h>
#include <qtmetamacros.h>
#include "iconimageprovider.hpp"
@ -25,8 +26,10 @@
static QHash<QQmlEngine*, EngineGeneration*> 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<QObject*>(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;
}
}

View File

@ -1,9 +1,11 @@
#pragma once
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qfilesystemwatcher.h>
#include <qobject.h>
#include <qpair.h>
#include <qqmlengine.h>
#include <qqmlincubator.h>
#include <qtclasshelpermacros.h>
@ -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<QString> 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:

View File

@ -10,6 +10,7 @@
#include <qguiapplication.h>
#include <qhash.h>
#include <qlogging.h>
#include <qqmldebug.h>
#include <qquickwindow.h>
#include <qstandardpaths.h>
#include <qstring.h>
@ -29,6 +30,9 @@ int qs_main(int argc, char** argv) {
auto desktopSettingsAware = true;
QHash<QString, QString> 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);
}

74
src/core/model.cpp Normal file
View File

@ -0,0 +1,74 @@
#include "model.hpp"
#include <qabstractitemmodel.h>
#include <qhash.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
qint32 UntypedObjectModel::rowCount(const QModelIndex& parent) const {
if (parent != QModelIndex()) return 0;
return static_cast<qint32>(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<int, QByteArray> UntypedObjectModel::roleNames() const { return {{0, "modelData"}}; }
QQmlListProperty<QObject> UntypedObjectModel::values() {
return QQmlListProperty<QObject>(
this,
nullptr,
&UntypedObjectModel::valuesCount,
&UntypedObjectModel::valueAt
);
}
qsizetype UntypedObjectModel::valuesCount(QQmlListProperty<QObject>* property) {
return static_cast<UntypedObjectModel*>(property->object)->valuesList.count(); // NOLINT
}
QObject* UntypedObjectModel::valueAt(QQmlListProperty<QObject>* property, qsizetype index) {
return static_cast<UntypedObjectModel*>(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<qint32>(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<qint32>(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); }

94
src/core/model.hpp Normal file
View File

@ -0,0 +1,94 @@
#pragma once
#include <qabstractitemmodel.h>
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#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<QObject> 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<int, QByteArray> roleNames() const override;
[[nodiscard]] QQmlListProperty<QObject> 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<QObject*> valuesList;
private:
static qsizetype valuesCount(QQmlListProperty<QObject>* property);
static QObject* valueAt(QQmlListProperty<QObject>* property, qsizetype index);
};
template <typename T>
class ObjectModel: public UntypedObjectModel {
public:
explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {}
[[nodiscard]] const QVector<T*>& valueList() const {
return *reinterpret_cast<const QVector<T*>*>(&this->valuesList); // NOLINT
}
void insertObject(T* object, qsizetype index = -1) {
this->UntypedObjectModel::insertObject(object, index);
}
void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); }
};

View File

@ -18,5 +18,7 @@ headers = [
"easingcurve.hpp",
"transformwatcher.hpp",
"boundcomponent.hpp",
"model.hpp",
"elapsedtimer.hpp",
]
-----

View File

@ -117,6 +117,10 @@ class PanelWindowInterface: public WindowInterface {
Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged);
/// Defaults to `ExclusionMode.Auto`.
Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged);
/// If the panel should render above standard windows. Defaults to true.
QSDOC_HIDE Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged);
/// Defaults to false.
QSDOC_HIDE Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged);
// clang-format on
QSDOC_NAMED_ELEMENT(PanelWindow);
@ -135,9 +139,17 @@ public:
[[nodiscard]] virtual ExclusionMode::Enum exclusionMode() const = 0;
virtual void setExclusionMode(ExclusionMode::Enum exclusionMode) = 0;
[[nodiscard]] virtual bool aboveWindows() const = 0;
virtual void setAboveWindows(bool aboveWindows) = 0;
[[nodiscard]] virtual bool focusable() const = 0;
virtual void setFocusable(bool focusable) = 0;
signals:
void anchorsChanged();
void marginsChanged();
void exclusiveZoneChanged();
void exclusionModeChanged();
void aboveWindowsChanged();
void focusableChanged();
};

View File

@ -46,7 +46,7 @@ ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(); }
void ProxyWindowBase::onReload(QObject* oldInstance) {
this->window = this->retrieveWindow(oldInstance);
auto wasVisible = this->window != nullptr && this->window->isVisible();
if (this->window == nullptr) this->window = new QQuickWindow();
if (this->window == nullptr) this->window = this->createQQuickWindow();
// The qml engine will leave the WindowInterface as owner of everything
// nested in an item, so we have to make sure the interface's children
@ -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; }

View File

@ -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;
}

View File

@ -110,8 +110,6 @@ class QuickshellGlobal: public QObject {
public:
[[nodiscard]] qint32 processId() const;
QuickshellGlobal(QObject* parent = nullptr);
QQmlListProperty<QuickshellScreenInfo> 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<QuickshellScreenInfo>* prop);
static QuickshellScreenInfo* screenAt(QQmlListProperty<QuickshellScreenInfo>* prop, qsizetype i);
};

View File

@ -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") {

View File

@ -1,5 +1,6 @@
#pragma once
#include <qdir.h>
#include <qhash.h>
#include <qloggingcategory.h>
#include <qnetworkaccessmanager.h>
@ -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 {

View File

@ -8,6 +8,7 @@
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlengine.h>
#include <qtmetamacros.h>
#include <qurl.h>
#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<ShellRoot*>(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() {

View File

@ -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()) {

View File

@ -1,6 +1,7 @@
#pragma once
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qhash.h>
#include <qloggingcategory.h>
#include <qvector.h>
@ -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<QString> scannedDirs;
QVector<QString> scannedFiles;
QHash<QString, QString> qmldirIntercepts;
private:
QDir rootPath;
};

View File

@ -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();
}

View File

@ -60,6 +60,9 @@ signals:
private slots:
void recalcChains();
void itemDestroyed();
void aDestroyed();
void bDestroyed();
private:
void resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent);

View File

@ -188,9 +188,9 @@ public:
dbus::DBusPropertyGroup properties;
dbus::DBusProperty<quint32> version {this->properties, "Version"};
dbus::DBusProperty<QString> textDirection {this->properties, "TextDirection"};
dbus::DBusProperty<QString> textDirection {this->properties, "TextDirection", "", false};
dbus::DBusProperty<QString> status {this->properties, "Status"};
dbus::DBusProperty<QStringList> iconThemePath {this->properties, "IconThemePath"};
dbus::DBusProperty<QStringList> iconThemePath {this->properties, "IconThemePath", {}, false};
void prepareToShow(qint32 item, bool sendOpened);
void updateLayout(qint32 parent, qint32 depth);

View File

@ -112,6 +112,8 @@ void asyncReadPropertyInternal(
}
void AbstractDBusProperty::tryUpdate(const QVariant& variant) {
this->mExists = true;
auto error = this->read(variant);
if (error.isValid()) {
qCWarning(logDbusProperties).noquote()
@ -159,6 +161,44 @@ void AbstractDBusProperty::update() {
}
}
void AbstractDBusProperty::write() {
if (this->group == nullptr) {
qFatal(logDbusProperties) << "Tried to write dbus property" << this->name
<< "which is not attached to a group";
} else {
const QString propStr = this->toString();
if (this->group->interface == nullptr) {
qFatal(logDbusProperties).noquote()
<< "Tried to write property" << propStr << "of a disconnected interface";
}
qCDebug(logDbusProperties).noquote() << "Writing property" << propStr;
auto pendingCall = this->group->propertyInterface->Set(
this->group->interface->interface(),
this->name,
QDBusVariant(this->serialize())
);
auto* call = new QDBusPendingCallWatcher(pendingCall, this);
auto responseCallback = [propStr](QDBusPendingCallWatcher* call) {
const QDBusPendingReply<> reply = *call;
if (reply.isError()) {
qCWarning(logDbusProperties).noquote() << "Error writing property" << propStr;
qCWarning(logDbusProperties) << reply.error();
}
delete call;
};
QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
}
}
bool AbstractDBusProperty::exists() const { return this->mExists; }
QString AbstractDBusProperty::toString() const {
const QString group = this->group == nullptr ? "{ NO GROUP }" : this->group->toString();
return group + ':' + this->name;
@ -232,7 +272,7 @@ void DBusPropertyGroup::updateAllViaGetAll() {
} else {
qCDebug(logDbusProperties).noquote()
<< "Received GetAll property set for" << this->toString();
this->updatePropertySet(reply.value());
this->updatePropertySet(reply.value(), true);
}
delete call;
@ -242,7 +282,7 @@ void DBusPropertyGroup::updateAllViaGetAll() {
QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
}
void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) {
void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool complainMissing) {
for (const auto [name, value]: properties.asKeyValueRange()) {
auto prop = std::find_if(
this->properties.begin(),
@ -251,11 +291,21 @@ void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) {
);
if (prop == this->properties.end()) {
qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" << this;
qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for"
<< this->toString();
} else {
(*prop)->tryUpdate(value);
}
}
if (complainMissing) {
for (const auto* prop: this->properties) {
if (prop->required && !properties.contains(prop->name)) {
qCWarning(logDbusProperties)
<< prop->name << "missing from property set for" << this->toString();
}
}
}
}
QString DBusPropertyGroup::toString() const {
@ -291,7 +341,7 @@ void DBusPropertyGroup::onPropertiesChanged(
}
}
this->updatePropertySet(changedProperties);
this->updatePropertySet(changedProperties, false);
}
} // namespace qs::dbus

View File

@ -79,22 +79,31 @@ class AbstractDBusProperty: public QObject {
Q_OBJECT;
public:
explicit AbstractDBusProperty(QString name, const QMetaType& type, QObject* parent = nullptr)
explicit AbstractDBusProperty(
QString name,
const QMetaType& type,
bool required,
QObject* parent = nullptr
)
: QObject(parent)
, name(std::move(name))
, type(type) {}
, type(type)
, required(required) {}
[[nodiscard]] bool exists() const;
[[nodiscard]] QString toString() const;
[[nodiscard]] virtual QString valueString() = 0;
public slots:
void update();
void write();
signals:
void changed();
protected:
virtual QDBusError read(const QVariant& variant) = 0;
virtual QVariant serialize() = 0;
private:
void tryUpdate(const QVariant& variant);
@ -103,6 +112,8 @@ private:
QString name;
QMetaType type;
bool required;
bool mExists = false;
friend class DBusPropertyGroup;
};
@ -133,7 +144,7 @@ private slots:
);
private:
void updatePropertySet(const QVariantMap& properties);
void updatePropertySet(const QVariantMap& properties, bool complainMissing);
DBusPropertiesInterface* propertyInterface = nullptr;
QDBusAbstractInterface* interface = nullptr;
@ -145,17 +156,23 @@ private:
template <typename T>
class DBusProperty: public AbstractDBusProperty {
public:
explicit DBusProperty(QString name, QObject* parent = nullptr, T value = T())
: AbstractDBusProperty(std::move(name), QMetaType::fromType<T>(), parent)
explicit DBusProperty(
QString name,
T value = T(),
bool required = true,
QObject* parent = nullptr
)
: AbstractDBusProperty(std::move(name), QMetaType::fromType<T>(), required, parent)
, value(std::move(value)) {}
explicit DBusProperty(
DBusPropertyGroup& group,
QString name,
QObject* parent = nullptr,
T value = T()
T value = T(),
bool required = true,
QObject* parent = nullptr
)
: DBusProperty(std::move(name), parent, std::move(value)) {
: DBusProperty(std::move(name), std::move(value), required, parent) {
group.attachProperty(this);
}
@ -165,7 +182,7 @@ public:
return str;
}
[[nodiscard]] T get() const { return this->value; }
[[nodiscard]] const T& get() const { return this->value; }
void set(T value) {
this->value = std::move(value);
@ -183,6 +200,8 @@ protected:
return result.error;
}
QVariant serialize() override { return QVariant::fromValue(this->value); }
private:
T value;

View File

@ -5,3 +5,7 @@ endif()
if (SERVICE_PIPEWIRE)
add_subdirectory(pipewire)
endif()
if (SERVICE_MPRIS)
add_subdirectory(mpris)
endif()

View File

@ -0,0 +1,39 @@
set_source_files_properties(org.mpris.MediaPlayer2.Player.xml PROPERTIES
CLASSNAME DBusMprisPlayer
NO_NAMESPACE TRUE
)
qt_add_dbus_interface(DBUS_INTERFACES
org.mpris.MediaPlayer2.Player.xml
dbus_player
)
set_source_files_properties(org.mpris.MediaPlayer2.xml PROPERTIES
CLASSNAME DBusMprisPlayerApp
NO_NAMESPACE TRUE
)
qt_add_dbus_interface(DBUS_INTERFACES
org.mpris.MediaPlayer2.xml
dbus_player_app
)
qt_add_library(quickshell-service-mpris STATIC
player.cpp
watcher.cpp
${DBUS_INTERFACES}
)
# dbus headers
target_include_directories(quickshell-service-mpris PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
qt_add_qml_module(quickshell-service-mpris
URI Quickshell.Services.Mpris
VERSION 0.1
)
target_link_libraries(quickshell-service-mpris PRIVATE ${QT_DEPS} quickshell-dbus)
target_link_libraries(quickshell PRIVATE quickshell-service-mprisplugin)
qs_pch(quickshell-service-mpris)
qs_pch(quickshell-service-mprisplugin)

View File

@ -0,0 +1,7 @@
name = "Quickshell.Services.Mpris"
description = "Mpris Service"
headers = [
"player.hpp",
"watcher.hpp",
]
-----

View File

@ -0,0 +1,24 @@
<node>
<interface name="org.mpris.MediaPlayer2.Player">
<method name="OpenUri">
<arg direction="in" type="s" name="Uri"/>
</method>
<method name="SetPosition">
<arg direction="in" type="o" name="TrackId"/>
<arg direction="in" type="x" name="Position"/>
</method>
<method name="Seek">
<arg direction="in" type="x" name="Offset"/>
</method>
<method name="PlayPause"/>
<method name="Next"/>
<method name="Previous"/>
<method name="Stop"/>
<method name="Play"/>
<method name="Pause"/>
<signal name="Seeked">
<arg type="x" name="Position"/>
</signal>
</interface>
</node>

View File

@ -0,0 +1,6 @@
<node>
<interface name="org.mpris.MediaPlayer2">
<method name="Raise"/>
<method name="Quit"/>
</interface>
</node>

View File

@ -0,0 +1,459 @@
#include "player.hpp"
#include <qcontainerfwd.h>
#include <qdatetime.h>
#include <qdbusconnection.h>
#include <qdbusextratypes.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../dbus/properties.hpp"
#include "dbus_player.h"
#include "dbus_player_app.h"
using namespace qs::dbus;
namespace qs::service::mpris {
Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg);
QString MprisPlaybackState::toString(MprisPlaybackState::Enum status) {
switch (status) {
case MprisPlaybackState::Stopped: return "Stopped";
case MprisPlaybackState::Playing: return "Playing";
case MprisPlaybackState::Paused: return "Paused";
default: return "Unknown Status";
}
}
QString MprisLoopState::toString(MprisLoopState::Enum status) {
switch (status) {
case MprisLoopState::None: return "None";
case MprisLoopState::Track: return "Track";
case MprisLoopState::Playlist: return "Playlist";
default: return "Unknown Status";
}
}
MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(parent) {
this->app = new DBusMprisPlayerApp(
address,
"/org/mpris/MediaPlayer2",
QDBusConnection::sessionBus(),
this
);
this->player =
new DBusMprisPlayer(address, "/org/mpris/MediaPlayer2", QDBusConnection::sessionBus(), this);
if (!this->player->isValid() || !this->app->isValid()) {
qCWarning(logMprisPlayer) << "Cannot create MprisPlayer for" << address;
return;
}
// clang-format off
QObject::connect(&this->pCanQuit, &AbstractDBusProperty::changed, this, &MprisPlayer::canQuitChanged);
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);
QObject::connect(&this->pCanControl, &AbstractDBusProperty::changed, this, &MprisPlayer::canControlChanged);
QObject::connect(&this->pCanSeek, &AbstractDBusProperty::changed, this, &MprisPlayer::canSeekChanged);
QObject::connect(&this->pCanGoNext, &AbstractDBusProperty::changed, this, &MprisPlayer::canGoNextChanged);
QObject::connect(&this->pCanGoPrevious, &AbstractDBusProperty::changed, this, &MprisPlayer::canGoPreviousChanged);
QObject::connect(&this->pCanPlay, &AbstractDBusProperty::changed, this, &MprisPlayer::canPlayChanged);
QObject::connect(&this->pCanPause, &AbstractDBusProperty::changed, this, &MprisPlayer::canPauseChanged);
QObject::connect(&this->pPosition, &AbstractDBusProperty::changed, this, &MprisPlayer::onPositionChanged);
QObject::connect(this->player, &DBusMprisPlayer::Seeked, this, &MprisPlayer::onSeek);
QObject::connect(&this->pVolume, &AbstractDBusProperty::changed, this, &MprisPlayer::volumeChanged);
QObject::connect(&this->pMetadata, &AbstractDBusProperty::changed, this, &MprisPlayer::onMetadataChanged);
QObject::connect(&this->pPlaybackStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::onPlaybackStatusChanged);
QObject::connect(&this->pLoopStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::onLoopStatusChanged);
QObject::connect(&this->pRate, &AbstractDBusProperty::changed, this, &MprisPlayer::rateChanged);
QObject::connect(&this->pMinRate, &AbstractDBusProperty::changed, this, &MprisPlayer::minRateChanged);
QObject::connect(&this->pMaxRate, &AbstractDBusProperty::changed, this, &MprisPlayer::maxRateChanged);
QObject::connect(&this->pShuffle, &AbstractDBusProperty::changed, this, &MprisPlayer::shuffleChanged);
QObject::connect(&this->playerProperties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished);
// Ensure user triggered position updates can update length.
QObject::connect(this, &MprisPlayer::positionChanged, this, &MprisPlayer::onExportedPositionChanged);
// clang-format on
this->appProperties.setInterface(this->app);
this->playerProperties.setInterface(this->player);
this->appProperties.updateAllViaGetAll();
this->playerProperties.updateAllViaGetAll();
}
void MprisPlayer::raise() {
if (!this->canRaise()) {
qWarning() << "Cannot call raise() on" << this << "because canRaise is false.";
return;
}
this->app->Raise();
}
void MprisPlayer::quit() {
if (!this->canQuit()) {
qWarning() << "Cannot call quit() on" << this << "because canQuit is false.";
return;
}
this->app->Quit();
}
void MprisPlayer::openUri(const QString& uri) { this->player->OpenUri(uri); }
void MprisPlayer::next() {
if (!this->canGoNext()) {
qWarning() << "Cannot call next() on" << this << "because canGoNext is false.";
return;
}
this->player->Next();
}
void MprisPlayer::previous() {
if (!this->canGoPrevious()) {
qWarning() << "Cannot call previous() on" << this << "because canGoPrevious is false.";
return;
}
this->player->Previous();
}
void MprisPlayer::seek(qreal offset) {
if (!this->canSeek()) {
qWarning() << "Cannot call seek() on" << this << "because canSeek is false.";
return;
}
auto target = static_cast<qlonglong>(offset * 1000) * 1000;
this->player->Seek(target);
}
bool MprisPlayer::isValid() const { return this->player->isValid(); }
QString MprisPlayer::address() const { return this->player->service(); }
bool MprisPlayer::canControl() const { return this->pCanControl.get(); }
bool MprisPlayer::canPlay() const { return this->canControl() && this->pCanPlay.get(); }
bool MprisPlayer::canPause() const { return this->canControl() && this->pCanPause.get(); }
bool MprisPlayer::canSeek() const { return this->canControl() && this->pCanSeek.get(); }
bool MprisPlayer::canGoNext() const { return this->canControl() && this->pCanGoNext.get(); }
bool MprisPlayer::canGoPrevious() const { return this->canControl() && this->pCanGoPrevious.get(); }
bool MprisPlayer::canQuit() const { return this->pCanQuit.get(); }
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
if (this->mPlaybackState == MprisPlaybackState::Stopped) return 0;
auto paused = this->mPlaybackState == MprisPlaybackState::Paused;
auto time = paused ? this->pausedTime : QDateTime::currentDateTime();
auto offset = time - this->lastPositionTimestamp;
auto rateMul = static_cast<qlonglong>(this->pRate.get() * 1000);
offset = (offset * rateMul) / 1000;
return (this->pPosition.get() / 1000) + offset.count();
}
qreal MprisPlayer::position() const {
if (!this->positionSupported()) return 0; // unsupported
if (this->mPlaybackState == MprisPlaybackState::Stopped) return 0;
return static_cast<qreal>(this->positionMs()) / 1000.0; // NOLINT
}
bool MprisPlayer::positionSupported() const { return this->pPosition.exists(); }
void MprisPlayer::setPosition(qreal position) {
if (this->pPosition.get() == -1) {
qWarning() << "Cannot set position of" << this << "because position is not supported.";
return;
}
if (!this->canSeek()) {
qWarning() << "Cannot set position of" << this << "because canSeek is false.";
return;
}
auto target = static_cast<qlonglong>(position * 1000) * 1000;
if (!this->mTrackId.isEmpty()) {
this->player->SetPosition(QDBusObjectPath(this->mTrackId), target);
} else {
auto pos = this->positionMs() * 1000;
this->player->Seek(target - pos);
}
this->pPosition.set(target);
}
void MprisPlayer::onPositionChanged() {
const bool firstChange = !this->lastPositionTimestamp.isValid();
this->lastPositionTimestamp = QDateTime::currentDateTimeUtc();
emit this->positionChanged();
if (firstChange) emit this->positionSupportedChanged();
}
void MprisPlayer::onExportedPositionChanged() {
if (!this->lengthSupported()) emit this->lengthChanged();
}
void MprisPlayer::onSeek(qlonglong time) { this->pPosition.set(time); }
qreal MprisPlayer::length() const {
if (this->mLength == -1) {
return this->position(); // unsupported
} else {
return static_cast<qreal>(this->mLength / 1000) / 1000; // NOLINT
}
}
bool MprisPlayer::lengthSupported() const { return this->mLength != -1; }
qreal MprisPlayer::volume() const { return this->pVolume.get(); }
bool MprisPlayer::volumeSupported() const { return this->pVolume.exists(); }
void MprisPlayer::setVolume(qreal volume) {
if (!this->canControl()) {
qWarning() << "Cannot set volume of" << this << "because canControl is false.";
return;
}
if (!this->volumeSupported()) {
qWarning() << "Cannot set volume of" << this << "because volume is not supported.";
return;
}
this->pVolume.set(volume);
this->pVolume.write();
}
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<qlonglong>()) {
length = lengthVariant.value<qlonglong>();
}
if (length != this->mLength) {
this->mLength = length;
emit this->lengthChanged();
}
auto trackChanged = false;
auto trackidVariant = this->pMetadata.get().value("mpris:trackid");
if (trackidVariant.isValid() && trackidVariant.canConvert<QString>()) {
auto trackId = trackidVariant.value<QString>();
if (trackId != this->mTrackId) {
this->mTrackId = trackId;
trackChanged = true;
}
}
// Helps to catch players without trackid.
auto urlVariant = this->pMetadata.get().value("xesam:url");
if (urlVariant.isValid() && urlVariant.canConvert<QString>()) {
auto url = urlVariant.value<QString>();
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; }
void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) {
if (playbackState == this->mPlaybackState) return;
switch (playbackState) {
case MprisPlaybackState::Stopped:
if (!this->canControl()) {
qWarning() << "Cannot set playbackState of" << this
<< "to Stopped because canControl is false.";
return;
}
this->player->Stop();
break;
case MprisPlaybackState::Playing:
if (!this->canPlay()) {
qWarning() << "Cannot set playbackState of" << this << "to Playing because canPlay is false.";
return;
}
this->player->Play();
break;
case MprisPlaybackState::Paused:
if (!this->canPause()) {
qWarning() << "Cannot set playbackState of" << this << "to Paused because canPause is false.";
return;
}
this->player->Pause();
break;
default:
qWarning() << "Cannot set playbackState of" << this << "to unknown value" << playbackState;
return;
}
}
void MprisPlayer::onPlaybackStatusChanged() {
const auto& status = this->pPlaybackStatus.get();
auto state = MprisPlaybackState::Stopped;
if (status == "Playing") {
state = MprisPlaybackState::Playing;
} else if (status == "Paused") {
this->pausedTime = QDateTime::currentDateTimeUtc();
state = MprisPlaybackState::Paused;
} else if (status == "Stopped") {
state = MprisPlaybackState::Stopped;
} else {
state = MprisPlaybackState::Stopped;
qWarning() << "Received unexpected PlaybackStatus for" << this << status;
}
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; }
bool MprisPlayer::loopSupported() const { return this->pLoopStatus.exists(); }
void MprisPlayer::setLoopState(MprisLoopState::Enum loopState) {
if (!this->canControl()) {
qWarning() << "Cannot set loopState of" << this << "because canControl is false.";
return;
}
if (!this->loopSupported()) {
qWarning() << "Cannot set loopState of" << this << "because loop state is not supported.";
return;
}
if (loopState == this->mLoopState) return;
QString loopStatusStr;
switch (loopState) {
case MprisLoopState::None: loopStatusStr = "None"; break;
case MprisLoopState::Track: loopStatusStr = "Track"; break;
case MprisLoopState::Playlist: loopStatusStr = "Playlist"; break;
default:
qWarning() << "Cannot set loopState of" << this << "to unknown value" << loopState;
return;
}
this->pLoopStatus.set(loopStatusStr);
this->pLoopStatus.write();
}
void MprisPlayer::onLoopStatusChanged() {
const auto& status = this->pLoopStatus.get();
if (status == "None") {
this->mLoopState = MprisLoopState::None;
} else if (status == "Track") {
this->mLoopState = MprisLoopState::Track;
} else if (status == "Playlist") {
this->mLoopState = MprisLoopState::Playlist;
} else {
this->mLoopState = MprisLoopState::None;
qWarning() << "Received unexpected LoopStatus for" << this << status;
}
emit this->loopStateChanged();
}
qreal MprisPlayer::rate() const { return this->pRate.get(); }
qreal MprisPlayer::minRate() const { return this->pMinRate.get(); }
qreal MprisPlayer::maxRate() const { return this->pMaxRate.get(); }
void MprisPlayer::setRate(qreal rate) {
if (rate == this->pRate.get()) return;
if (rate < this->pMinRate.get() || rate > this->pMaxRate.get()) {
qWarning() << "Cannot set rate for" << this << "to" << rate
<< "which is outside of minRate and maxRate" << this->pMinRate.get()
<< this->pMaxRate.get();
return;
}
this->pRate.set(rate);
this->pRate.write();
}
bool MprisPlayer::shuffle() const { return this->pShuffle.get(); }
bool MprisPlayer::shuffleSupported() const { return this->pShuffle.exists(); }
void MprisPlayer::setShuffle(bool shuffle) {
if (!this->shuffleSupported()) {
qWarning() << "Cannot set shuffle for" << this << "because shuffle is not supported.";
return;
}
if (!this->canControl()) {
qWarning() << "Cannot set shuffle state of" << this << "because canControl is false.";
return;
}
this->pShuffle.set(shuffle);
this->pShuffle.write();
}
bool MprisPlayer::fullscreen() const { return this->pFullscreen.get(); }
void MprisPlayer::setFullscreen(bool fullscreen) {
if (!this->canSetFullscreen()) {
qWarning() << "Cannot set fullscreen for" << this << "because canSetFullscreen is false.";
return;
}
this->pFullscreen.set(fullscreen);
this->pFullscreen.write();
}
QList<QString> MprisPlayer::supportedUriSchemes() const { return this->pSupportedUriSchemes.get(); }
QList<QString> MprisPlayer::supportedMimeTypes() const { return this->pSupportedMimeTypes.get(); }
void MprisPlayer::onGetAllFinished() {
if (this->volumeSupported()) emit this->volumeSupportedChanged();
if (this->loopSupported()) emit this->loopSupportedChanged();
if (this->shuffleSupported()) emit this->shuffleSupportedChanged();
emit this->ready();
}
} // namespace qs::service::mpris

View File

@ -0,0 +1,332 @@
#pragma once
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../core/doc.hpp"
#include "../../dbus/properties.hpp"
#include "dbus_player.h"
#include "dbus_player_app.h"
namespace qs::service::mpris {
class MprisPlaybackState: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum {
Stopped = 0,
Playing = 1,
Paused = 2,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(MprisPlaybackState::Enum status);
};
class MprisLoopState: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum {
None = 0,
Track = 1,
Playlist = 2,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(MprisLoopState::Enum status);
};
///! A media player exposed over MPRIS.
/// A media player exposed over MPRIS.
///
/// > [!WARNING] Support for various functionality and general compliance to
/// > the MPRIS specification varies wildly by player.
/// > Always check the associated `canXyz` and `xyzSupported` properties if available.
///
/// > [!INFO] The TrackList and Playlist interfaces were not implemented as we could not
/// > find any media players using them to test against.
class MprisPlayer: public QObject {
Q_OBJECT;
// clang-format off
Q_PROPERTY(bool canControl READ canControl NOTIFY canControlChanged);
Q_PROPERTY(bool canPlay READ canPlay NOTIFY canPlayChanged);
Q_PROPERTY(bool canPause READ canPause NOTIFY canPauseChanged);
Q_PROPERTY(bool canSeek READ canSeek NOTIFY canSeekChanged);
Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY canGoNextChanged);
Q_PROPERTY(bool canGoPrevious READ canGoPrevious NOTIFY canGoPreviousChanged);
Q_PROPERTY(bool canQuit READ canQuit NOTIFY canQuitChanged);
Q_PROPERTY(bool canRaise READ canRaise NOTIFY canRaiseChanged);
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.
///
/// May only be written to if `canSeek` and `positionSupported` are true.
///
/// > [!WARNING] To avoid excessive property updates wasting CPU while `position` is not
/// > actively monitored, `position` usually will not update reactively, unless a nonlinear
/// > change in position occurs, however reading it will always return the current position.
/// >
/// > If you want to actively monitor the position, the simplest way it to emit the `positionChanged`
/// > signal manually for the duration you are monitoring it, Using a [FrameAnimation] if you need
/// > the value to update smoothly, such as on a slider, or a [Timer] if not, as shown below.
/// >
/// > ```qml {filename="Using a FrameAnimation"}
/// > FrameAnimation {
/// > // only emit the signal when the position is actually changing.
/// > running: player.playbackState == MprisPlaybackState.Playing
/// > // emit the positionChanged signal every frame.
/// > onTriggered: player.positionChanged()
/// > }
/// > ```
/// >
/// > ```qml {filename="Using a Timer"}
/// > Timer {
/// > // only emit the signal when the position is actually changing.
/// > running: player.playbackState == MprisPlaybackState.Playing
/// > // Make sure the position updates at least once per second.
/// > interval: 1000
/// > repeat: true
/// > // emit the positionChanged signal every second.
/// > onTriggered: player.positionChanged()
/// > }
/// > ```
///
/// [FrameAnimation]: https://doc.qt.io/qt-6/qml-qtquick-frameanimation.html
/// [Timer]: https://doc.qt.io/qt-6/qml-qtqml-timer.html
Q_PROPERTY(qreal position READ position WRITE setPosition NOTIFY positionChanged);
Q_PROPERTY(bool positionSupported READ positionSupported NOTIFY positionSupportedChanged);
/// The length of the playing track, as seconds, with millisecond precision,
/// or the value of `position` if `lengthSupported` is false.
Q_PROPERTY(qreal length READ length NOTIFY lengthChanged);
Q_PROPERTY(bool lengthSupported READ lengthSupported NOTIFY lengthSupportedChanged);
/// The volume of the playing track from 0.0 to 1.0, or 1.0 if `volumeSupported` is false.
///
/// May only be written to if `canControl` and `volumeSupported` are true.
Q_PROPERTY(qreal volume READ volume WRITE setVolume NOTIFY volumeChanged);
Q_PROPERTY(bool volumeSupported READ volumeSupported NOTIFY volumeSupportedChanged);
/// Metadata of the current track.
///
/// A map of common properties is available [here](https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata).
/// Do not count on any of them actually being present.
Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged);
/// The playback state of the media player.
///
/// - If `canPlay` is false, you cannot assign the `Playing` state.
/// - If `canPause` is false, you cannot assign the `Paused` state.
/// - If `canControl` is false, you cannot assign the `Stopped` state.
/// (or any of the others, though their repsective properties will also be false)
Q_PROPERTY(MprisPlaybackState::Enum playbackState READ playbackState WRITE setPlaybackState NOTIFY playbackStateChanged);
/// The loop state of the media player, or `None` if `loopSupported` is false.
///
/// May only be written to if `canControl` and `loopSupported` are true.
Q_PROPERTY(MprisLoopState::Enum loopState READ loopState WRITE setLoopState NOTIFY loopStateChanged);
Q_PROPERTY(bool loopSupported READ loopSupported NOTIFY loopSupportedChanged);
/// The speed the song is playing at, as a multiplier.
///
/// Only values between `minRate` and `maxRate` (inclusive) may be written to the property.
/// Additionally, It is recommended that you only write common values such as `0.25`, `0.5`, `1.0`, `2.0`
/// to the property, as media players are free to ignore the value, and are more likely to
/// accept common ones.
Q_PROPERTY(qreal rate READ rate WRITE setRate NOTIFY rateChanged);
Q_PROPERTY(qreal minRate READ minRate NOTIFY minRateChanged);
Q_PROPERTY(qreal maxRate READ maxRate NOTIFY maxRateChanged);
/// If the play queue is currently being shuffled, or false if `shuffleSupported` is false.
///
/// May only be written if `canControl` and `shuffleSupported` are true.
Q_PROPERTY(bool shuffle READ shuffle WRITE setShuffle NOTIFY shuffleChanged);
Q_PROPERTY(bool shuffleSupported READ shuffleSupported NOTIFY shuffleSupportedChanged);
/// If the player is currently shown in fullscreen.
///
/// May only be written to if `canSetFullscreen` is true.
Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged);
/// Uri schemes supported by `openUri`.
Q_PROPERTY(QList<QString> supportedUriSchemes READ supportedUriSchemes NOTIFY supportedUriSchemesChanged);
/// Mime types supported by `openUri`.
Q_PROPERTY(QList<QString> supportedMimeTypes READ supportedMimeTypes NOTIFY supportedMimeTypesChanged);
// clang-format on
QML_ELEMENT;
QML_UNCREATABLE("MprisPlayers can only be acquired from Mpris");
public:
explicit MprisPlayer(const QString& address, QObject* parent = nullptr);
/// Bring the media player to the front of the window stack.
///
/// May only be called if `canRaise` is true.
Q_INVOKABLE void raise();
/// Quit the media player.
///
/// May only be called if `canQuit` is true.
Q_INVOKABLE void quit();
/// Open the given URI in the media player.
///
/// Many players will silently ignore this, especially if the uri
/// does not match `supportedUriSchemes` and `supportedMimeTypes`.
Q_INVOKABLE void openUri(const QString& uri);
/// Play the next song.
///
/// May only be called if `canGoNext` is true.
Q_INVOKABLE void next();
/// Play the previous song, or go back to the beginning of the current one.
///
/// May only be called if `canGoPrevious` is true.
Q_INVOKABLE void previous();
/// Change `position` by an offset.
///
/// Even if `positionSupported` is false and you cannot set `position`,
/// this function may work.
///
/// May only be called if `canSeek` is true.
Q_INVOKABLE void seek(qreal offset);
[[nodiscard]] bool isValid() const;
[[nodiscard]] QString address() const;
[[nodiscard]] bool canControl() const;
[[nodiscard]] bool canSeek() const;
[[nodiscard]] bool canGoNext() const;
[[nodiscard]] bool canGoPrevious() const;
[[nodiscard]] bool canPlay() const;
[[nodiscard]] bool canPause() const;
[[nodiscard]] bool canQuit() const;
[[nodiscard]] bool canRaise() const;
[[nodiscard]] bool canSetFullscreen() const;
[[nodiscard]] QString identity() const;
[[nodiscard]] QString desktopEntry() const;
[[nodiscard]] qlonglong positionMs() const;
[[nodiscard]] qreal position() const;
[[nodiscard]] bool positionSupported() const;
void setPosition(qreal position);
[[nodiscard]] qreal length() const;
[[nodiscard]] bool lengthSupported() const;
[[nodiscard]] qreal volume() const;
[[nodiscard]] bool volumeSupported() const;
void setVolume(qreal volume);
[[nodiscard]] QVariantMap metadata() const;
[[nodiscard]] MprisPlaybackState::Enum playbackState() const;
void setPlaybackState(MprisPlaybackState::Enum playbackState);
[[nodiscard]] MprisLoopState::Enum loopState() const;
[[nodiscard]] bool loopSupported() const;
void setLoopState(MprisLoopState::Enum loopState);
[[nodiscard]] qreal rate() const;
[[nodiscard]] qreal minRate() const;
[[nodiscard]] qreal maxRate() const;
void setRate(qreal rate);
[[nodiscard]] bool shuffle() const;
[[nodiscard]] bool shuffleSupported() const;
void setShuffle(bool shuffle);
[[nodiscard]] bool fullscreen() const;
void setFullscreen(bool fullscreen);
[[nodiscard]] QList<QString> supportedUriSchemes() const;
[[nodiscard]] QList<QString> supportedMimeTypes() const;
signals:
void trackChanged();
QSDOC_HIDE void ready();
void canControlChanged();
void canPlayChanged();
void canPauseChanged();
void canSeekChanged();
void canGoNextChanged();
void canGoPreviousChanged();
void canQuitChanged();
void canRaiseChanged();
void canSetFullscreenChanged();
void identityChanged();
void desktopEntryChanged();
void positionChanged();
void positionSupportedChanged();
void lengthChanged();
void lengthSupportedChanged();
void volumeChanged();
void volumeSupportedChanged();
void metadataChanged();
void playbackStateChanged();
void loopStateChanged();
void loopSupportedChanged();
void rateChanged();
void minRateChanged();
void maxRateChanged();
void shuffleChanged();
void shuffleSupportedChanged();
void fullscreenChanged();
void supportedUriSchemesChanged();
void supportedMimeTypesChanged();
private slots:
void onGetAllFinished();
void onPositionChanged();
void onExportedPositionChanged();
void onSeek(qlonglong time);
void onMetadataChanged();
void onPlaybackStatusChanged();
void onLoopStatusChanged();
private:
// clang-format off
dbus::DBusPropertyGroup appProperties;
dbus::DBusProperty<QString> pIdentity {this->appProperties, "Identity"};
dbus::DBusProperty<QString> pDesktopEntry {this->appProperties, "DesktopEntry", "", false};
dbus::DBusProperty<bool> pCanQuit {this->appProperties, "CanQuit"};
dbus::DBusProperty<bool> pCanRaise {this->appProperties, "CanRaise"};
dbus::DBusProperty<bool> pFullscreen {this->appProperties, "Fullscreen", false, false};
dbus::DBusProperty<bool> pCanSetFullscreen {this->appProperties, "CanSetFullscreen", false, false};
dbus::DBusProperty<QList<QString>> pSupportedUriSchemes {this->appProperties, "SupportedUriSchemes"};
dbus::DBusProperty<QList<QString>> pSupportedMimeTypes {this->appProperties, "SupportedMimeTypes"};
dbus::DBusPropertyGroup playerProperties;
dbus::DBusProperty<bool> pCanControl {this->playerProperties, "CanControl"};
dbus::DBusProperty<bool> pCanPlay {this->playerProperties, "CanPlay"};
dbus::DBusProperty<bool> pCanPause {this->playerProperties, "CanPause"};
dbus::DBusProperty<bool> pCanSeek {this->playerProperties, "CanSeek"};
dbus::DBusProperty<bool> pCanGoNext {this->playerProperties, "CanGoNext"};
dbus::DBusProperty<bool> pCanGoPrevious {this->playerProperties, "CanGoPrevious"};
dbus::DBusProperty<qlonglong> pPosition {this->playerProperties, "Position", 0, false}; // "required"
dbus::DBusProperty<double> pVolume {this->playerProperties, "Volume", 1, false}; // "required"
dbus::DBusProperty<QVariantMap> pMetadata {this->playerProperties, "Metadata"};
dbus::DBusProperty<QString> pPlaybackStatus {this->playerProperties, "PlaybackStatus"};
dbus::DBusProperty<QString> pLoopStatus {this->playerProperties, "LoopStatus", "", false};
dbus::DBusProperty<double> pRate {this->playerProperties, "Rate", 1, false}; // "required"
dbus::DBusProperty<double> pMinRate {this->playerProperties, "MinimumRate", 1, false}; // "required"
dbus::DBusProperty<double> pMaxRate {this->playerProperties, "MaximumRate", 1, false}; // "required"
dbus::DBusProperty<bool> pShuffle {this->playerProperties, "Shuffle", false, false};
// clang-format on
MprisPlaybackState::Enum mPlaybackState = MprisPlaybackState::Stopped;
MprisLoopState::Enum mLoopState = MprisLoopState::None;
QDateTime lastPositionTimestamp;
QDateTime pausedTime;
qlonglong mLength = -1;
DBusMprisPlayerApp* app = nullptr;
DBusMprisPlayer* player = nullptr;
QString mTrackId;
QString mUrl;
};
} // namespace qs::service::mpris

View File

@ -0,0 +1,114 @@
#include "watcher.hpp"
#include <qcontainerfwd.h>
#include <qdbusconnection.h>
#include <qdbusconnectioninterface.h>
#include <qdbusservicewatcher.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qqmllist.h>
#include "../../core/model.hpp"
#include "player.hpp"
namespace qs::service::mpris {
Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg);
MprisWatcher::MprisWatcher() {
qCDebug(logMprisWatcher) << "Starting MprisWatcher";
auto bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
qCWarning(logMprisWatcher) << "Could not connect to DBus. Mpris service will not work.";
return;
}
// clang-format off
QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &MprisWatcher::onServiceRegistered);
QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &MprisWatcher::onServiceUnregistered);
// clang-format on
this->serviceWatcher.setWatchMode(
QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration
);
this->serviceWatcher.addWatchedService("org.mpris.MediaPlayer2*");
this->serviceWatcher.setConnection(bus);
this->registerExisting();
}
void MprisWatcher::registerExisting() {
const QStringList& list = QDBusConnection::sessionBus().interface()->registeredServiceNames();
for (const QString& service: list) {
if (service.startsWith("org.mpris.MediaPlayer2")) {
qCDebug(logMprisWatcher).noquote() << "Found Mpris service" << service;
this->registerPlayer(service);
}
}
}
void MprisWatcher::onServiceRegistered(const QString& service) {
if (service.startsWith("org.mpris.MediaPlayer2")) {
qCDebug(logMprisWatcher).noquote() << "Mpris service " << service << " registered.";
this->registerPlayer(service);
} else {
qCWarning(logMprisWatcher) << "Got a registration event for untracked service" << service;
}
}
void MprisWatcher::onServiceUnregistered(const QString& service) {
if (auto* player = this->mPlayers.value(service)) {
player->deleteLater();
this->mPlayers.remove(service);
qCDebug(logMprisWatcher) << "Unregistered MprisPlayer" << service;
} else {
qCWarning(logMprisWatcher) << "Got service unregister event for untracked service" << service;
}
}
void MprisWatcher::onPlayerReady() {
auto* player = qobject_cast<MprisPlayer*>(this->sender());
this->readyPlayers.insertObject(player);
}
void MprisWatcher::onPlayerDestroyed(QObject* object) {
auto* player = static_cast<MprisPlayer*>(object); // NOLINT
this->readyPlayers.removeObject(player);
}
ObjectModel<MprisPlayer>* MprisWatcher::players() { return &this->readyPlayers; }
void MprisWatcher::registerPlayer(const QString& address) {
if (this->mPlayers.contains(address)) {
qCDebug(logMprisWatcher) << "Skipping duplicate registration of MprisPlayer" << address;
return;
}
auto* player = new MprisPlayer(address, this);
if (!player->isValid()) {
qCWarning(logMprisWatcher) << "Ignoring invalid MprisPlayer registration of" << address;
delete player;
return;
}
this->mPlayers.insert(address, player);
QObject::connect(player, &MprisPlayer::ready, this, &MprisWatcher::onPlayerReady);
QObject::connect(player, &QObject::destroyed, this, &MprisWatcher::onPlayerDestroyed);
qCDebug(logMprisWatcher) << "Registered MprisPlayer" << address;
}
MprisWatcher* MprisWatcher::instance() {
static MprisWatcher* instance = new MprisWatcher(); // NOLINT
return instance;
}
ObjectModel<MprisPlayer>* MprisQml::players() { // NOLINT
return MprisWatcher::instance()->players();
}
} // namespace qs::service::mpris

View File

@ -0,0 +1,57 @@
#pragma once
#include <qdbuscontext.h>
#include <qdbusinterface.h>
#include <qdbusservicewatcher.h>
#include <qhash.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include "../../core/model.hpp"
#include "player.hpp"
namespace qs::service::mpris {
///! Provides access to MprisPlayers.
class MprisWatcher: public QObject {
Q_OBJECT;
public:
[[nodiscard]] ObjectModel<MprisPlayer>* players();
static MprisWatcher* instance();
private slots:
void onServiceRegistered(const QString& service);
void onServiceUnregistered(const QString& service);
void onPlayerReady();
void onPlayerDestroyed(QObject* object);
private:
explicit MprisWatcher();
void registerExisting();
void registerPlayer(const QString& address);
QDBusServiceWatcher serviceWatcher;
QHash<QString, MprisPlayer*> mPlayers;
ObjectModel<MprisPlayer> readyPlayers {this};
};
class MprisQml: public QObject {
Q_OBJECT;
QML_NAMED_ELEMENT(Mpris);
QML_SINGLETON;
/// All connected MPRIS players.
Q_PROPERTY(ObjectModel<MprisPlayer>* players READ players CONSTANT);
public:
explicit MprisQml(QObject* parent = nullptr): QObject(parent) {};
[[nodiscard]] ObjectModel<MprisPlayer>* players();
};
} // namespace qs::service::mpris

View File

@ -8,6 +8,7 @@
#include <qtypes.h>
#include <qvariant.h>
#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<PwNodeIface> Pipewire::nodes() {
return QQmlListProperty<PwNodeIface>(this, nullptr, &Pipewire::nodesCount, &Pipewire::nodeAt);
}
qsizetype Pipewire::nodesCount(QQmlListProperty<PwNodeIface>* property) {
return static_cast<Pipewire*>(property->object)->mNodes.count(); // NOLINT
}
PwNodeIface* Pipewire::nodeAt(QQmlListProperty<PwNodeIface>* property, qsizetype index) {
return static_cast<Pipewire*>(property->object)->mNodes.at(index); // NOLINT
}
ObjectModel<PwNodeIface>* 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<PwNodeIface*>(object); // NOLINT
this->mNodes.removeOne(iface);
emit this->nodesChanged();
this->mNodes.removeObject(iface);
}
QQmlListProperty<PwLinkIface> Pipewire::links() {
return QQmlListProperty<PwLinkIface>(this, nullptr, &Pipewire::linksCount, &Pipewire::linkAt);
}
qsizetype Pipewire::linksCount(QQmlListProperty<PwLinkIface>* property) {
return static_cast<Pipewire*>(property->object)->mLinks.count(); // NOLINT
}
PwLinkIface* Pipewire::linkAt(QQmlListProperty<PwLinkIface>* property, qsizetype index) {
return static_cast<Pipewire*>(property->object)->mLinks.at(index); // NOLINT
}
ObjectModel<PwLinkIface>* 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<PwLinkIface*>(object); // NOLINT
this->mLinks.removeOne(iface);
emit this->linksChanged();
this->mLinks.removeObject(iface);
}
QQmlListProperty<PwLinkGroupIface> Pipewire::linkGroups() {
return QQmlListProperty<PwLinkGroupIface>(
this,
nullptr,
&Pipewire::linkGroupsCount,
&Pipewire::linkGroupAt
);
}
qsizetype Pipewire::linkGroupsCount(QQmlListProperty<PwLinkGroupIface>* property) {
return static_cast<Pipewire*>(property->object)->mLinkGroups.count(); // NOLINT
}
PwLinkGroupIface*
Pipewire::linkGroupAt(QQmlListProperty<PwLinkGroupIface>* property, qsizetype index) {
return static_cast<Pipewire*>(property->object)->mLinkGroups.at(index); // NOLINT
}
ObjectModel<PwLinkGroupIface>* 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<PwLinkGroupIface*>(object); // NOLINT
this->mLinkGroups.removeOne(iface);
emit this->linkGroupsChanged();
this->mLinkGroups.removeObject(iface);
}
PwNodeIface* Pipewire::defaultAudioSink() const { // NOLINT

View File

@ -8,6 +8,7 @@
#include <qtmetamacros.h>
#include <qtypes.h>
#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<PwNodeIface> nodes READ nodes NOTIFY nodesChanged);
Q_PROPERTY(ObjectModel<PwNodeIface>* nodes READ nodes CONSTANT);
/// All pipewire links.
Q_PROPERTY(QQmlListProperty<PwLinkIface> links READ links NOTIFY linksChanged);
Q_PROPERTY(ObjectModel<PwLinkIface>* links READ links CONSTANT);
/// All pipewire link groups.
Q_PROPERTY(QQmlListProperty<PwLinkGroupIface> linkGroups READ linkGroups NOTIFY linkGroupsChanged);
Q_PROPERTY(ObjectModel<PwLinkGroupIface>* 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<PwNodeIface> nodes();
[[nodiscard]] QQmlListProperty<PwLinkIface> links();
[[nodiscard]] QQmlListProperty<PwLinkGroupIface> linkGroups();
[[nodiscard]] ObjectModel<PwNodeIface>* nodes();
[[nodiscard]] ObjectModel<PwLinkIface>* links();
[[nodiscard]] ObjectModel<PwLinkGroupIface>* 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<PwNodeIface>* property);
static PwNodeIface* nodeAt(QQmlListProperty<PwNodeIface>* property, qsizetype index);
static qsizetype linksCount(QQmlListProperty<PwLinkIface>* property);
static PwLinkIface* linkAt(QQmlListProperty<PwLinkIface>* property, qsizetype index);
static qsizetype linkGroupsCount(QQmlListProperty<PwLinkGroupIface>* property);
static PwLinkGroupIface*
linkGroupAt(QQmlListProperty<PwLinkGroupIface>* property, qsizetype index);
QVector<PwNodeIface*> mNodes;
QVector<PwLinkIface*> mLinks;
QVector<PwLinkGroupIface*> mLinkGroups;
ObjectModel<PwNodeIface> mNodes {this};
ObjectModel<PwLinkIface> mLinks {this};
ObjectModel<PwLinkGroupIface> mLinkGroups {this};
};
///! Tracks all link connections to a given node.

View File

@ -54,14 +54,14 @@ public:
dbus::DBusProperty<QString> status {this->properties, "Status"};
dbus::DBusProperty<QString> category {this->properties, "Category"};
dbus::DBusProperty<quint32> windowId {this->properties, "WindowId"};
dbus::DBusProperty<QString> iconThemePath {this->properties, "IconThemePath"};
dbus::DBusProperty<QString> iconName {this->properties, "IconName"};
dbus::DBusProperty<DBusSniIconPixmapList> iconPixmaps {this->properties, "IconPixmap"};
dbus::DBusProperty<QString> iconThemePath {this->properties, "IconThemePath", "", false};
dbus::DBusProperty<QString> iconName {this->properties, "IconName", "", false}; // IconPixmap may be set
dbus::DBusProperty<DBusSniIconPixmapList> iconPixmaps {this->properties, "IconPixmap", {}, false}; // IconName may be set
dbus::DBusProperty<QString> overlayIconName {this->properties, "OverlayIconName"};
dbus::DBusProperty<DBusSniIconPixmapList> overlayIconPixmaps {this->properties, "OverlayIconPixmap"};
dbus::DBusProperty<QString> attentionIconName {this->properties, "AttentionIconName"};
dbus::DBusProperty<DBusSniIconPixmapList> attentionIconPixmaps {this->properties, "AttentionIconPixmap"};
dbus::DBusProperty<QString> attentionMovieName {this->properties, "AttentionMovieName"};
dbus::DBusProperty<QString> attentionMovieName {this->properties, "AttentionMovieName", "", false};
dbus::DBusProperty<DBusSniTooltip> tooltip {this->properties, "ToolTip"};
dbus::DBusProperty<bool> isMenu {this->properties, "ItemIsMenu"};
dbus::DBusProperty<QDBusObjectPath> menuPath {this->properties, "Menu"};

View File

@ -9,6 +9,7 @@
#include <qtmetamacros.h>
#include <qtypes.h>
#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<SystemTrayItem> SystemTray::items() {
return QQmlListProperty<SystemTrayItem>(
this,
nullptr,
&SystemTray::itemsCount,
&SystemTray::itemAt
);
}
qsizetype SystemTray::itemsCount(QQmlListProperty<SystemTrayItem>* property) {
return reinterpret_cast<SystemTray*>(property->object)->mItems.count(); // NOLINT
}
SystemTrayItem* SystemTray::itemAt(QQmlListProperty<SystemTrayItem>* property, qsizetype index) {
return reinterpret_cast<SystemTray*>(property->object)->mItems.at(index); // NOLINT
}
ObjectModel<SystemTrayItem>* SystemTray::items() { return &this->mItems; }
SystemTrayItem* SystemTrayMenuWatcher::trayItem() const { return this->item; }

View File

@ -2,10 +2,9 @@
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#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<SystemTrayItem> items READ items NOTIFY itemsChanged);
Q_PROPERTY(ObjectModel<SystemTrayItem>* items READ items CONSTANT);
QML_ELEMENT;
QML_SINGLETON;
public:
explicit SystemTray(QObject* parent = nullptr);
[[nodiscard]] QQmlListProperty<SystemTrayItem> items();
signals:
void itemsChanged();
[[nodiscard]] ObjectModel<SystemTrayItem>* items();
private slots:
void onItemRegistered(qs::service::sni::StatusNotifierItem* item);
void onItemUnregistered(qs::service::sni::StatusNotifierItem* item);
private:
static qsizetype itemsCount(QQmlListProperty<SystemTrayItem>* property);
static SystemTrayItem* itemAt(QQmlListProperty<SystemTrayItem>* property, qsizetype index);
QList<SystemTrayItem*> mItems;
ObjectModel<SystemTrayItem> mItems {this};
};
///! Accessor for SystemTrayItem menus.

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -1,20 +0,0 @@
#include <qqml.h>
#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

View File

@ -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)

View File

@ -1,20 +0,0 @@
#include <qqml.h>
#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

View File

@ -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)

View File

@ -0,0 +1,561 @@
#include "connection.hpp"
#include <algorithm>
#include <functional>
#include <utility>
#include <qbytearrayview.h>
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qjsonarray.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qlocalsocket.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtenvironmentvariables.h>
#include <qtimer.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#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<void(bool, QByteArray)>& 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<HyprlandMonitor>* HyprlandIpc::monitors() { return &this->mMonitors; }
ObjectModel<HyprlandWorkspace>* HyprlandIpc::workspaces() { return &this->mWorkspaces; }
QVector<QByteArrayView> HyprlandIpc::parseEventArgs(QByteArrayView event, quint16 count) {
auto args = QVector<QByteArrayView>();
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<QString> HyprlandIpcEvent::parse(qint32 argumentCount) const {
auto args = QVector<QString>();
for (auto arg: this->parseView(argumentCount)) {
args.push_back(QString::fromUtf8(arg));
}
return args;
}
QVector<QByteArrayView> 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<QString>();
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<HyprlandWorkspace*>();
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<QString>();
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<HyprlandMonitor*>();
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

View File

@ -0,0 +1,124 @@
#pragma once
#include <functional>
#include <qbytearrayview.h>
#include <qcontainerfwd.h>
#include <qhash.h>
#include <qlocalsocket.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#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<QString> parse(qint32 argumentCount) const;
[[nodiscard]] QVector<QByteArrayView> 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<void(bool, QByteArray)>& callback);
void dispatch(const QString& request);
[[nodiscard]] HyprlandMonitor* monitorFor(QuickshellScreenInfo* screen);
[[nodiscard]] HyprlandMonitor* focusedMonitor() const;
void setFocusedMonitor(HyprlandMonitor* monitor);
[[nodiscard]] ObjectModel<HyprlandMonitor>* monitors();
[[nodiscard]] ObjectModel<HyprlandWorkspace>* 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<QByteArrayView> 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<HyprlandMonitor> mMonitors {this};
ObjectModel<HyprlandWorkspace> mWorkspaces {this};
HyprlandMonitor* mFocusedMonitor = nullptr;
//HyprlandWorkspace* activeWorkspace = nullptr;
HyprlandIpcEvent event {this};
};
} // namespace qs::hyprland::ipc

View File

@ -0,0 +1,136 @@
#include "monitor.hpp"
#include <utility>
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#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<qint32>();
auto name = object.value("name").value<QString>();
auto description = object.value("description").value<QString>();
auto x = object.value("x").value<qint32>();
auto y = object.value("y").value<qint32>();
auto width = object.value("width").value<qint32>();
auto height = object.value("height").value<qint32>();
auto scale = object.value("height").value<qint32>();
auto activeWorkspaceObj = object.value("activeWorkspace").value<QVariantMap>();
auto activeWorkspaceId = activeWorkspaceObj.value("id").value<qint32>();
auto activeWorkspaceName = activeWorkspaceObj.value("name").value<QString>();
auto focused = object.value("focused").value<bool>();
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

View File

@ -0,0 +1,85 @@
#pragma once
#include <qbytearrayview.h>
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#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

View File

@ -0,0 +1,52 @@
#include "qml.hpp"
#include <qobject.h>
#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<HyprlandMonitor>* HyprlandIpcQml::monitors() {
return HyprlandIpc::instance()->monitors();
}
ObjectModel<HyprlandWorkspace>* HyprlandIpcQml::workspaces() {
return HyprlandIpc::instance()->workspaces();
}
} // namespace qs::hyprland::ipc

View File

@ -0,0 +1,66 @@
#pragma once
#include <qbytearrayview.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#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<HyprlandMonitor>* monitors READ monitors CONSTANT);
/// All hyprland workspaces.
Q_PROPERTY(ObjectModel<HyprlandWorkspace>* 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<HyprlandMonitor>* monitors();
[[nodiscard]] static ObjectModel<HyprlandWorkspace>* 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

View File

@ -0,0 +1,79 @@
#include "workspace.hpp"
#include <utility>
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#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<qint32>();
auto name = object.value("name").value<QString>();
auto monitorId = object.value("monitorID").value<qint32>();
auto monitorName = object.value("monitor").value<QString>();
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

View File

@ -0,0 +1,59 @@
#pragma once
#include <qbytearrayview.h>
#include <qjsonobject.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#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

View File

@ -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",
]

View File

@ -1,5 +1,7 @@
#include <qguiapplication.h>
#include <qlogging.h>
#include <qqml.h>
#include <qtenvironmentvariables.h>
#include "../core/plugin.hpp"
@ -10,7 +12,19 @@
namespace {
class WaylandPlugin: public QuickshellPlugin {
bool applies() override { return QGuiApplication::platformName() == "wayland"; }
bool applies() override {
auto isWayland = QGuiApplication::platformName() == "wayland";
if (!isWayland && !qEnvironmentVariable("WAYLAND_DISPLAY").isEmpty()) {
qWarning() << "--- WARNING ---";
qWarning() << "WAYLAND_DISPLAY is present but QT_QPA_PLATFORM is"
<< QGuiApplication::platformName();
qWarning() << "If you are actually running wayland, set QT_QPA_PLATFORM to \"wayland\" or "
"most functionality will be broken.";
}
return isWayland;
}
void registerTypes() override {
#ifdef QS_WAYLAND_WLR_LAYERSHELL
@ -20,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,

View File

@ -4,5 +4,6 @@ headers = [
"wlr_layershell/window.hpp",
"wlr_layershell.hpp",
"session_lock.hpp",
"toplevel_management/qml.hpp",
]
-----

View File

@ -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)

View File

@ -0,0 +1,228 @@
#include "handle.hpp"
#include <cstddef>
#include <private/qwaylanddisplay_p.h>
#include <private/qwaylandinputdevice_p.h>
#include <private/qwaylandintegration_p.h>
#include <private/qwaylandscreen_p.h>
#include <private/qwaylandwindow_p.h>
#include <qcontainerfwd.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include <wayland-util.h>
#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<QScreen*> 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<QtWaylandClient::QWaylandScreen*>(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<QtWaylandClient::QWaylandWindow*>(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<QtWaylandClient::QWaylandWindow*>(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<QtWaylandClient::QWaylandWindow*>(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

View File

@ -0,0 +1,77 @@
#pragma once
#include <qobject.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include <qwayland-wlr-foreign-toplevel-management-unstable-v1.h>
#include <qwindow.h>
#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<QScreen*> 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<QScreen*> 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

View File

@ -0,0 +1,67 @@
#include "manager.hpp"
#include <qcontainerfwd.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qwaylandclientextension.h>
#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<ToplevelHandle*>& 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<ToplevelHandle*>(this->sender());
this->mReadyToplevels.push_back(handle);
emit this->toplevelReady(handle);
}
void ToplevelManager::onToplevelClosed() {
auto* handle = qobject_cast<ToplevelHandle*>(this->sender());
this->mReadyToplevels.removeOne(handle);
this->mToplevels.removeOne(handle);
}
} // namespace qs::wayland::toplevel_management::impl

View File

@ -0,0 +1,47 @@
#pragma once
#include <qcontainerfwd.h>
#include <qloggingcategory.h>
#include <qtmetamacros.h>
#include <qwayland-wlr-foreign-toplevel-management-unstable-v1.h>
#include <qwaylandclientextension.h>
#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<ToplevelManager>
, public QtWayland::zwlr_foreign_toplevel_manager_v1 {
Q_OBJECT;
public:
[[nodiscard]] bool available() const;
[[nodiscard]] const QVector<ToplevelHandle*>& 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<ToplevelHandle*> mToplevels;
QVector<ToplevelHandle*> mReadyToplevels;
};
} // namespace qs::wayland::toplevel_management::impl

View File

@ -0,0 +1,153 @@
#include "qml.hpp"
#include <qobject.h>
#include <qtmetamacros.h>
#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<ProxyWindowBase*>(window);
if (proxyWindow == nullptr) {
if (auto* iface = qobject_cast<WindowInterface*>(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<Toplevel>* 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<Toplevel*>(this->sender());
this->mToplevels.removeObject(toplevel);
}
ToplevelManager* ToplevelManager::instance() {
static auto* instance = new ToplevelManager(); // NOLINT
return instance;
}
ObjectModel<Toplevel>* ToplevelManagerQml::toplevels() {
return ToplevelManager::instance()->toplevels();
}
} // namespace qs::wayland::toplevel_management

View File

@ -0,0 +1,140 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#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<Toplevel>* toplevels();
static ToplevelManager* instance();
private slots:
void onToplevelReady(impl::ToplevelHandle* handle);
void onToplevelClosed();
private:
explicit ToplevelManager();
ObjectModel<Toplevel> 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<Toplevel>* toplevels READ toplevels CONSTANT);
QML_NAMED_ELEMENT(ToplevelManager);
QML_SINGLETON;
public:
explicit ToplevelManagerQml(QObject* parent = nullptr): QObject(parent) {}
[[nodiscard]] static ObjectModel<Toplevel>* toplevels();
};
} // namespace qs::wayland::toplevel_management

View File

@ -0,0 +1,270 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wlr_foreign_toplevel_management_unstable_v1">
<copyright>
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.
</copyright>
<interface name="zwlr_foreign_toplevel_manager_v1" version="3">
<description summary="list and control opened apps">
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
</description>
<event name="toplevel">
<description summary="a toplevel has been created">
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.
</description>
<arg name="toplevel" type="new_id" interface="zwlr_foreign_toplevel_handle_v1"/>
</event>
<request name="stop">
<description summary="stop sending events">
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.
</description>
</request>
<event name="finished" type="destructor">
<description summary="the compositor has finished with the toplevel manager">
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.
</description>
</event>
</interface>
<interface name="zwlr_foreign_toplevel_handle_v1" version="3">
<description summary="an opened toplevel">
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.
</description>
<event name="title">
<description summary="title change">
This event is emitted whenever the title of the toplevel changes.
</description>
<arg name="title" type="string"/>
</event>
<event name="app_id">
<description summary="app-id change">
This event is emitted whenever the app-id of the toplevel changes.
</description>
<arg name="app_id" type="string"/>
</event>
<event name="output_enter">
<description summary="toplevel entered an output">
This event is emitted whenever the toplevel becomes visible on
the given output. A toplevel may be visible on multiple outputs.
</description>
<arg name="output" type="object" interface="wl_output"/>
</event>
<event name="output_leave">
<description summary="toplevel left an output">
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.
</description>
<arg name="output" type="object" interface="wl_output"/>
</event>
<request name="set_maximized">
<description summary="requests that the toplevel be maximized">
Requests that the toplevel be maximized. If the maximized state actually
changes, this will be indicated by the state event.
</description>
</request>
<request name="unset_maximized">
<description summary="requests that the toplevel be unmaximized">
Requests that the toplevel be unmaximized. If the maximized state actually
changes, this will be indicated by the state event.
</description>
</request>
<request name="set_minimized">
<description summary="requests that the toplevel be minimized">
Requests that the toplevel be minimized. If the minimized state actually
changes, this will be indicated by the state event.
</description>
</request>
<request name="unset_minimized">
<description summary="requests that the toplevel be unminimized">
Requests that the toplevel be unminimized. If the minimized state actually
changes, this will be indicated by the state event.
</description>
</request>
<request name="activate">
<description summary="activate the toplevel">
Request that this toplevel be activated on the given seat.
There is no guarantee the toplevel will be actually activated.
</description>
<arg name="seat" type="object" interface="wl_seat"/>
</request>
<enum name="state">
<description summary="types of states on the toplevel">
The different states that a toplevel can have. These have the same meaning
as the states with the same names defined in xdg-toplevel
</description>
<entry name="maximized" value="0" summary="the toplevel is maximized"/>
<entry name="minimized" value="1" summary="the toplevel is minimized"/>
<entry name="activated" value="2" summary="the toplevel is active"/>
<entry name="fullscreen" value="3" summary="the toplevel is fullscreen" since="2"/>
</enum>
<event name="state">
<description summary="the toplevel state changed">
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.
</description>
<arg name="state" type="array"/>
</event>
<event name="done">
<description summary="all information about the toplevel has been sent">
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.
</description>
</event>
<request name="close">
<description summary="request that the toplevel be closed">
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.
</description>
</request>
<request name="set_rectangle">
<description summary="the rectangle which represents the toplevel">
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.
</description>
<arg name="surface" type="object" interface="wl_surface"/>
<arg name="x" type="int"/>
<arg name="y" type="int"/>
<arg name="width" type="int"/>
<arg name="height" type="int"/>
</request>
<enum name="error">
<entry name="invalid_rectangle" value="0"
summary="the provided rectangle is invalid"/>
</enum>
<event name="closed">
<description summary="this toplevel has been destroyed">
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.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="destroy the zwlr_foreign_toplevel_handle_v1 object">
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.
</description>
</request>
<!-- Version 2 additions -->
<request name="set_fullscreen" since="2">
<description summary="request that the toplevel be fullscreened">
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.
</description>
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
</request>
<request name="unset_fullscreen" since="2">
<description summary="request that the toplevel be unfullscreened">
Requests that the toplevel be unfullscreened. If the fullscreen state
actually changes, this will be indicated by the state event.
</description>
</request>
<!-- Version 3 additions -->
<event name="parent" since="3">
<description summary="parent change">
This event is emitted whenever the parent of the toplevel changes.
No event is emitted when the parent handle is destroyed by the client.
</description>
<arg name="parent" type="object" interface="zwlr_foreign_toplevel_handle_v1" allow-null="true"/>
</event>
</interface>
</protocol>

View File

@ -114,6 +114,18 @@ void WlrLayershell::setAnchors(Anchors anchors) {
if (!anchors.verticalConstraint()) this->ProxyWindowBase::setHeight(this->mHeight);
}
bool WlrLayershell::aboveWindows() const { return this->layer() > WlrLayer::Bottom; }
void WlrLayershell::setAboveWindows(bool aboveWindows) {
this->setLayer(aboveWindows ? WlrLayer::Top : WlrLayer::Bottom);
}
bool WlrLayershell::focusable() const { return this->keyboardFocus() != WlrKeyboardFocus::None; }
void WlrLayershell::setFocusable(bool focusable) {
this->setKeyboardFocus(focusable ? WlrKeyboardFocus::OnDemand : WlrKeyboardFocus::None);
}
QString WlrLayershell::ns() const { return this->ext->ns(); }
void WlrLayershell::setNamespace(QString ns) {
@ -190,6 +202,8 @@ WaylandPanelInterface::WaylandPanelInterface(QObject* parent)
QObject::connect(this->layer, &WlrLayershell::marginsChanged, this, &WaylandPanelInterface::marginsChanged);
QObject::connect(this->layer, &WlrLayershell::exclusiveZoneChanged, this, &WaylandPanelInterface::exclusiveZoneChanged);
QObject::connect(this->layer, &WlrLayershell::exclusionModeChanged, this, &WaylandPanelInterface::exclusionModeChanged);
QObject::connect(this->layer, &WlrLayershell::layerChanged, this, &WaylandPanelInterface::aboveWindowsChanged);
QObject::connect(this->layer, &WlrLayershell::keyboardFocusChanged, this, &WaylandPanelInterface::focusableChanged);
// clang-format on
}
@ -224,6 +238,8 @@ proxyPair(Anchors, anchors, setAnchors);
proxyPair(Margins, margins, setMargins);
proxyPair(qint32, exclusiveZone, setExclusiveZone);
proxyPair(ExclusionMode::Enum, exclusionMode, setExclusionMode);
proxyPair(bool, focusable, setFocusable);
proxyPair(bool, aboveWindows, setAboveWindows);
#undef proxyPair
// NOLINTEND

View File

@ -8,6 +8,7 @@
#include <qtypes.h>
#include "../core/doc.hpp"
#include "../core/panelinterface.hpp"
#include "../core/proxywindow.hpp"
#include "wlr_layershell/window.hpp"
@ -54,6 +55,8 @@ class WlrLayershell: public ProxyWindowBase {
QSDOC_HIDE Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged);
QSDOC_HIDE Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged);
QSDOC_HIDE Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged);
QSDOC_HIDE Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY layerChanged);
QSDOC_HIDE Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY keyboardFocusChanged);
QML_ATTACHED(WlrLayershell);
QML_ELEMENT;
// clang-format on
@ -92,6 +95,12 @@ public:
[[nodiscard]] Margins margins() const;
void setMargins(Margins margins); // NOLINT
[[nodiscard]] bool aboveWindows() const;
void setAboveWindows(bool aboveWindows);
[[nodiscard]] bool focusable() const;
void setFocusable(bool focusable);
static WlrLayershell* qmlAttachedProperties(QObject* object);
signals:
@ -161,6 +170,12 @@ public:
[[nodiscard]] ExclusionMode::Enum exclusionMode() const override;
void setExclusionMode(ExclusionMode::Enum exclusionMode) override;
[[nodiscard]] bool aboveWindows() const override;
void setAboveWindows(bool aboveWindows) override;
[[nodiscard]] bool focusable() const override;
void setFocusable(bool focusable) override;
// NOLINTEND
private:

View File

@ -7,7 +7,6 @@
#include <private/qwaylandsurface_p.h>
#include <private/qwaylandwindow_p.h>
#include <qlogging.h>
#include <qpoint.h>
#include <qrect.h>
#include <qsize.h>
#include <qtversionchecks.h>
@ -18,6 +17,10 @@
#include "shell_integration.hpp"
#include "window.hpp"
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
#include <qpoint.h>
#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());
}

View File

@ -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");

View File

@ -2,6 +2,7 @@
#include <qobject.h>
#include <qscreen.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwindow.h>
@ -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);

22
src/x11/CMakeLists.txt Normal file
View File

@ -0,0 +1,22 @@
find_package(XCB REQUIRED COMPONENTS XCB)
qt_add_library(quickshell-x11 STATIC
util.cpp
panel_window.cpp
)
qt_add_qml_module(quickshell-x11
URI Quickshell.X11
VERSION 0.1
)
add_library(quickshell-x11-init OBJECT init.cpp)
target_link_libraries(quickshell-x11 PRIVATE ${QT_DEPS} ${XCB_LIBRARIES})
target_link_libraries(quickshell-x11-init PRIVATE ${QT_DEPS} ${XCB_LIBRARIES})
qs_pch(quickshell-x11)
qs_pch(quickshell-x11plugin)
qs_pch(quickshell-x11-init)
target_link_libraries(quickshell PRIVATE quickshell-x11plugin quickshell-x11-init)

29
src/x11/init.cpp Normal file
View File

@ -0,0 +1,29 @@
#include <qguiapplication.h>
#include <qqml.h>
#include "../core/plugin.hpp"
#include "panel_window.hpp"
#include "util.hpp"
namespace {
class X11Plugin: public QuickshellPlugin {
bool applies() override { return QGuiApplication::platformName() == "xcb"; }
void init() override { XAtom::initAtoms(); }
void registerTypes() override {
qmlRegisterType<XPanelInterface>("Quickshell._X11Overlay", 1, 0, "PanelWindow");
qmlRegisterModuleImport(
"Quickshell",
QQmlModuleImportModuleAny,
"Quickshell._X11Overlay",
QQmlModuleImportLatest
);
}
};
QS_REGISTER_PLUGIN(X11Plugin);
} // namespace

431
src/x11/panel_window.cpp Normal file
View File

@ -0,0 +1,431 @@
#include "panel_window.hpp"
#include <array>
#include <map>
#include <qevent.h>
#include <qlist.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmlengine.h>
#include <qqmllist.h>
#include <qquickwindow.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <xcb/xproto.h>
#include "../core/generation.hpp"
#include "../core/panelinterface.hpp"
#include "../core/proxywindow.hpp"
#include "util.hpp"
class XPanelStack {
public:
static XPanelStack* instance() {
static XPanelStack* stack = nullptr; // NOLINT
if (stack == nullptr) {
stack = new XPanelStack();
}
return stack;
}
[[nodiscard]] const QList<XPanelWindow*>& panels(XPanelWindow* panel) {
return this->mPanels[EngineGeneration::findObjectGeneration(panel)];
}
void addPanel(XPanelWindow* panel) {
auto& panels = this->mPanels[EngineGeneration::findObjectGeneration(panel)];
if (!panels.contains(panel)) {
panels.push_back(panel);
}
}
void removePanel(XPanelWindow* panel) {
auto& panels = this->mPanels[EngineGeneration::findObjectGeneration(panel)];
if (panels.removeOne(panel)) {
if (panels.isEmpty()) {
this->mPanels.erase(EngineGeneration::findObjectGeneration(panel));
}
// from the bottom up, update all panels
for (auto* panel: panels) {
panel->updateDimensions();
}
}
}
private:
std::map<EngineGeneration*, QList<XPanelWindow*>> mPanels;
};
bool XPanelEventFilter::eventFilter(QObject* watched, QEvent* event) {
if (event->type() == QEvent::PlatformSurface) {
auto* surfaceEvent = static_cast<QPlatformSurfaceEvent*>(event); // NOLINT
if (surfaceEvent->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) {
emit this->surfaceCreated();
}
}
return this->QObject::eventFilter(watched, event);
}
XPanelWindow::XPanelWindow(QObject* parent): ProxyWindowBase(parent) {
QObject::connect(
&this->eventFilter,
&XPanelEventFilter::surfaceCreated,
this,
&XPanelWindow::xInit
);
}
XPanelWindow::~XPanelWindow() { XPanelStack::instance()->removePanel(this); }
void XPanelWindow::connectWindow() {
this->ProxyWindowBase::connectWindow();
this->window->installEventFilter(&this->eventFilter);
this->connectScreen();
// clang-format off
QObject::connect(this->window, &QQuickWindow::screenChanged, this, &XPanelWindow::connectScreen);
QObject::connect(this->window, &QQuickWindow::visibleChanged, this, &XPanelWindow::updatePanelStack);
// clang-format on
// qt overwrites _NET_WM_STATE, so we have to use the qt api
// QXcbWindow::WindowType::Dock in qplatformwindow_p.h
// see QXcbWindow::setWindowFlags in qxcbwindow.cpp
this->window->setProperty("_q_xcb_wm_window_type", 0x000004);
// at least one flag needs to change for the above property to apply
this->window->setFlag(Qt::FramelessWindowHint);
this->updateAboveWindows();
this->updateFocusable();
if (this->window->handle() != nullptr) {
this->xInit();
this->updatePanelStack();
}
}
void XPanelWindow::setWidth(qint32 width) {
this->mWidth = width;
// only update the actual size if not blocked by anchors
if (!this->mAnchors.horizontalConstraint()) {
this->ProxyWindowBase::setWidth(width);
this->updateDimensions();
}
}
void XPanelWindow::setHeight(qint32 height) {
this->mHeight = height;
// only update the actual size if not blocked by anchors
if (!this->mAnchors.verticalConstraint()) {
this->ProxyWindowBase::setHeight(height);
this->updateDimensions();
}
}
Anchors XPanelWindow::anchors() const { return this->mAnchors; }
void XPanelWindow::setAnchors(Anchors anchors) {
if (this->mAnchors == anchors) return;
this->mAnchors = anchors;
this->updateDimensions();
emit this->anchorsChanged();
}
qint32 XPanelWindow::exclusiveZone() const { return this->mExclusiveZone; }
void XPanelWindow::setExclusiveZone(qint32 exclusiveZone) {
if (this->mExclusiveZone == exclusiveZone) return;
this->mExclusiveZone = exclusiveZone;
const bool wasNormal = this->mExclusionMode == ExclusionMode::Normal;
this->setExclusionMode(ExclusionMode::Normal);
if (wasNormal) this->updateStrut();
emit this->exclusiveZoneChanged();
}
ExclusionMode::Enum XPanelWindow::exclusionMode() const { return this->mExclusionMode; }
void XPanelWindow::setExclusionMode(ExclusionMode::Enum exclusionMode) {
if (this->mExclusionMode == exclusionMode) return;
this->mExclusionMode = exclusionMode;
this->updateStrut();
emit this->exclusionModeChanged();
}
Margins XPanelWindow::margins() const { return this->mMargins; }
void XPanelWindow::setMargins(Margins margins) {
if (this->mMargins == margins) return;
this->mMargins = margins;
this->updateDimensions();
emit this->marginsChanged();
}
bool XPanelWindow::aboveWindows() const { return this->mAboveWindows; }
void XPanelWindow::setAboveWindows(bool aboveWindows) {
if (this->mAboveWindows == aboveWindows) return;
this->mAboveWindows = aboveWindows;
this->updateAboveWindows();
emit this->aboveWindowsChanged();
}
bool XPanelWindow::focusable() const { return this->mFocusable; }
void XPanelWindow::setFocusable(bool focusable) {
if (this->mFocusable == focusable) return;
this->mFocusable = focusable;
this->updateFocusable();
emit this->focusableChanged();
}
void XPanelWindow::xInit() { this->updateDimensions(); }
void XPanelWindow::connectScreen() {
if (this->mTrackedScreen != nullptr) {
QObject::disconnect(this->mTrackedScreen, nullptr, this, nullptr);
}
this->mTrackedScreen = this->window->screen();
if (this->mTrackedScreen != nullptr) {
QObject::connect(
this->mTrackedScreen,
&QScreen::geometryChanged,
this,
&XPanelWindow::updateDimensions
);
}
}
void XPanelWindow::updateDimensions() {
if (this->window == nullptr || this->window->handle() == nullptr) return;
auto screenGeometry = this->window->screen()->virtualGeometry();
if (this->mExclusionMode != ExclusionMode::Ignore) {
for (auto* panel: XPanelStack::instance()->panels(this)) {
// we only care about windows below us
if (panel == this) break;
int side = -1;
quint32 exclusiveZone = 0;
panel->getExclusion(side, exclusiveZone);
if (exclusiveZone == 0) continue;
auto zone = static_cast<qint32>(exclusiveZone);
screenGeometry.adjust(
side == 0 ? zone : 0,
side == 2 ? zone : 0,
side == 1 ? -zone : 0,
side == 3 ? -zone : 0
);
}
}
auto geometry = QRect();
if (this->mAnchors.horizontalConstraint()) {
geometry.setX(screenGeometry.x() + this->mMargins.mLeft);
geometry.setWidth(screenGeometry.width() - this->mMargins.mLeft - this->mMargins.mRight);
} else {
if (this->mAnchors.mLeft) {
geometry.setX(screenGeometry.x() + this->mMargins.mLeft);
} else if (this->mAnchors.mRight) {
geometry.setX(
screenGeometry.x() + screenGeometry.width() - this->mWidth - this->mMargins.mRight
);
} else {
geometry.setX(screenGeometry.x() + screenGeometry.width() / 2 - this->mWidth / 2);
}
geometry.setWidth(this->mWidth);
}
if (this->mAnchors.verticalConstraint()) {
geometry.setY(screenGeometry.y() + this->mMargins.mTop);
geometry.setHeight(screenGeometry.height() - this->mMargins.mTop - this->mMargins.mBottom);
} else {
if (this->mAnchors.mTop) {
geometry.setY(screenGeometry.y() + this->mMargins.mTop);
} else if (this->mAnchors.mBottom) {
geometry.setY(
screenGeometry.y() + screenGeometry.height() - this->mHeight - this->mMargins.mBottom
);
} else {
geometry.setY(screenGeometry.y() + screenGeometry.height() / 2 - this->mHeight / 2);
}
geometry.setHeight(this->mHeight);
}
this->window->setGeometry(geometry);
this->updateStrut();
}
void XPanelWindow::updatePanelStack() {
if (this->window->isVisible()) {
XPanelStack::instance()->addPanel(this);
} else {
XPanelStack::instance()->removePanel(this);
}
}
void XPanelWindow::getExclusion(int& side, quint32& exclusiveZone) {
if (this->mExclusionMode == ExclusionMode::Ignore) return;
auto& anchors = this->mAnchors;
if (anchors.mLeft || anchors.mRight || anchors.mTop || anchors.mBottom) {
if (!anchors.horizontalConstraint()
&& (anchors.verticalConstraint() || (!anchors.mTop && !anchors.mBottom)))
{
side = anchors.mLeft ? 0 : anchors.mRight ? 1 : -1;
} else if (!anchors.verticalConstraint()
&& (anchors.horizontalConstraint() || (!anchors.mLeft && !anchors.mRight)))
{
side = anchors.mTop ? 2 : anchors.mBottom ? 3 : -1;
}
}
if (side == -1) return;
auto autoExclude = this->mExclusionMode == ExclusionMode::Auto;
if (autoExclude) {
if (side == 0 || side == 1) {
exclusiveZone = this->mWidth + (side == 0 ? this->mMargins.mLeft : this->mMargins.mRight);
} else {
exclusiveZone = this->mHeight + (side == 2 ? this->mMargins.mTop : this->mMargins.mBottom);
}
} else {
exclusiveZone = this->mExclusiveZone;
}
}
void XPanelWindow::updateStrut() {
if (this->window == nullptr || this->window->handle() == nullptr) return;
auto* conn = x11Connection();
int side = -1;
quint32 exclusiveZone = 0;
this->getExclusion(side, exclusiveZone);
if (side == -1 || this->mExclusionMode == ExclusionMode::Ignore) {
xcb_delete_property(conn, this->window->winId(), XAtom::_NET_WM_STRUT.atom());
xcb_delete_property(conn, this->window->winId(), XAtom::_NET_WM_STRUT_PARTIAL.atom());
return;
}
auto data = std::array<quint32, 12>();
data[side] = exclusiveZone;
// https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm45573693101552
// assuming "specified in root window coordinates" means relative to the window geometry
// in which case only the end position should be set, to the opposite extent.
data[side * 2 + 5] = side == 0 || side == 1 ? this->window->height() : this->window->width();
xcb_change_property(
conn,
XCB_PROP_MODE_REPLACE,
this->window->winId(),
XAtom::_NET_WM_STRUT.atom(),
XCB_ATOM_CARDINAL,
32,
4,
data.data()
);
xcb_change_property(
conn,
XCB_PROP_MODE_REPLACE,
this->window->winId(),
XAtom::_NET_WM_STRUT_PARTIAL.atom(),
XCB_ATOM_CARDINAL,
32,
12,
data.data()
);
}
void XPanelWindow::updateAboveWindows() {
if (this->window == nullptr) return;
this->window->setFlag(Qt::WindowStaysOnBottomHint, !this->mAboveWindows);
this->window->setFlag(Qt::WindowStaysOnTopHint, this->mAboveWindows);
}
void XPanelWindow::updateFocusable() {
if (this->window == nullptr) return;
this->window->setFlag(Qt::WindowDoesNotAcceptFocus, !this->mFocusable);
}
// XPanelInterface
XPanelInterface::XPanelInterface(QObject* parent)
: PanelWindowInterface(parent)
, panel(new XPanelWindow(this)) {
// clang-format off
QObject::connect(this->panel, &ProxyWindowBase::windowConnected, this, &XPanelInterface::windowConnected);
QObject::connect(this->panel, &ProxyWindowBase::visibleChanged, this, &XPanelInterface::visibleChanged);
QObject::connect(this->panel, &ProxyWindowBase::backerVisibilityChanged, this, &XPanelInterface::backingWindowVisibleChanged);
QObject::connect(this->panel, &ProxyWindowBase::heightChanged, this, &XPanelInterface::heightChanged);
QObject::connect(this->panel, &ProxyWindowBase::widthChanged, this, &XPanelInterface::widthChanged);
QObject::connect(this->panel, &ProxyWindowBase::screenChanged, this, &XPanelInterface::screenChanged);
QObject::connect(this->panel, &ProxyWindowBase::windowTransformChanged, this, &XPanelInterface::windowTransformChanged);
QObject::connect(this->panel, &ProxyWindowBase::colorChanged, this, &XPanelInterface::colorChanged);
QObject::connect(this->panel, &ProxyWindowBase::maskChanged, this, &XPanelInterface::maskChanged);
// panel specific
QObject::connect(this->panel, &XPanelWindow::anchorsChanged, this, &XPanelInterface::anchorsChanged);
QObject::connect(this->panel, &XPanelWindow::marginsChanged, this, &XPanelInterface::marginsChanged);
QObject::connect(this->panel, &XPanelWindow::exclusiveZoneChanged, this, &XPanelInterface::exclusiveZoneChanged);
QObject::connect(this->panel, &XPanelWindow::exclusionModeChanged, this, &XPanelInterface::exclusionModeChanged);
QObject::connect(this->panel, &XPanelWindow::aboveWindowsChanged, this, &XPanelInterface::aboveWindowsChanged);
QObject::connect(this->panel, &XPanelWindow::focusableChanged, this, &XPanelInterface::focusableChanged);
// clang-format on
}
void XPanelInterface::onReload(QObject* oldInstance) {
QQmlEngine::setContextForObject(this->panel, QQmlEngine::contextForObject(this));
auto* old = qobject_cast<XPanelInterface*>(oldInstance);
this->panel->reload(old != nullptr ? old->panel : nullptr);
}
QQmlListProperty<QObject> XPanelInterface::data() { return this->panel->data(); }
ProxyWindowBase* XPanelInterface::proxyWindow() const { return this->panel; }
QQuickItem* XPanelInterface::contentItem() const { return this->panel->contentItem(); }
bool XPanelInterface::isBackingWindowVisible() const { return this->panel->isVisibleDirect(); }
// NOLINTBEGIN
#define proxyPair(type, get, set) \
type XPanelInterface::get() const { return this->panel->get(); } \
void XPanelInterface::set(type value) { this->panel->set(value); }
proxyPair(bool, isVisible, setVisible);
proxyPair(qint32, width, setWidth);
proxyPair(qint32, height, setHeight);
proxyPair(QuickshellScreenInfo*, screen, setScreen);
proxyPair(QColor, color, setColor);
proxyPair(PendingRegion*, mask, setMask);
// panel specific
proxyPair(Anchors, anchors, setAnchors);
proxyPair(Margins, margins, setMargins);
proxyPair(qint32, exclusiveZone, setExclusiveZone);
proxyPair(ExclusionMode::Enum, exclusionMode, setExclusionMode);
proxyPair(bool, focusable, setFocusable);
proxyPair(bool, aboveWindows, setAboveWindows);
#undef proxyPair
// NOLINTEND

160
src/x11/panel_window.hpp Normal file
View File

@ -0,0 +1,160 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qquickwindow.h>
#include <qscreen.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include "../core/doc.hpp"
#include "../core/panelinterface.hpp"
#include "../core/proxywindow.hpp"
class XPanelStack;
class XPanelEventFilter: public QObject {
Q_OBJECT;
public:
explicit XPanelEventFilter(QObject* parent = nullptr): QObject(parent) {}
signals:
void surfaceCreated();
protected:
bool eventFilter(QObject* watched, QEvent* event) override;
};
class XPanelWindow: public ProxyWindowBase {
QSDOC_BASECLASS(PanelWindowInterface);
Q_OBJECT;
// clang-format off
QSDOC_HIDE Q_PROPERTY(Anchors anchors READ anchors WRITE setAnchors NOTIFY anchorsChanged);
QSDOC_HIDE Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged);
QSDOC_HIDE Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged);
QSDOC_HIDE Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged);
QSDOC_HIDE Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged);
QSDOC_HIDE Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged);
// clang-format on
QML_ELEMENT;
public:
explicit XPanelWindow(QObject* parent = nullptr);
~XPanelWindow() override;
Q_DISABLE_COPY_MOVE(XPanelWindow);
void connectWindow() override;
void setWidth(qint32 width) override;
void setHeight(qint32 height) override;
[[nodiscard]] Anchors anchors() const;
void setAnchors(Anchors anchors);
[[nodiscard]] qint32 exclusiveZone() const;
void setExclusiveZone(qint32 exclusiveZone);
[[nodiscard]] ExclusionMode::Enum exclusionMode() const;
void setExclusionMode(ExclusionMode::Enum exclusionMode);
[[nodiscard]] Margins margins() const;
void setMargins(Margins margins);
[[nodiscard]] bool aboveWindows() const;
void setAboveWindows(bool aboveWindows);
[[nodiscard]] bool focusable() const;
void setFocusable(bool focusable);
signals:
QSDOC_HIDE void anchorsChanged();
QSDOC_HIDE void exclusiveZoneChanged();
QSDOC_HIDE void exclusionModeChanged();
QSDOC_HIDE void marginsChanged();
QSDOC_HIDE void aboveWindowsChanged();
QSDOC_HIDE void focusableChanged();
private slots:
void xInit();
void connectScreen();
void updateDimensions();
void updatePanelStack();
private:
void getExclusion(int& side, quint32& exclusiveZone);
void updateStrut();
void updateAboveWindows();
void updateFocusable();
QPointer<QScreen> mTrackedScreen = nullptr;
bool mAboveWindows = true;
bool mFocusable = false;
Anchors mAnchors;
Margins mMargins;
qint32 mExclusiveZone = 0;
ExclusionMode::Enum mExclusionMode = ExclusionMode::Auto;
XPanelEventFilter eventFilter;
friend class XPanelStack;
};
class XPanelInterface: public PanelWindowInterface {
Q_OBJECT;
public:
explicit XPanelInterface(QObject* parent = nullptr);
void onReload(QObject* oldInstance) override;
[[nodiscard]] ProxyWindowBase* proxyWindow() const override;
[[nodiscard]] QQuickItem* contentItem() const override;
// NOLINTBEGIN
[[nodiscard]] bool isVisible() const override;
[[nodiscard]] bool isBackingWindowVisible() const override;
void setVisible(bool visible) override;
[[nodiscard]] qint32 width() const override;
void setWidth(qint32 width) override;
[[nodiscard]] qint32 height() const override;
void setHeight(qint32 height) override;
[[nodiscard]] QuickshellScreenInfo* screen() const override;
void setScreen(QuickshellScreenInfo* screen) override;
[[nodiscard]] QColor color() const override;
void setColor(QColor color) override;
[[nodiscard]] PendingRegion* mask() const override;
void setMask(PendingRegion* mask) override;
[[nodiscard]] QQmlListProperty<QObject> data() override;
// panel specific
[[nodiscard]] Anchors anchors() const override;
void setAnchors(Anchors anchors) override;
[[nodiscard]] Margins margins() const override;
void setMargins(Margins margins) override;
[[nodiscard]] qint32 exclusiveZone() const override;
void setExclusiveZone(qint32 exclusiveZone) override;
[[nodiscard]] ExclusionMode::Enum exclusionMode() const override;
void setExclusionMode(ExclusionMode::Enum exclusionMode) override;
[[nodiscard]] bool aboveWindows() const override;
void setAboveWindows(bool aboveWindows) override;
[[nodiscard]] bool focusable() const override;
void setFocusable(bool focusable) override;
// NOLINTEND
private:
XPanelWindow* panel;
friend class WlrLayershell;
};

55
src/x11/util.cpp Normal file
View File

@ -0,0 +1,55 @@
#include "util.hpp"
#include <qbytearray.h>
#include <qguiapplication.h>
#include <qguiapplication_platform.h>
#include <xcb/xcb.h>
#include <xcb/xproto.h>
xcb_connection_t* x11Connection() {
static xcb_connection_t* conn = nullptr; // NOLINT
if (conn == nullptr) {
if (auto* x11Application = dynamic_cast<QGuiApplication*>(QGuiApplication::instance())
->nativeInterface<QNativeInterface::QX11Application>())
{
conn = x11Application->connection();
}
}
return conn;
}
// NOLINTBEGIN
XAtom XAtom::_NET_WM_STRUT {};
XAtom XAtom::_NET_WM_STRUT_PARTIAL {};
// NOLINTEND
void XAtom::initAtoms() {
_NET_WM_STRUT.init("_NET_WM_STRUT");
_NET_WM_STRUT_PARTIAL.init("_NET_WM_STRUT_PARTIAL");
}
void XAtom::init(const QByteArray& name) {
this->cookie = xcb_intern_atom(x11Connection(), 0, name.length(), name.data());
}
bool XAtom::isValid() {
this->resolve();
return this->mAtom != XCB_ATOM_NONE;
}
const xcb_atom_t& XAtom::atom() {
this->resolve();
return this->mAtom;
}
void XAtom::resolve() {
if (!this->resolved) {
this->resolved = true;
auto* reply = xcb_intern_atom_reply(x11Connection(), this->cookie, nullptr);
if (reply != nullptr) this->mAtom = reply->atom;
free(reply); // NOLINT
}
}

29
src/x11/util.hpp Normal file
View File

@ -0,0 +1,29 @@
#pragma once
#include <qbytearray.h>
#include <qtclasshelpermacros.h>
#include <xcb/xcb.h>
#include <xcb/xproto.h>
xcb_connection_t* x11Connection();
class XAtom {
public:
[[nodiscard]] bool isValid();
[[nodiscard]] const xcb_atom_t& atom();
// NOLINTBEGIN
static XAtom _NET_WM_STRUT;
static XAtom _NET_WM_STRUT_PARTIAL;
// NOLINTEND
static void initAtoms();
private:
void init(const QByteArray& name);
void resolve();
bool resolved = false;
xcb_atom_t mAtom = XCB_ATOM_NONE;
xcb_intern_atom_cookie_t cookie {};
};