forked from quickshell/quickshell
Compare commits
35 commits
Author | SHA1 | Date | |
---|---|---|---|
outfoxxed | 67783ec24c | ||
outfoxxed | b5b9c1f6c3 | ||
outfoxxed | 5d1def3e49 | ||
outfoxxed | bc349998df | ||
outfoxxed | ef1a4134f0 | ||
outfoxxed | d14ca70984 | ||
outfoxxed | be237b6ab5 | ||
outfoxxed | 37fecfc990 | ||
outfoxxed | b1f5a5eb94 | ||
outfoxxed | 9d5dd402b9 | ||
outfoxxed | 29f02d837d | ||
outfoxxed | 7d20b472dd | ||
outfoxxed | bd504daf56 | ||
outfoxxed | 238ca8cf0b | ||
outfoxxed | a8506edbb9 | ||
outfoxxed | d56c07ceb3 | ||
outfoxxed | 84bb4098ad | ||
outfoxxed | 6c9526761c | ||
outfoxxed | 7feae55ebe | ||
outfoxxed | 569c40494d | ||
outfoxxed | 0519acf1d6 | ||
outfoxxed | 33fac67798 | ||
outfoxxed | 7ad3671dd1 | ||
outfoxxed | 4e92d82992 | ||
outfoxxed | 5a84e73442 | ||
outfoxxed | 06240ccf80 | ||
outfoxxed | 5016dbf0d4 | ||
outfoxxed | 6326f60ce2 | ||
outfoxxed | ac339cb23b | ||
outfoxxed | f2df3da596 | ||
outfoxxed | ed3708f5cb | ||
outfoxxed | af45502913 | ||
outfoxxed | 4ee9ac7f7c | ||
kossLAN | 3b6d1c3bd8 | ||
outfoxxed | 73cfeba61b |
|
@ -5,6 +5,7 @@ Checks: >
|
|||
-*,
|
||||
bugprone-*,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-bugprone-forward-declararion-namespace,
|
||||
concurrency-*,
|
||||
cppcoreguidelines-*,
|
||||
-cppcoreguidelines-owning-memory,
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,3 +1,7 @@
|
|||
# related repos
|
||||
/docs
|
||||
/examples
|
||||
|
||||
# build files
|
||||
/result
|
||||
/build/
|
||||
|
|
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -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
165
BUILD.md
Normal 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
|
||||
```
|
|
@ -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
99
CONTRIBUTING.md
Normal 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.
|
99
README.md
99
README.md
|
@ -11,22 +11,9 @@ Hosted on: [outfoxxed's gitea], [github]
|
|||
Documentation available at [quickshell.outfoxxed.me](https://quickshell.outfoxxed.me) or
|
||||
can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quickshell-docs) repo.
|
||||
|
||||
Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples)
|
||||
Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples)
|
||||
repo.
|
||||
|
||||
Both the documentation and examples are included as submodules with revisions that work with the current
|
||||
version of quickshell.
|
||||
|
||||
You can clone everything with
|
||||
```
|
||||
$ git clone --recursive https://git.outfoxxed.me/outfoxxed/quickshell.git
|
||||
```
|
||||
|
||||
Or clone missing submodules later with
|
||||
```
|
||||
$ git submodule update --init --recursive
|
||||
```
|
||||
|
||||
# Installation
|
||||
|
||||
## Nix
|
||||
|
@ -48,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
|
||||
|
||||
|
|
35
default.nix
35
default.nix
|
@ -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
docs
|
@ -1 +0,0 @@
|
|||
Subproject commit ff5da84a8b258a9b2caaf978ddb6de23635d8903
|
1
examples
1
examples
|
@ -1 +0,0 @@
|
|||
Subproject commit b9e744b50673304dfddb68f3da2a2e906d028b96
|
|
@ -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 {
|
||||
|
|
|
@ -11,6 +11,10 @@ endif()
|
|||
|
||||
if (WAYLAND)
|
||||
add_subdirectory(wayland)
|
||||
endif ()
|
||||
endif()
|
||||
|
||||
if (X11)
|
||||
add_subdirectory(x11)
|
||||
endif()
|
||||
|
||||
add_subdirectory(services)
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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
22
src/core/elapsedtimer.cpp
Normal 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
45
src/core/elapsedtimer.hpp
Normal 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;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
74
src/core/model.cpp
Normal 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
94
src/core/model.hpp
Normal 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); }
|
||||
};
|
|
@ -18,5 +18,7 @@ headers = [
|
|||
"easingcurve.hpp",
|
||||
"transformwatcher.hpp",
|
||||
"boundcomponent.hpp",
|
||||
"model.hpp",
|
||||
"elapsedtimer.hpp",
|
||||
]
|
||||
-----
|
||||
|
|
|
@ -117,6 +117,10 @@ class PanelWindowInterface: public WindowInterface {
|
|||
Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged);
|
||||
/// Defaults to `ExclusionMode.Auto`.
|
||||
Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged);
|
||||
/// If the panel should render above standard windows. Defaults to true.
|
||||
QSDOC_HIDE Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged);
|
||||
/// Defaults to false.
|
||||
QSDOC_HIDE Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged);
|
||||
// clang-format on
|
||||
QSDOC_NAMED_ELEMENT(PanelWindow);
|
||||
|
||||
|
@ -135,9 +139,17 @@ public:
|
|||
[[nodiscard]] virtual ExclusionMode::Enum exclusionMode() const = 0;
|
||||
virtual void setExclusionMode(ExclusionMode::Enum exclusionMode) = 0;
|
||||
|
||||
[[nodiscard]] virtual bool aboveWindows() const = 0;
|
||||
virtual void setAboveWindows(bool aboveWindows) = 0;
|
||||
|
||||
[[nodiscard]] virtual bool focusable() const = 0;
|
||||
virtual void setFocusable(bool focusable) = 0;
|
||||
|
||||
signals:
|
||||
void anchorsChanged();
|
||||
void marginsChanged();
|
||||
void exclusiveZoneChanged();
|
||||
void exclusionModeChanged();
|
||||
void aboveWindowsChanged();
|
||||
void focusableChanged();
|
||||
};
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -60,6 +60,9 @@ signals:
|
|||
|
||||
private slots:
|
||||
void recalcChains();
|
||||
void itemDestroyed();
|
||||
void aDestroyed();
|
||||
void bDestroyed();
|
||||
|
||||
private:
|
||||
void resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -5,3 +5,7 @@ endif()
|
|||
if (SERVICE_PIPEWIRE)
|
||||
add_subdirectory(pipewire)
|
||||
endif()
|
||||
|
||||
if (SERVICE_MPRIS)
|
||||
add_subdirectory(mpris)
|
||||
endif()
|
||||
|
|
39
src/services/mpris/CMakeLists.txt
Normal file
39
src/services/mpris/CMakeLists.txt
Normal 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)
|
7
src/services/mpris/module.md
Normal file
7
src/services/mpris/module.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
name = "Quickshell.Services.Mpris"
|
||||
description = "Mpris Service"
|
||||
headers = [
|
||||
"player.hpp",
|
||||
"watcher.hpp",
|
||||
]
|
||||
-----
|
24
src/services/mpris/org.mpris.MediaPlayer2.Player.xml
Normal file
24
src/services/mpris/org.mpris.MediaPlayer2.Player.xml
Normal 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>
|
6
src/services/mpris/org.mpris.MediaPlayer2.xml
Normal file
6
src/services/mpris/org.mpris.MediaPlayer2.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<node>
|
||||
<interface name="org.mpris.MediaPlayer2">
|
||||
<method name="Raise"/>
|
||||
<method name="Quit"/>
|
||||
</interface>
|
||||
</node>
|
459
src/services/mpris/player.cpp
Normal file
459
src/services/mpris/player.cpp
Normal 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
|
332
src/services/mpris/player.hpp
Normal file
332
src/services/mpris/player.hpp
Normal 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
|
114
src/services/mpris/watcher.cpp
Normal file
114
src/services/mpris/watcher.cpp
Normal 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
|
57
src/services/mpris/watcher.hpp
Normal file
57
src/services/mpris/watcher.hpp
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"};
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
18
src/wayland/hyprland/ipc/CMakeLists.txt
Normal file
18
src/wayland/hyprland/ipc/CMakeLists.txt
Normal 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)
|
561
src/wayland/hyprland/ipc/connection.cpp
Normal file
561
src/wayland/hyprland/ipc/connection.cpp
Normal 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
|
124
src/wayland/hyprland/ipc/connection.hpp
Normal file
124
src/wayland/hyprland/ipc/connection.hpp
Normal 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
|
136
src/wayland/hyprland/ipc/monitor.cpp
Normal file
136
src/wayland/hyprland/ipc/monitor.cpp
Normal 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
|
85
src/wayland/hyprland/ipc/monitor.hpp
Normal file
85
src/wayland/hyprland/ipc/monitor.hpp
Normal 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
|
52
src/wayland/hyprland/ipc/qml.cpp
Normal file
52
src/wayland/hyprland/ipc/qml.cpp
Normal 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
|
66
src/wayland/hyprland/ipc/qml.hpp
Normal file
66
src/wayland/hyprland/ipc/qml.hpp
Normal 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
|
79
src/wayland/hyprland/ipc/workspace.cpp
Normal file
79
src/wayland/hyprland/ipc/workspace.cpp
Normal 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
|
59
src/wayland/hyprland/ipc/workspace.hpp
Normal file
59
src/wayland/hyprland/ipc/workspace.hpp
Normal 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
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -4,5 +4,6 @@ headers = [
|
|||
"wlr_layershell/window.hpp",
|
||||
"wlr_layershell.hpp",
|
||||
"session_lock.hpp",
|
||||
"toplevel_management/qml.hpp",
|
||||
]
|
||||
-----
|
||||
|
|
22
src/wayland/toplevel_management/CMakeLists.txt
Normal file
22
src/wayland/toplevel_management/CMakeLists.txt
Normal 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)
|
228
src/wayland/toplevel_management/handle.cpp
Normal file
228
src/wayland/toplevel_management/handle.cpp
Normal 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
|
77
src/wayland/toplevel_management/handle.hpp
Normal file
77
src/wayland/toplevel_management/handle.hpp
Normal 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
|
67
src/wayland/toplevel_management/manager.cpp
Normal file
67
src/wayland/toplevel_management/manager.cpp
Normal 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
|
47
src/wayland/toplevel_management/manager.hpp
Normal file
47
src/wayland/toplevel_management/manager.hpp
Normal 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
|
153
src/wayland/toplevel_management/qml.cpp
Normal file
153
src/wayland/toplevel_management/qml.cpp
Normal 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
|
140
src/wayland/toplevel_management/qml.hpp
Normal file
140
src/wayland/toplevel_management/qml.hpp
Normal 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
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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
22
src/x11/CMakeLists.txt
Normal 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
29
src/x11/init.cpp
Normal 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
431
src/x11/panel_window.cpp
Normal 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
160
src/x11/panel_window.hpp
Normal 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
55
src/x11/util.cpp
Normal 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
29
src/x11/util.hpp
Normal 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 {};
|
||||
};
|
Loading…
Reference in a new issue