forked from quickshell/quickshell
Compare commits
32 commits
4ee9ac7f7c
...
67783ec24c
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 |
|
@ -5,6 +5,7 @@ Checks: >
|
||||||
-*,
|
-*,
|
||||||
bugprone-*,
|
bugprone-*,
|
||||||
-bugprone-easily-swappable-parameters,
|
-bugprone-easily-swappable-parameters,
|
||||||
|
-bugprone-forward-declararion-namespace,
|
||||||
concurrency-*,
|
concurrency-*,
|
||||||
cppcoreguidelines-*,
|
cppcoreguidelines-*,
|
||||||
-cppcoreguidelines-owning-memory,
|
-cppcoreguidelines-owning-memory,
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,3 +1,7 @@
|
||||||
|
# related repos
|
||||||
|
/docs
|
||||||
|
/examples
|
||||||
|
|
||||||
# build files
|
# build files
|
||||||
/result
|
/result
|
||||||
/build/
|
/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,13 +9,15 @@ option(BUILD_TESTING "Build tests" OFF)
|
||||||
option(ASAN "Enable ASAN" OFF)
|
option(ASAN "Enable ASAN" OFF)
|
||||||
option(FRAME_POINTERS "Always keep frame pointers" ${ASAN})
|
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(SOCKETS "Enable unix socket support" ON)
|
||||||
option(WAYLAND "Enable wayland support" ON)
|
option(WAYLAND "Enable wayland support" ON)
|
||||||
option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" 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_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(X11 "Enable X11 support" ON)
|
||||||
option(HYPRLAND "Support hyprland specific features" ON)
|
option(HYPRLAND "Support hyprland specific features" ON)
|
||||||
|
option(HYPRLAND_IPC "Hyprland IPC" ON)
|
||||||
option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON)
|
option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON)
|
||||||
option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
|
option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
|
||||||
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
|
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
|
||||||
|
@ -23,13 +25,14 @@ option(SERVICE_PIPEWIRE "PipeWire service" ON)
|
||||||
option(SERVICE_MPRIS "Mpris service" ON)
|
option(SERVICE_MPRIS "Mpris service" ON)
|
||||||
|
|
||||||
message(STATUS "Quickshell configuration")
|
message(STATUS "Quickshell configuration")
|
||||||
message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}")
|
message(STATUS " Jemalloc: ${USE_JEMALLOC}")
|
||||||
message(STATUS " Build tests: ${BUILD_TESTING}")
|
message(STATUS " Build tests: ${BUILD_TESTING}")
|
||||||
message(STATUS " Sockets: ${SOCKETS}")
|
message(STATUS " Sockets: ${SOCKETS}")
|
||||||
message(STATUS " Wayland: ${WAYLAND}")
|
message(STATUS " Wayland: ${WAYLAND}")
|
||||||
if (WAYLAND)
|
if (WAYLAND)
|
||||||
message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}")
|
message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}")
|
||||||
message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}")
|
message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}")
|
||||||
|
message(STATUS " Toplevel Management: ${WAYLAND_TOPLEVEL_MANAGEMENT}")
|
||||||
endif ()
|
endif ()
|
||||||
message(STATUS " X11: ${X11}")
|
message(STATUS " X11: ${X11}")
|
||||||
message(STATUS " Services")
|
message(STATUS " Services")
|
||||||
|
@ -38,6 +41,7 @@ message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}")
|
||||||
message(STATUS " Mpris: ${SERVICE_MPRIS}")
|
message(STATUS " Mpris: ${SERVICE_MPRIS}")
|
||||||
message(STATUS " Hyprland: ${HYPRLAND}")
|
message(STATUS " Hyprland: ${HYPRLAND}")
|
||||||
if (HYPRLAND)
|
if (HYPRLAND)
|
||||||
|
message(STATUS " IPC: ${HYPRLAND_IPC}")
|
||||||
message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
|
message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
|
||||||
message(STATUS " Global Shortcuts: ${HYPRLAND_GLOBAL_SHORTCUTS}")
|
message(STATUS " Global Shortcuts: ${HYPRLAND_GLOBAL_SHORTCUTS}")
|
||||||
endif()
|
endif()
|
||||||
|
@ -132,8 +136,11 @@ function (qs_pch target)
|
||||||
endif()
|
endif()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
if (NVIDIA_COMPAT)
|
|
||||||
add_compile_definitions(NVIDIA_COMPAT)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_subdirectory(src)
|
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.
|
103
README.md
103
README.md
|
@ -14,19 +14,6 @@ can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quick
|
||||||
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.
|
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
|
# Installation
|
||||||
|
|
||||||
## Nix
|
## Nix
|
||||||
|
@ -48,81 +35,33 @@ This repo has a nix flake you can use to install the package directly:
|
||||||
Quickshell's binary is available at `quickshell.packages.<system>.default` to be added to
|
Quickshell's binary is available at `quickshell.packages.<system>.default` to be added to
|
||||||
lists such as `environment.systemPackages` or `home.packages`.
|
lists such as `environment.systemPackages` or `home.packages`.
|
||||||
|
|
||||||
`quickshell.packages.<system>.nvidia` is also available for nvidia users which fixes some
|
The package contains several features detailed in [BUILD.md](BUILD.md) which can be enabled
|
||||||
common crashes.
|
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.
|
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
|
## Anything else
|
||||||
To build quickshell at all, you will need the following packages (names may vary by distro)
|
See [BUILD.md](BUILD.md) for instructions on building and packaging quickshell.
|
||||||
|
|
||||||
- just
|
# Contributing / Development
|
||||||
- cmake
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
||||||
- ninja
|
|
||||||
- Qt6 [ QtBase, QtDeclarative ]
|
|
||||||
|
|
||||||
To build with wayland support you will additionally need:
|
|
||||||
- pkg-config
|
|
||||||
- wayland
|
|
||||||
- wayland-scanner (may be part of wayland on some distros)
|
|
||||||
- wayland-protocols
|
|
||||||
- Qt6 [ QtWayland ]
|
|
||||||
|
|
||||||
To build with x11 support you will additionally need:
|
|
||||||
- libxcb
|
|
||||||
|
|
||||||
To build with pipewire support you will additionally need:
|
|
||||||
- libpipewire
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
To make a release build of quickshell run:
|
|
||||||
```sh
|
|
||||||
$ just release
|
|
||||||
```
|
|
||||||
|
|
||||||
If running an nvidia GPU, instead run:
|
|
||||||
```sh
|
|
||||||
$ just configure release -DNVIDIA_COMPAT=ON
|
|
||||||
$ just build
|
|
||||||
```
|
|
||||||
|
|
||||||
(These commands are just aliases for cmake commands you can run directly,
|
|
||||||
see the Justfile for more information.)
|
|
||||||
|
|
||||||
If you have all the dependencies installed and they are in expected
|
|
||||||
locations this will build correctly.
|
|
||||||
|
|
||||||
To install to /usr/local/bin run as root (usually `sudo`) in the same folder:
|
|
||||||
```
|
|
||||||
$ just install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building (Nix)
|
|
||||||
|
|
||||||
You can build directly using the provided nix flake or nix package.
|
|
||||||
```
|
|
||||||
nix build
|
|
||||||
nix build -f package.nix # calls default.nix with a basic callPackage expression
|
|
||||||
```
|
|
||||||
|
|
||||||
# Development
|
|
||||||
|
|
||||||
For nix there is a devshell available from `shell.nix` and as a devShell
|
|
||||||
output from the flake.
|
|
||||||
|
|
||||||
The Justfile contains various useful aliases:
|
|
||||||
- `just configure [<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`
|
|
||||||
|
|
||||||
#### License
|
#### License
|
||||||
|
|
||||||
|
|
33
default.nix
33
default.nix
|
@ -8,6 +8,7 @@
|
||||||
cmake,
|
cmake,
|
||||||
ninja,
|
ninja,
|
||||||
qt6,
|
qt6,
|
||||||
|
jemalloc,
|
||||||
wayland,
|
wayland,
|
||||||
wayland-protocols,
|
wayland-protocols,
|
||||||
xorg,
|
xorg,
|
||||||
|
@ -25,11 +26,12 @@
|
||||||
else "unknown"),
|
else "unknown"),
|
||||||
|
|
||||||
debug ? false,
|
debug ? false,
|
||||||
enableWayland ? true,
|
withJemalloc ? true, # masks heap fragmentation
|
||||||
enableX11 ? true,
|
withQtSvg ? true,
|
||||||
enablePipewire ? true,
|
withWayland ? true,
|
||||||
nvidiaCompat ? false,
|
withX11 ? true,
|
||||||
svgSupport ? true, # you almost always want this
|
withPipewire ? true,
|
||||||
|
withHyprland ? true,
|
||||||
}: buildStdenv.mkDerivation {
|
}: buildStdenv.mkDerivation {
|
||||||
pname = "quickshell${lib.optionalString debug "-debug"}";
|
pname = "quickshell${lib.optionalString debug "-debug"}";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
|
@ -39,8 +41,8 @@
|
||||||
cmake
|
cmake
|
||||||
ninja
|
ninja
|
||||||
qt6.wrapQtAppsHook
|
qt6.wrapQtAppsHook
|
||||||
] ++ (lib.optionals enableWayland [
|
|
||||||
pkg-config
|
pkg-config
|
||||||
|
] ++ (lib.optionals withWayland [
|
||||||
wayland-protocols
|
wayland-protocols
|
||||||
wayland-scanner
|
wayland-scanner
|
||||||
]);
|
]);
|
||||||
|
@ -49,12 +51,13 @@
|
||||||
qt6.qtbase
|
qt6.qtbase
|
||||||
qt6.qtdeclarative
|
qt6.qtdeclarative
|
||||||
]
|
]
|
||||||
++ (lib.optionals enableWayland [ qt6.qtwayland wayland ])
|
++ (lib.optional withJemalloc jemalloc)
|
||||||
++ (lib.optionals enableX11 [ xorg.libxcb ])
|
++ (lib.optional withQtSvg qt6.qtsvg)
|
||||||
++ (lib.optionals svgSupport [ qt6.qtsvg ])
|
++ (lib.optionals withWayland [ qt6.qtwayland wayland ])
|
||||||
++ (lib.optionals enablePipewire [ pipewire ]);
|
++ (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
|
configurePhase = let
|
||||||
cmakeBuildType = if debug
|
cmakeBuildType = if debug
|
||||||
|
@ -67,9 +70,11 @@
|
||||||
|
|
||||||
cmakeFlags = [
|
cmakeFlags = [
|
||||||
"-DGIT_REVISION=${gitRev}"
|
"-DGIT_REVISION=${gitRev}"
|
||||||
] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF"
|
]
|
||||||
++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON"
|
++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF"
|
||||||
++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF";
|
++ lib.optional (!withWayland) "-DWAYLAND=OFF"
|
||||||
|
++ lib.optional (!withPipewire) "-DSERVICE_PIPEWIRE=OFF"
|
||||||
|
++ lib.optional (!withHyprland) "-DHYPRLAND=OFF";
|
||||||
|
|
||||||
buildPhase = "ninjaBuildPhase";
|
buildPhase = "ninjaBuildPhase";
|
||||||
enableParallelBuilding = true;
|
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 {
|
quickshell = pkgs.callPackage ./default.nix {
|
||||||
gitRev = self.rev or self.dirtyRev;
|
gitRev = self.rev or self.dirtyRev;
|
||||||
};
|
};
|
||||||
quickshell-nvidia = quickshell.override { nvidiaCompat = true; };
|
|
||||||
|
|
||||||
default = quickshell;
|
default = quickshell;
|
||||||
nvidia = quickshell-nvidia;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
devShells = forEachSystem (system: pkgs: rec {
|
devShells = forEachSystem (system: pkgs: rec {
|
||||||
|
|
|
@ -26,6 +26,8 @@ qt_add_library(quickshell-core STATIC
|
||||||
imageprovider.cpp
|
imageprovider.cpp
|
||||||
transformwatcher.cpp
|
transformwatcher.cpp
|
||||||
boundcomponent.cpp
|
boundcomponent.cpp
|
||||||
|
model.cpp
|
||||||
|
elapsedtimer.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
|
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
|
||||||
|
|
|
@ -10,5 +10,8 @@
|
||||||
#define QSDOC_ELEMENT
|
#define QSDOC_ELEMENT
|
||||||
#define QSDOC_NAMED_ELEMENT(name)
|
#define QSDOC_NAMED_ELEMENT(name)
|
||||||
|
|
||||||
|
// change the cname used for this type
|
||||||
|
#define QSDOC_CNAME(name)
|
||||||
|
|
||||||
// overridden properties
|
// overridden properties
|
||||||
#define QSDOC_PROPERTY_OVERRIDE(...)
|
#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 <qcontainerfwd.h>
|
||||||
#include <qcoreapplication.h>
|
#include <qcoreapplication.h>
|
||||||
#include <qdebug.h>
|
#include <qdebug.h>
|
||||||
|
#include <qdir.h>
|
||||||
|
#include <qfileinfo.h>
|
||||||
#include <qfilesystemwatcher.h>
|
#include <qfilesystemwatcher.h>
|
||||||
#include <qhash.h>
|
#include <qhash.h>
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
|
@ -12,7 +14,6 @@
|
||||||
#include <qqmlcontext.h>
|
#include <qqmlcontext.h>
|
||||||
#include <qqmlengine.h>
|
#include <qqmlengine.h>
|
||||||
#include <qqmlincubator.h>
|
#include <qqmlincubator.h>
|
||||||
#include <qtimer.h>
|
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
#include "iconimageprovider.hpp"
|
#include "iconimageprovider.hpp"
|
||||||
|
@ -25,8 +26,10 @@
|
||||||
|
|
||||||
static QHash<QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
|
static QHash<QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
|
||||||
|
|
||||||
EngineGeneration::EngineGeneration(QmlScanner scanner)
|
EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
|
||||||
: scanner(std::move(scanner))
|
: rootPath(rootPath)
|
||||||
|
, scanner(std::move(scanner))
|
||||||
|
, urlInterceptor(this->rootPath)
|
||||||
, interceptNetFactory(this->scanner.qmldirIntercepts)
|
, interceptNetFactory(this->scanner.qmldirIntercepts)
|
||||||
, engine(new QQmlEngine()) {
|
, engine(new QQmlEngine()) {
|
||||||
g_generations.insert(this->engine, this);
|
g_generations.insert(this->engine, this);
|
||||||
|
@ -43,36 +46,41 @@ EngineGeneration::EngineGeneration(QmlScanner scanner)
|
||||||
}
|
}
|
||||||
|
|
||||||
EngineGeneration::~EngineGeneration() {
|
EngineGeneration::~EngineGeneration() {
|
||||||
g_generations.remove(this->engine);
|
if (this->engine != nullptr) {
|
||||||
delete this->engine;
|
qFatal() << this << "destroyed without calling destroy()";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EngineGeneration::destroy() {
|
void EngineGeneration::destroy() {
|
||||||
// Multiple generations can detect a reload at the same time.
|
if (this->watcher != nullptr) {
|
||||||
delete this->watcher;
|
// Multiple generations can detect a reload at the same time.
|
||||||
this->watcher = nullptr;
|
QObject::disconnect(this->watcher, nullptr, this, nullptr);
|
||||||
|
this->watcher->deleteLater();
|
||||||
|
this->watcher = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// Yes all of this is actually necessary.
|
if (this->root != nullptr) {
|
||||||
if (this->engine != nullptr && this->root != nullptr) {
|
|
||||||
QObject::connect(this->root, &QObject::destroyed, this, [this]() {
|
QObject::connect(this->root, &QObject::destroyed, this, [this]() {
|
||||||
// The timer seems to fix *one* of the possible qml item destructor crashes.
|
// prevent further js execution between garbage collection and engine destruction.
|
||||||
QTimer::singleShot(0, [this]() {
|
this->engine->setInterrupted(true);
|
||||||
// Garbage is not collected during engine destruction.
|
|
||||||
this->engine->collectGarbage();
|
|
||||||
|
|
||||||
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.
|
// Garbage is not collected during engine destruction.
|
||||||
// Pray you don't hit one.
|
this->engine->collectGarbage();
|
||||||
// Note: it appeats *some* of the crashes are related to values owned by the generation.
|
|
||||||
// Test by commenting the connect() above.
|
delete this->engine;
|
||||||
this->engine->deleteLater();
|
this->engine = nullptr;
|
||||||
this->engine = nullptr;
|
delete this;
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this->root->deleteLater();
|
this->root->deleteLater();
|
||||||
this->root = nullptr;
|
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) {
|
for (auto& file: this->scanner.scannedFiles) {
|
||||||
this->watcher->addPath(file);
|
this->watcher->addPath(file);
|
||||||
|
this->watcher->addPath(QFileInfo(file).dir().absolutePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
QObject::connect(
|
QObject::connect(
|
||||||
this->watcher,
|
this->watcher,
|
||||||
&QFileSystemWatcher::fileChanged,
|
&QFileSystemWatcher::fileChanged,
|
||||||
this,
|
this,
|
||||||
&EngineGeneration::filesChanged
|
&EngineGeneration::onFileChanged
|
||||||
|
);
|
||||||
|
|
||||||
|
QObject::connect(
|
||||||
|
this->watcher,
|
||||||
|
&QFileSystemWatcher::directoryChanged,
|
||||||
|
this,
|
||||||
|
&EngineGeneration::onDirectoryChanged
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
|
||||||
auto* obj = dynamic_cast<QObject*>(controller);
|
auto* obj = dynamic_cast<QObject*>(controller);
|
||||||
|
|
||||||
|
@ -235,12 +269,16 @@ void EngineGeneration::assignIncubationController() {
|
||||||
this->engine->setIncubationController(controller);
|
this->engine->setIncubationController(controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EngineGeneration* EngineGeneration::findEngineGeneration(QQmlEngine* engine) {
|
||||||
|
return g_generations.value(engine);
|
||||||
|
}
|
||||||
|
|
||||||
EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) {
|
EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) {
|
||||||
while (object != nullptr) {
|
while (object != nullptr) {
|
||||||
auto* context = QQmlEngine::contextForObject(object);
|
auto* context = QQmlEngine::contextForObject(object);
|
||||||
|
|
||||||
if (context != nullptr) {
|
if (context != nullptr) {
|
||||||
if (auto* generation = g_generations.value(context->engine())) {
|
if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) {
|
||||||
return generation;
|
return generation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
#include <qcontainerfwd.h>
|
||||||
|
#include <qdir.h>
|
||||||
#include <qfilesystemwatcher.h>
|
#include <qfilesystemwatcher.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qpair.h>
|
#include <qpair.h>
|
||||||
|
#include <qqmlengine.h>
|
||||||
#include <qqmlincubator.h>
|
#include <qqmlincubator.h>
|
||||||
#include <qtclasshelpermacros.h>
|
#include <qtclasshelpermacros.h>
|
||||||
|
|
||||||
|
@ -14,12 +16,13 @@
|
||||||
#include "singleton.hpp"
|
#include "singleton.hpp"
|
||||||
|
|
||||||
class RootWrapper;
|
class RootWrapper;
|
||||||
|
class QuickshellGlobal;
|
||||||
|
|
||||||
class EngineGeneration: public QObject {
|
class EngineGeneration: public QObject {
|
||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit EngineGeneration(QmlScanner scanner);
|
explicit EngineGeneration(const QDir& rootPath, QmlScanner scanner);
|
||||||
~EngineGeneration() override;
|
~EngineGeneration() override;
|
||||||
Q_DISABLE_COPY_MOVE(EngineGeneration);
|
Q_DISABLE_COPY_MOVE(EngineGeneration);
|
||||||
|
|
||||||
|
@ -30,9 +33,11 @@ public:
|
||||||
void registerIncubationController(QQmlIncubationController* controller);
|
void registerIncubationController(QQmlIncubationController* controller);
|
||||||
void deregisterIncubationController(QQmlIncubationController* controller);
|
void deregisterIncubationController(QQmlIncubationController* controller);
|
||||||
|
|
||||||
|
static EngineGeneration* findEngineGeneration(QQmlEngine* engine);
|
||||||
static EngineGeneration* findObjectGeneration(QObject* object);
|
static EngineGeneration* findObjectGeneration(QObject* object);
|
||||||
|
|
||||||
RootWrapper* wrapper = nullptr;
|
RootWrapper* wrapper = nullptr;
|
||||||
|
QDir rootPath;
|
||||||
QmlScanner scanner;
|
QmlScanner scanner;
|
||||||
QsUrlInterceptor urlInterceptor;
|
QsUrlInterceptor urlInterceptor;
|
||||||
QsInterceptNetworkAccessManagerFactory interceptNetFactory;
|
QsInterceptNetworkAccessManagerFactory interceptNetFactory;
|
||||||
|
@ -40,8 +45,10 @@ public:
|
||||||
ShellRoot* root = nullptr;
|
ShellRoot* root = nullptr;
|
||||||
SingletonRegistry singletonRegistry;
|
SingletonRegistry singletonRegistry;
|
||||||
QFileSystemWatcher* watcher = nullptr;
|
QFileSystemWatcher* watcher = nullptr;
|
||||||
|
QVector<QString> deletedWatchedFiles;
|
||||||
DelayedQmlIncubationController delayedIncubationController;
|
DelayedQmlIncubationController delayedIncubationController;
|
||||||
bool reloadComplete = false;
|
bool reloadComplete = false;
|
||||||
|
QuickshellGlobal* qsgInstance = nullptr;
|
||||||
|
|
||||||
void destroy();
|
void destroy();
|
||||||
|
|
||||||
|
@ -50,6 +57,8 @@ signals:
|
||||||
void reloadFinished();
|
void reloadFinished();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
void onFileChanged(const QString& name);
|
||||||
|
void onDirectoryChanged();
|
||||||
void incubationControllerDestroyed();
|
void incubationControllerDestroyed();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include <qguiapplication.h>
|
#include <qguiapplication.h>
|
||||||
#include <qhash.h>
|
#include <qhash.h>
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
|
#include <qqmldebug.h>
|
||||||
#include <qquickwindow.h>
|
#include <qquickwindow.h>
|
||||||
#include <qstandardpaths.h>
|
#include <qstandardpaths.h>
|
||||||
#include <qstring.h>
|
#include <qstring.h>
|
||||||
|
@ -29,6 +30,9 @@ int qs_main(int argc, char** argv) {
|
||||||
auto desktopSettingsAware = true;
|
auto desktopSettingsAware = true;
|
||||||
QHash<QString, QString> envOverrides;
|
QHash<QString, QString> envOverrides;
|
||||||
|
|
||||||
|
int debugPort = -1;
|
||||||
|
bool waitForDebug = false;
|
||||||
|
|
||||||
{
|
{
|
||||||
const auto app = QCoreApplication(argc, argv);
|
const auto app = QCoreApplication(argc, argv);
|
||||||
QCoreApplication::setApplicationName("quickshell");
|
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 configOption = QCommandLineOption({"c", "config"}, "Name of a configuration in the manifest.", "name");
|
||||||
auto pathOption = QCommandLineOption({"p", "path"}, "Path to a configuration file.", "path");
|
auto pathOption = QCommandLineOption({"p", "path"}, "Path to a configuration file.", "path");
|
||||||
auto workdirOption = QCommandLineOption({"d", "workdir"}, "Initial working directory.", "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
|
// clang-format on
|
||||||
|
|
||||||
parser.addOption(currentOption);
|
parser.addOption(currentOption);
|
||||||
|
@ -51,8 +57,30 @@ int qs_main(int argc, char** argv) {
|
||||||
parser.addOption(configOption);
|
parser.addOption(configOption);
|
||||||
parser.addOption(pathOption);
|
parser.addOption(pathOption);
|
||||||
parser.addOption(workdirOption);
|
parser.addOption(workdirOption);
|
||||||
|
parser.addOption(debugPortOption);
|
||||||
|
parser.addOption(debugWaitOption);
|
||||||
parser.process(app);
|
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);
|
auto printCurrent = parser.isSet(currentOption);
|
||||||
|
|
||||||
|
@ -298,6 +326,13 @@ int qs_main(int argc, char** argv) {
|
||||||
qputenv(var.toUtf8(), val.toUtf8());
|
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::setDesktopSettingsAware(desktopSettingsAware);
|
||||||
|
|
||||||
QGuiApplication* app = nullptr;
|
QGuiApplication* app = nullptr;
|
||||||
|
@ -308,6 +343,13 @@ int qs_main(int argc, char** argv) {
|
||||||
app = new QGuiApplication(argc, 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()) {
|
if (!workingDirectory.isEmpty()) {
|
||||||
QDir::setCurrent(workingDirectory);
|
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",
|
"easingcurve.hpp",
|
||||||
"transformwatcher.hpp",
|
"transformwatcher.hpp",
|
||||||
"boundcomponent.hpp",
|
"boundcomponent.hpp",
|
||||||
|
"model.hpp",
|
||||||
|
"elapsedtimer.hpp",
|
||||||
]
|
]
|
||||||
-----
|
-----
|
||||||
|
|
|
@ -156,16 +156,7 @@ void ProxyWindowBase::completeWindow() {
|
||||||
emit this->screenChanged();
|
emit this->screenChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ProxyWindowBase::deleteOnInvisible() const {
|
bool ProxyWindowBase::deleteOnInvisible() const { return false; }
|
||||||
#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
|
|
||||||
}
|
|
||||||
|
|
||||||
QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; }
|
QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; }
|
||||||
QQuickItem* ProxyWindowBase::contentItem() const { return this->mContentItem; }
|
QQuickItem* ProxyWindowBase::contentItem() const { return this->mContentItem; }
|
||||||
|
|
|
@ -187,3 +187,14 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT
|
||||||
|
|
||||||
return qEnvironmentVariable(vstr.data());
|
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:
|
public:
|
||||||
[[nodiscard]] qint32 processId() const;
|
[[nodiscard]] qint32 processId() const;
|
||||||
|
|
||||||
QuickshellGlobal(QObject* parent = nullptr);
|
|
||||||
|
|
||||||
QQmlListProperty<QuickshellScreenInfo> screens();
|
QQmlListProperty<QuickshellScreenInfo> screens();
|
||||||
|
|
||||||
/// Reload the shell from the [ShellRoot].
|
/// Reload the shell from the [ShellRoot].
|
||||||
|
@ -133,17 +131,25 @@ public:
|
||||||
[[nodiscard]] bool watchFiles() const;
|
[[nodiscard]] bool watchFiles() const;
|
||||||
void setWatchFiles(bool watchFiles);
|
void setWatchFiles(bool watchFiles);
|
||||||
|
|
||||||
|
static QuickshellGlobal* create(QQmlEngine* engine, QJSEngine* /*unused*/);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
/// Sent when the last window is closed.
|
/// Sent when the last window is closed.
|
||||||
///
|
///
|
||||||
/// To make the application exit when the last window is closed run `Qt.quit()`.
|
/// To make the application exit when the last window is closed run `Qt.quit()`.
|
||||||
void lastWindowClosed();
|
void lastWindowClosed();
|
||||||
|
/// The reload sequence has completed successfully.
|
||||||
|
void reloadCompleted();
|
||||||
|
/// The reload sequence has failed.
|
||||||
|
void reloadFailed(QString errorString);
|
||||||
|
|
||||||
void screensChanged();
|
void screensChanged();
|
||||||
void workingDirectoryChanged();
|
void workingDirectoryChanged();
|
||||||
void watchFilesChanged();
|
void watchFilesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
QuickshellGlobal(QObject* parent = nullptr);
|
||||||
|
|
||||||
static qsizetype screensCount(QQmlListProperty<QuickshellScreenInfo>* prop);
|
static qsizetype screensCount(QQmlListProperty<QuickshellScreenInfo>* prop);
|
||||||
static QuickshellScreenInfo* screenAt(QQmlListProperty<QuickshellScreenInfo>* prop, qsizetype i);
|
static QuickshellScreenInfo* screenAt(QQmlListProperty<QuickshellScreenInfo>* prop, qsizetype i);
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,22 @@
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
|
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
|
// 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.
|
// asynchronous loading over a network. qsintercept is considered to be over a network.
|
||||||
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") {
|
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <qdir.h>
|
||||||
#include <qhash.h>
|
#include <qhash.h>
|
||||||
#include <qloggingcategory.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qnetworkaccessmanager.h>
|
#include <qnetworkaccessmanager.h>
|
||||||
|
@ -13,7 +14,12 @@ Q_DECLARE_LOGGING_CATEGORY(logQsIntercept);
|
||||||
|
|
||||||
class QsUrlInterceptor: public QQmlAbstractUrlInterceptor {
|
class QsUrlInterceptor: public QQmlAbstractUrlInterceptor {
|
||||||
public:
|
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 {
|
class QsInterceptDataReply: public QNetworkReply {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qqmlcomponent.h>
|
#include <qqmlcomponent.h>
|
||||||
#include <qqmlengine.h>
|
#include <qqmlengine.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
#include <qurl.h>
|
#include <qurl.h>
|
||||||
|
|
||||||
#include "generation.hpp"
|
#include "generation.hpp"
|
||||||
|
@ -42,10 +43,11 @@ RootWrapper::~RootWrapper() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void RootWrapper::reloadGraph(bool hard) {
|
void RootWrapper::reloadGraph(bool hard) {
|
||||||
auto scanner = QmlScanner();
|
auto rootPath = QFileInfo(this->rootPath).dir();
|
||||||
|
auto scanner = QmlScanner(rootPath);
|
||||||
scanner.scanQmlFile(this->rootPath);
|
scanner.scanQmlFile(this->rootPath);
|
||||||
|
|
||||||
auto* generation = new EngineGeneration(std::move(scanner));
|
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
|
||||||
generation->wrapper = this;
|
generation->wrapper = this;
|
||||||
|
|
||||||
// todo: move into EngineGeneration
|
// todo: move into EngineGeneration
|
||||||
|
@ -63,17 +65,28 @@ void RootWrapper::reloadGraph(bool hard) {
|
||||||
auto* obj = component.beginCreate(generation->engine->rootContext());
|
auto* obj = component.beginCreate(generation->engine->rootContext());
|
||||||
|
|
||||||
if (obj == nullptr) {
|
if (obj == nullptr) {
|
||||||
qWarning() << component.errorString().toStdString().c_str();
|
const QString error = "failed to create root component\n" + component.errorString();
|
||||||
qWarning() << "failed to create root component";
|
qWarning().noquote() << error;
|
||||||
delete generation;
|
generation->destroy();
|
||||||
|
|
||||||
|
if (this->generation != nullptr && this->generation->qsgInstance != nullptr) {
|
||||||
|
emit this->generation->qsgInstance->reloadFailed(error);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* newRoot = qobject_cast<ShellRoot*>(obj);
|
auto* newRoot = qobject_cast<ShellRoot*>(obj);
|
||||||
if (newRoot == nullptr) {
|
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 obj;
|
||||||
delete generation;
|
generation->destroy();
|
||||||
|
|
||||||
|
if (this->generation != nullptr && this->generation->qsgInstance != nullptr) {
|
||||||
|
emit this->generation->qsgInstance->reloadFailed(error);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,8 +94,13 @@ void RootWrapper::reloadGraph(bool hard) {
|
||||||
|
|
||||||
component.completeCreate();
|
component.completeCreate();
|
||||||
|
|
||||||
|
auto isReload = this->generation != nullptr;
|
||||||
generation->onReload(hard ? nullptr : this->generation);
|
generation->onReload(hard ? nullptr : this->generation);
|
||||||
if (hard) delete this->generation;
|
|
||||||
|
if (hard && this->generation != nullptr) {
|
||||||
|
this->generation->destroy();
|
||||||
|
}
|
||||||
|
|
||||||
this->generation = generation;
|
this->generation = generation;
|
||||||
|
|
||||||
qInfo() << "Configuration Loaded";
|
qInfo() << "Configuration Loaded";
|
||||||
|
@ -95,6 +113,10 @@ void RootWrapper::reloadGraph(bool hard) {
|
||||||
);
|
);
|
||||||
|
|
||||||
this->onWatchFilesChanged();
|
this->onWatchFilesChanged();
|
||||||
|
|
||||||
|
if (isReload && this->generation->qsgInstance != nullptr) {
|
||||||
|
emit this->generation->qsgInstance->reloadCompleted();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RootWrapper::onWatchFilesChanged() {
|
void RootWrapper::onWatchFilesChanged() {
|
||||||
|
|
|
@ -103,7 +103,15 @@ bool QmlScanner::scanQmlFile(const QString& path) {
|
||||||
this->scanDir(currentdir.path());
|
this->scanDir(currentdir.path());
|
||||||
|
|
||||||
for (auto& import: imports) {
|
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();
|
auto cpath = QFileInfo(ipath).canonicalFilePath();
|
||||||
|
|
||||||
if (cpath.isEmpty()) {
|
if (cpath.isEmpty()) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
#include <qcontainerfwd.h>
|
||||||
|
#include <qdir.h>
|
||||||
#include <qhash.h>
|
#include <qhash.h>
|
||||||
#include <qloggingcategory.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qvector.h>
|
#include <qvector.h>
|
||||||
|
@ -10,6 +11,8 @@ Q_DECLARE_LOGGING_CATEGORY(logQmlScanner);
|
||||||
// expects canonical paths
|
// expects canonical paths
|
||||||
class QmlScanner {
|
class QmlScanner {
|
||||||
public:
|
public:
|
||||||
|
QmlScanner(const QDir& rootPath): rootPath(rootPath) {}
|
||||||
|
|
||||||
void scanDir(const QString& path);
|
void scanDir(const QString& path);
|
||||||
// returns if the file has a singleton
|
// returns if the file has a singleton
|
||||||
bool scanQmlFile(const QString& path);
|
bool scanQmlFile(const QString& path);
|
||||||
|
@ -17,4 +20,7 @@ public:
|
||||||
QVector<QString> scannedDirs;
|
QVector<QString> scannedDirs;
|
||||||
QVector<QString> scannedFiles;
|
QVector<QString> scannedFiles;
|
||||||
QHash<QString, QString> qmldirIntercepts;
|
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::parentChanged, this, &TransformWatcher::recalcChains);
|
||||||
QObject::connect(item, &QQuickItem::windowChanged, 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() {
|
void TransformWatcher::linkChains() {
|
||||||
|
@ -103,6 +106,18 @@ void TransformWatcher::unlinkChains() {
|
||||||
for (auto* item: this->childChain) {
|
for (auto* item: this->childChain) {
|
||||||
QObject::disconnect(item, nullptr, this, nullptr);
|
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() {
|
void TransformWatcher::recalcChains() {
|
||||||
|
@ -111,26 +126,57 @@ void TransformWatcher::recalcChains() {
|
||||||
this->linkChains();
|
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; }
|
QQuickItem* TransformWatcher::a() const { return this->mA; }
|
||||||
|
|
||||||
void TransformWatcher::setA(QQuickItem* a) {
|
void TransformWatcher::setA(QQuickItem* a) {
|
||||||
if (this->mA == a) return;
|
if (this->mA == a) return;
|
||||||
|
if (this->mA != nullptr) QObject::disconnect(this->mA, nullptr, this, nullptr);
|
||||||
this->mA = a;
|
this->mA = a;
|
||||||
|
|
||||||
|
if (this->mA != nullptr) {
|
||||||
|
QObject::connect(this->mA, &QObject::destroyed, this, &TransformWatcher::aDestroyed);
|
||||||
|
}
|
||||||
|
|
||||||
this->recalcChains();
|
this->recalcChains();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TransformWatcher::aDestroyed() {
|
||||||
|
this->mA = nullptr;
|
||||||
|
this->unlinkChains();
|
||||||
|
emit this->aChanged();
|
||||||
|
}
|
||||||
|
|
||||||
QQuickItem* TransformWatcher::b() const { return this->mB; }
|
QQuickItem* TransformWatcher::b() const { return this->mB; }
|
||||||
|
|
||||||
void TransformWatcher::setB(QQuickItem* b) {
|
void TransformWatcher::setB(QQuickItem* b) {
|
||||||
if (this->mB == b) return;
|
if (this->mB == b) return;
|
||||||
|
if (this->mB != nullptr) QObject::disconnect(this->mB, nullptr, this, nullptr);
|
||||||
this->mB = b;
|
this->mB = b;
|
||||||
|
|
||||||
|
if (this->mB != nullptr) {
|
||||||
|
QObject::connect(this->mB, &QObject::destroyed, this, &TransformWatcher::bDestroyed);
|
||||||
|
}
|
||||||
|
|
||||||
this->recalcChains();
|
this->recalcChains();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TransformWatcher::bDestroyed() {
|
||||||
|
this->mB = nullptr;
|
||||||
|
this->unlinkChains();
|
||||||
|
emit this->bChanged();
|
||||||
|
}
|
||||||
|
|
||||||
QQuickItem* TransformWatcher::commonParent() const { return this->mCommonParent; }
|
QQuickItem* TransformWatcher::commonParent() const { return this->mCommonParent; }
|
||||||
|
|
||||||
void TransformWatcher::setCommonParent(QQuickItem* commonParent) {
|
void TransformWatcher::setCommonParent(QQuickItem* commonParent) {
|
||||||
if (this->mCommonParent == commonParent) return;
|
if (this->mCommonParent == commonParent) return;
|
||||||
this->mCommonParent = commonParent;
|
this->mCommonParent = commonParent;
|
||||||
this->resolveChains();
|
this->recalcChains();
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,9 @@ signals:
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void recalcChains();
|
void recalcChains();
|
||||||
|
void itemDestroyed();
|
||||||
|
void aDestroyed();
|
||||||
|
void bDestroyed();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent);
|
void resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent);
|
||||||
|
|
|
@ -60,6 +60,7 @@ MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(paren
|
||||||
QObject::connect(&this->pCanRaise, &AbstractDBusProperty::changed, this, &MprisPlayer::canRaiseChanged);
|
QObject::connect(&this->pCanRaise, &AbstractDBusProperty::changed, this, &MprisPlayer::canRaiseChanged);
|
||||||
QObject::connect(&this->pCanSetFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::canSetFullscreenChanged);
|
QObject::connect(&this->pCanSetFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::canSetFullscreenChanged);
|
||||||
QObject::connect(&this->pIdentity, &AbstractDBusProperty::changed, this, &MprisPlayer::identityChanged);
|
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->pFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::fullscreenChanged);
|
||||||
QObject::connect(&this->pSupportedUriSchemes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedUriSchemesChanged);
|
QObject::connect(&this->pSupportedUriSchemes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedUriSchemesChanged);
|
||||||
QObject::connect(&this->pSupportedMimeTypes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedMimeTypesChanged);
|
QObject::connect(&this->pSupportedMimeTypes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedMimeTypesChanged);
|
||||||
|
@ -155,6 +156,7 @@ bool MprisPlayer::canRaise() const { return this->pCanRaise.get(); }
|
||||||
bool MprisPlayer::canSetFullscreen() const { return this->pCanSetFullscreen.get(); }
|
bool MprisPlayer::canSetFullscreen() const { return this->pCanSetFullscreen.get(); }
|
||||||
|
|
||||||
QString MprisPlayer::identity() const { return this->pIdentity.get(); }
|
QString MprisPlayer::identity() const { return this->pIdentity.get(); }
|
||||||
|
QString MprisPlayer::desktopEntry() const { return this->pDesktopEntry.get(); }
|
||||||
|
|
||||||
qlonglong MprisPlayer::positionMs() const {
|
qlonglong MprisPlayer::positionMs() const {
|
||||||
if (!this->positionSupported()) return 0; // unsupported
|
if (!this->positionSupported()) return 0; // unsupported
|
||||||
|
@ -190,15 +192,15 @@ void MprisPlayer::setPosition(qreal position) {
|
||||||
}
|
}
|
||||||
|
|
||||||
auto target = static_cast<qlonglong>(position * 1000) * 1000;
|
auto target = static_cast<qlonglong>(position * 1000) * 1000;
|
||||||
this->pPosition.set(target);
|
|
||||||
|
|
||||||
if (!this->mTrackId.isEmpty()) {
|
if (!this->mTrackId.isEmpty()) {
|
||||||
this->player->SetPosition(QDBusObjectPath(this->mTrackId), target);
|
this->player->SetPosition(QDBusObjectPath(this->mTrackId), target);
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
auto pos = this->positionMs() * 1000;
|
auto pos = this->positionMs() * 1000;
|
||||||
this->player->Seek(target - pos);
|
this->player->Seek(target - pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->pPosition.set(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MprisPlayer::onPositionChanged() {
|
void MprisPlayer::onPositionChanged() {
|
||||||
|
@ -245,6 +247,8 @@ void MprisPlayer::setVolume(qreal volume) {
|
||||||
QVariantMap MprisPlayer::metadata() const { return this->pMetadata.get(); }
|
QVariantMap MprisPlayer::metadata() const { return this->pMetadata.get(); }
|
||||||
|
|
||||||
void MprisPlayer::onMetadataChanged() {
|
void MprisPlayer::onMetadataChanged() {
|
||||||
|
emit this->metadataChanged();
|
||||||
|
|
||||||
auto lengthVariant = this->pMetadata.get().value("mpris:length");
|
auto lengthVariant = this->pMetadata.get().value("mpris:length");
|
||||||
qlonglong length = -1;
|
qlonglong length = -1;
|
||||||
if (lengthVariant.isValid() && lengthVariant.canConvert<qlonglong>()) {
|
if (lengthVariant.isValid() && lengthVariant.canConvert<qlonglong>()) {
|
||||||
|
@ -256,13 +260,34 @@ void MprisPlayer::onMetadataChanged() {
|
||||||
emit this->lengthChanged();
|
emit this->lengthChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto trackChanged = false;
|
||||||
|
|
||||||
auto trackidVariant = this->pMetadata.get().value("mpris:trackid");
|
auto trackidVariant = this->pMetadata.get().value("mpris:trackid");
|
||||||
if (trackidVariant.isValid() && trackidVariant.canConvert<QString>()) {
|
if (trackidVariant.isValid() && trackidVariant.canConvert<QString>()) {
|
||||||
this->mTrackId = trackidVariant.value<QString>();
|
auto trackId = trackidVariant.value<QString>();
|
||||||
this->onSeek(0);
|
|
||||||
|
if (trackId != this->mTrackId) {
|
||||||
|
this->mTrackId = trackId;
|
||||||
|
trackChanged = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emit this->metadataChanged();
|
// Helps to catch players without trackid.
|
||||||
|
auto urlVariant = this->pMetadata.get().value("xesam:url");
|
||||||
|
if (urlVariant.isValid() && urlVariant.canConvert<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; }
|
MprisPlaybackState::Enum MprisPlayer::playbackState() const { return this->mPlaybackState; }
|
||||||
|
@ -305,18 +330,25 @@ void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) {
|
||||||
void MprisPlayer::onPlaybackStatusChanged() {
|
void MprisPlayer::onPlaybackStatusChanged() {
|
||||||
const auto& status = this->pPlaybackStatus.get();
|
const auto& status = this->pPlaybackStatus.get();
|
||||||
|
|
||||||
|
auto state = MprisPlaybackState::Stopped;
|
||||||
if (status == "Playing") {
|
if (status == "Playing") {
|
||||||
this->mPlaybackState = MprisPlaybackState::Playing;
|
state = MprisPlaybackState::Playing;
|
||||||
} else if (status == "Paused") {
|
} else if (status == "Paused") {
|
||||||
this->mPlaybackState = MprisPlaybackState::Paused;
|
this->pausedTime = QDateTime::currentDateTimeUtc();
|
||||||
|
state = MprisPlaybackState::Paused;
|
||||||
} else if (status == "Stopped") {
|
} else if (status == "Stopped") {
|
||||||
this->mPlaybackState = MprisPlaybackState::Stopped;
|
state = MprisPlaybackState::Stopped;
|
||||||
} else {
|
} else {
|
||||||
this->mPlaybackState = MprisPlaybackState::Stopped;
|
state = MprisPlaybackState::Stopped;
|
||||||
qWarning() << "Received unexpected PlaybackStatus for" << this << status;
|
qWarning() << "Received unexpected PlaybackStatus for" << this << status;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit this->playbackStateChanged();
|
if (state != this->mPlaybackState) {
|
||||||
|
// make sure we're in sync at least on play/pause. Some players don't automatically send this.
|
||||||
|
this->pPosition.update();
|
||||||
|
this->mPlaybackState = state;
|
||||||
|
emit this->playbackStateChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MprisLoopState::Enum MprisPlayer::loopState() const { return this->mLoopState; }
|
MprisLoopState::Enum MprisPlayer::loopState() const { return this->mLoopState; }
|
||||||
|
|
|
@ -68,6 +68,8 @@ class MprisPlayer: public QObject {
|
||||||
Q_PROPERTY(bool canSetFullscreen READ canSetFullscreen NOTIFY canSetFullscreenChanged);
|
Q_PROPERTY(bool canSetFullscreen READ canSetFullscreen NOTIFY canSetFullscreenChanged);
|
||||||
/// The human readable name of the media player.
|
/// The human readable name of the media player.
|
||||||
Q_PROPERTY(QString identity READ identity NOTIFY identityChanged);
|
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,
|
/// The current position in the playing track, as seconds, with millisecond precision,
|
||||||
/// or `0` if `positionSupported` is false.
|
/// or `0` if `positionSupported` is false.
|
||||||
///
|
///
|
||||||
|
@ -204,6 +206,7 @@ public:
|
||||||
[[nodiscard]] bool canSetFullscreen() const;
|
[[nodiscard]] bool canSetFullscreen() const;
|
||||||
|
|
||||||
[[nodiscard]] QString identity() const;
|
[[nodiscard]] QString identity() const;
|
||||||
|
[[nodiscard]] QString desktopEntry() const;
|
||||||
|
|
||||||
[[nodiscard]] qlonglong positionMs() const;
|
[[nodiscard]] qlonglong positionMs() const;
|
||||||
[[nodiscard]] qreal position() const;
|
[[nodiscard]] qreal position() const;
|
||||||
|
@ -242,6 +245,8 @@ public:
|
||||||
[[nodiscard]] QList<QString> supportedMimeTypes() const;
|
[[nodiscard]] QList<QString> supportedMimeTypes() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
void trackChanged();
|
||||||
|
|
||||||
QSDOC_HIDE void ready();
|
QSDOC_HIDE void ready();
|
||||||
void canControlChanged();
|
void canControlChanged();
|
||||||
void canPlayChanged();
|
void canPlayChanged();
|
||||||
|
@ -253,6 +258,7 @@ signals:
|
||||||
void canRaiseChanged();
|
void canRaiseChanged();
|
||||||
void canSetFullscreenChanged();
|
void canSetFullscreenChanged();
|
||||||
void identityChanged();
|
void identityChanged();
|
||||||
|
void desktopEntryChanged();
|
||||||
void positionChanged();
|
void positionChanged();
|
||||||
void positionSupportedChanged();
|
void positionSupportedChanged();
|
||||||
void lengthChanged();
|
void lengthChanged();
|
||||||
|
@ -285,6 +291,7 @@ private:
|
||||||
// clang-format off
|
// clang-format off
|
||||||
dbus::DBusPropertyGroup appProperties;
|
dbus::DBusPropertyGroup appProperties;
|
||||||
dbus::DBusProperty<QString> pIdentity {this->appProperties, "Identity"};
|
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> pCanQuit {this->appProperties, "CanQuit"};
|
||||||
dbus::DBusProperty<bool> pCanRaise {this->appProperties, "CanRaise"};
|
dbus::DBusProperty<bool> pCanRaise {this->appProperties, "CanRaise"};
|
||||||
dbus::DBusProperty<bool> pFullscreen {this->appProperties, "Fullscreen", false, false};
|
dbus::DBusProperty<bool> pFullscreen {this->appProperties, "Fullscreen", false, false};
|
||||||
|
@ -319,6 +326,7 @@ private:
|
||||||
DBusMprisPlayerApp* app = nullptr;
|
DBusMprisPlayerApp* app = nullptr;
|
||||||
DBusMprisPlayer* player = nullptr;
|
DBusMprisPlayer* player = nullptr;
|
||||||
QString mTrackId;
|
QString mTrackId;
|
||||||
|
QString mUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace qs::service::mpris
|
} // namespace qs::service::mpris
|
||||||
|
|
|
@ -4,21 +4,19 @@
|
||||||
#include <qdbusconnection.h>
|
#include <qdbusconnection.h>
|
||||||
#include <qdbusconnectioninterface.h>
|
#include <qdbusconnectioninterface.h>
|
||||||
#include <qdbusservicewatcher.h>
|
#include <qdbusservicewatcher.h>
|
||||||
#include <qlist.h>
|
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
#include <qloggingcategory.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qqmllist.h>
|
#include <qqmllist.h>
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
|
|
||||||
|
#include "../../core/model.hpp"
|
||||||
#include "player.hpp"
|
#include "player.hpp"
|
||||||
|
|
||||||
namespace qs::service::mpris {
|
namespace qs::service::mpris {
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg);
|
Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg);
|
||||||
|
|
||||||
MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) {
|
MprisWatcher::MprisWatcher() {
|
||||||
qCDebug(logMprisWatcher) << "Starting MprisWatcher";
|
qCDebug(logMprisWatcher) << "Starting MprisWatcher";
|
||||||
|
|
||||||
auto bus = QDBusConnection::sessionBus();
|
auto bus = QDBusConnection::sessionBus();
|
||||||
|
@ -74,34 +72,15 @@ void MprisWatcher::onServiceUnregistered(const QString& service) {
|
||||||
|
|
||||||
void MprisWatcher::onPlayerReady() {
|
void MprisWatcher::onPlayerReady() {
|
||||||
auto* player = qobject_cast<MprisPlayer*>(this->sender());
|
auto* player = qobject_cast<MprisPlayer*>(this->sender());
|
||||||
this->readyPlayers.push_back(player);
|
this->readyPlayers.insertObject(player);
|
||||||
emit this->playersChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MprisWatcher::onPlayerDestroyed(QObject* object) {
|
void MprisWatcher::onPlayerDestroyed(QObject* object) {
|
||||||
auto* player = static_cast<MprisPlayer*>(object); // NOLINT
|
auto* player = static_cast<MprisPlayer*>(object); // NOLINT
|
||||||
|
this->readyPlayers.removeObject(player);
|
||||||
if (this->readyPlayers.removeOne(player)) {
|
|
||||||
emit this->playersChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QQmlListProperty<MprisPlayer> MprisWatcher::players() {
|
ObjectModel<MprisPlayer>* MprisWatcher::players() { return &this->readyPlayers; }
|
||||||
return QQmlListProperty<MprisPlayer>(
|
|
||||||
this,
|
|
||||||
nullptr,
|
|
||||||
&MprisWatcher::playersCount,
|
|
||||||
&MprisWatcher::playerAt
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
qsizetype MprisWatcher::playersCount(QQmlListProperty<MprisPlayer>* property) {
|
|
||||||
return static_cast<MprisWatcher*>(property->object)->readyPlayers.count(); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
MprisPlayer* MprisWatcher::playerAt(QQmlListProperty<MprisPlayer>* property, qsizetype index) {
|
|
||||||
return static_cast<MprisWatcher*>(property->object)->readyPlayers.at(index); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
void MprisWatcher::registerPlayer(const QString& address) {
|
void MprisWatcher::registerPlayer(const QString& address) {
|
||||||
if (this->mPlayers.contains(address)) {
|
if (this->mPlayers.contains(address)) {
|
||||||
|
@ -123,4 +102,13 @@ void MprisWatcher::registerPlayer(const QString& address) {
|
||||||
qCDebug(logMprisWatcher) << "Registered MprisPlayer" << address;
|
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
|
} // namespace qs::service::mpris
|
||||||
|
|
|
@ -4,14 +4,13 @@
|
||||||
#include <qdbusinterface.h>
|
#include <qdbusinterface.h>
|
||||||
#include <qdbusservicewatcher.h>
|
#include <qdbusservicewatcher.h>
|
||||||
#include <qhash.h>
|
#include <qhash.h>
|
||||||
#include <qlist.h>
|
|
||||||
#include <qloggingcategory.h>
|
#include <qloggingcategory.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
#include <qqmllist.h>
|
#include <qqmllist.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <qtypes.h>
|
|
||||||
|
|
||||||
|
#include "../../core/model.hpp"
|
||||||
#include "player.hpp"
|
#include "player.hpp"
|
||||||
|
|
||||||
namespace qs::service::mpris {
|
namespace qs::service::mpris {
|
||||||
|
@ -19,18 +18,11 @@ namespace qs::service::mpris {
|
||||||
///! Provides access to MprisPlayers.
|
///! Provides access to MprisPlayers.
|
||||||
class MprisWatcher: public QObject {
|
class MprisWatcher: public QObject {
|
||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
QML_NAMED_ELEMENT(Mpris);
|
|
||||||
QML_SINGLETON;
|
|
||||||
/// All connected MPRIS players.
|
|
||||||
Q_PROPERTY(QQmlListProperty<MprisPlayer> players READ players NOTIFY playersChanged);
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MprisWatcher(QObject* parent = nullptr);
|
[[nodiscard]] ObjectModel<MprisPlayer>* players();
|
||||||
|
|
||||||
[[nodiscard]] QQmlListProperty<MprisPlayer> players();
|
static MprisWatcher* instance();
|
||||||
|
|
||||||
signals:
|
|
||||||
void playersChanged();
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onServiceRegistered(const QString& service);
|
void onServiceRegistered(const QString& service);
|
||||||
|
@ -39,15 +31,27 @@ private slots:
|
||||||
void onPlayerDestroyed(QObject* object);
|
void onPlayerDestroyed(QObject* object);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static qsizetype playersCount(QQmlListProperty<MprisPlayer>* property);
|
explicit MprisWatcher();
|
||||||
static MprisPlayer* playerAt(QQmlListProperty<MprisPlayer>* property, qsizetype index);
|
|
||||||
|
|
||||||
void registerExisting();
|
void registerExisting();
|
||||||
void registerPlayer(const QString& address);
|
void registerPlayer(const QString& address);
|
||||||
|
|
||||||
QDBusServiceWatcher serviceWatcher;
|
QDBusServiceWatcher serviceWatcher;
|
||||||
QHash<QString, MprisPlayer*> mPlayers;
|
QHash<QString, MprisPlayer*> mPlayers;
|
||||||
QList<MprisPlayer*> readyPlayers;
|
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
|
} // namespace qs::service::mpris
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include <qtypes.h>
|
#include <qtypes.h>
|
||||||
#include <qvariant.h>
|
#include <qvariant.h>
|
||||||
|
|
||||||
|
#include "../../core/model.hpp"
|
||||||
#include "connection.hpp"
|
#include "connection.hpp"
|
||||||
#include "link.hpp"
|
#include "link.hpp"
|
||||||
#include "metadata.hpp"
|
#include "metadata.hpp"
|
||||||
|
@ -65,88 +66,43 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) {
|
||||||
// clang-format on
|
// clang-format on
|
||||||
}
|
}
|
||||||
|
|
||||||
QQmlListProperty<PwNodeIface> Pipewire::nodes() {
|
ObjectModel<PwNodeIface>* Pipewire::nodes() { return &this->mNodes; }
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
void Pipewire::onNodeAdded(PwNode* node) {
|
void Pipewire::onNodeAdded(PwNode* node) {
|
||||||
auto* iface = PwNodeIface::instance(node);
|
auto* iface = PwNodeIface::instance(node);
|
||||||
QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onNodeRemoved);
|
QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onNodeRemoved);
|
||||||
|
this->mNodes.insertObject(iface);
|
||||||
this->mNodes.push_back(iface);
|
|
||||||
emit this->nodesChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Pipewire::onNodeRemoved(QObject* object) {
|
void Pipewire::onNodeRemoved(QObject* object) {
|
||||||
auto* iface = static_cast<PwNodeIface*>(object); // NOLINT
|
auto* iface = static_cast<PwNodeIface*>(object); // NOLINT
|
||||||
this->mNodes.removeOne(iface);
|
this->mNodes.removeObject(iface);
|
||||||
emit this->nodesChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QQmlListProperty<PwLinkIface> Pipewire::links() {
|
ObjectModel<PwLinkIface>* Pipewire::links() { return &this->mLinks; }
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
void Pipewire::onLinkAdded(PwLink* link) {
|
void Pipewire::onLinkAdded(PwLink* link) {
|
||||||
auto* iface = PwLinkIface::instance(link);
|
auto* iface = PwLinkIface::instance(link);
|
||||||
QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkRemoved);
|
QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkRemoved);
|
||||||
|
this->mLinks.insertObject(iface);
|
||||||
this->mLinks.push_back(iface);
|
|
||||||
emit this->linksChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Pipewire::onLinkRemoved(QObject* object) {
|
void Pipewire::onLinkRemoved(QObject* object) {
|
||||||
auto* iface = static_cast<PwLinkIface*>(object); // NOLINT
|
auto* iface = static_cast<PwLinkIface*>(object); // NOLINT
|
||||||
this->mLinks.removeOne(iface);
|
this->mLinks.removeObject(iface);
|
||||||
emit this->linksChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QQmlListProperty<PwLinkGroupIface> Pipewire::linkGroups() {
|
ObjectModel<PwLinkGroupIface>* Pipewire::linkGroups() { return &this->mLinkGroups; }
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
void Pipewire::onLinkGroupAdded(PwLinkGroup* linkGroup) {
|
void Pipewire::onLinkGroupAdded(PwLinkGroup* linkGroup) {
|
||||||
auto* iface = PwLinkGroupIface::instance(linkGroup);
|
auto* iface = PwLinkGroupIface::instance(linkGroup);
|
||||||
QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkGroupRemoved);
|
QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkGroupRemoved);
|
||||||
|
this->mLinkGroups.insertObject(iface);
|
||||||
this->mLinkGroups.push_back(iface);
|
|
||||||
emit this->linkGroupsChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Pipewire::onLinkGroupRemoved(QObject* object) {
|
void Pipewire::onLinkGroupRemoved(QObject* object) {
|
||||||
auto* iface = static_cast<PwLinkGroupIface*>(object); // NOLINT
|
auto* iface = static_cast<PwLinkGroupIface*>(object); // NOLINT
|
||||||
this->mLinkGroups.removeOne(iface);
|
this->mLinkGroups.removeObject(iface);
|
||||||
emit this->linkGroupsChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PwNodeIface* Pipewire::defaultAudioSink() const { // NOLINT
|
PwNodeIface* Pipewire::defaultAudioSink() const { // NOLINT
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <qtypes.h>
|
#include <qtypes.h>
|
||||||
|
|
||||||
|
#include "../../core/model.hpp"
|
||||||
#include "link.hpp"
|
#include "link.hpp"
|
||||||
#include "node.hpp"
|
#include "node.hpp"
|
||||||
#include "registry.hpp"
|
#include "registry.hpp"
|
||||||
|
@ -52,11 +53,11 @@ class Pipewire: public QObject {
|
||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
// clang-format off
|
// clang-format off
|
||||||
/// All pipewire nodes.
|
/// All pipewire nodes.
|
||||||
Q_PROPERTY(QQmlListProperty<PwNodeIface> nodes READ nodes NOTIFY nodesChanged);
|
Q_PROPERTY(ObjectModel<PwNodeIface>* nodes READ nodes CONSTANT);
|
||||||
/// All pipewire links.
|
/// All pipewire links.
|
||||||
Q_PROPERTY(QQmlListProperty<PwLinkIface> links READ links NOTIFY linksChanged);
|
Q_PROPERTY(ObjectModel<PwLinkIface>* links READ links CONSTANT);
|
||||||
/// All pipewire link groups.
|
/// 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`.
|
/// The default audio sink or `null`.
|
||||||
Q_PROPERTY(PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged);
|
Q_PROPERTY(PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged);
|
||||||
/// The default audio source or `null`.
|
/// The default audio source or `null`.
|
||||||
|
@ -68,16 +69,13 @@ class Pipewire: public QObject {
|
||||||
public:
|
public:
|
||||||
explicit Pipewire(QObject* parent = nullptr);
|
explicit Pipewire(QObject* parent = nullptr);
|
||||||
|
|
||||||
[[nodiscard]] QQmlListProperty<PwNodeIface> nodes();
|
[[nodiscard]] ObjectModel<PwNodeIface>* nodes();
|
||||||
[[nodiscard]] QQmlListProperty<PwLinkIface> links();
|
[[nodiscard]] ObjectModel<PwLinkIface>* links();
|
||||||
[[nodiscard]] QQmlListProperty<PwLinkGroupIface> linkGroups();
|
[[nodiscard]] ObjectModel<PwLinkGroupIface>* linkGroups();
|
||||||
[[nodiscard]] PwNodeIface* defaultAudioSink() const;
|
[[nodiscard]] PwNodeIface* defaultAudioSink() const;
|
||||||
[[nodiscard]] PwNodeIface* defaultAudioSource() const;
|
[[nodiscard]] PwNodeIface* defaultAudioSource() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void nodesChanged();
|
|
||||||
void linksChanged();
|
|
||||||
void linkGroupsChanged();
|
|
||||||
void defaultAudioSinkChanged();
|
void defaultAudioSinkChanged();
|
||||||
void defaultAudioSourceChanged();
|
void defaultAudioSourceChanged();
|
||||||
|
|
||||||
|
@ -90,17 +88,9 @@ private slots:
|
||||||
void onLinkGroupRemoved(QObject* object);
|
void onLinkGroupRemoved(QObject* object);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static qsizetype nodesCount(QQmlListProperty<PwNodeIface>* property);
|
ObjectModel<PwNodeIface> mNodes {this};
|
||||||
static PwNodeIface* nodeAt(QQmlListProperty<PwNodeIface>* property, qsizetype index);
|
ObjectModel<PwLinkIface> mLinks {this};
|
||||||
static qsizetype linksCount(QQmlListProperty<PwLinkIface>* property);
|
ObjectModel<PwLinkGroupIface> mLinkGroups {this};
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
///! Tracks all link connections to a given node.
|
///! Tracks all link connections to a given node.
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <qtypes.h>
|
#include <qtypes.h>
|
||||||
|
|
||||||
|
#include "../../core/model.hpp"
|
||||||
#include "../../dbus/dbusmenu/dbusmenu.hpp"
|
#include "../../dbus/dbusmenu/dbusmenu.hpp"
|
||||||
#include "../../dbus/properties.hpp"
|
#include "../../dbus/properties.hpp"
|
||||||
#include "host.hpp"
|
#include "host.hpp"
|
||||||
|
@ -106,46 +107,25 @@ SystemTray::SystemTray(QObject* parent): QObject(parent) {
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
for (auto* item: host->items()) {
|
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) {
|
void SystemTray::onItemRegistered(StatusNotifierItem* item) {
|
||||||
this->mItems.push_back(new SystemTrayItem(item, this));
|
this->mItems.insertObject(new SystemTrayItem(item, this));
|
||||||
emit this->itemsChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SystemTray::onItemUnregistered(StatusNotifierItem* item) {
|
void SystemTray::onItemUnregistered(StatusNotifierItem* item) {
|
||||||
SystemTrayItem* trayItem = nullptr;
|
for (const auto* storedItem: this->mItems.valueList()) {
|
||||||
|
if (storedItem->item == item) {
|
||||||
this->mItems.removeIf([item, &trayItem](SystemTrayItem* testItem) {
|
this->mItems.removeObject(storedItem);
|
||||||
if (testItem->item == item) {
|
delete storedItem;
|
||||||
trayItem = testItem;
|
break;
|
||||||
return true;
|
}
|
||||||
} else return false;
|
}
|
||||||
});
|
|
||||||
|
|
||||||
emit this->itemsChanged();
|
|
||||||
|
|
||||||
delete trayItem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QQmlListProperty<SystemTrayItem> SystemTray::items() {
|
ObjectModel<SystemTrayItem>* SystemTray::items() { return &this->mItems; }
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemTrayItem* SystemTrayMenuWatcher::trayItem() const { return this->item; }
|
SystemTrayItem* SystemTrayMenuWatcher::trayItem() const { return this->item; }
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,9 @@
|
||||||
|
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
#include <qqmllist.h>
|
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <qtypes.h>
|
|
||||||
|
|
||||||
|
#include "../../core/model.hpp"
|
||||||
#include "item.hpp"
|
#include "item.hpp"
|
||||||
|
|
||||||
namespace SystemTrayStatus { // NOLINT
|
namespace SystemTrayStatus { // NOLINT
|
||||||
|
@ -108,27 +107,21 @@ signals:
|
||||||
class SystemTray: public QObject {
|
class SystemTray: public QObject {
|
||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
/// List of all system tray icons.
|
/// 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_ELEMENT;
|
||||||
QML_SINGLETON;
|
QML_SINGLETON;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit SystemTray(QObject* parent = nullptr);
|
explicit SystemTray(QObject* parent = nullptr);
|
||||||
|
|
||||||
[[nodiscard]] QQmlListProperty<SystemTrayItem> items();
|
[[nodiscard]] ObjectModel<SystemTrayItem>* items();
|
||||||
|
|
||||||
signals:
|
|
||||||
void itemsChanged();
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onItemRegistered(qs::service::sni::StatusNotifierItem* item);
|
void onItemRegistered(qs::service::sni::StatusNotifierItem* item);
|
||||||
void onItemUnregistered(qs::service::sni::StatusNotifierItem* item);
|
void onItemUnregistered(qs::service::sni::StatusNotifierItem* item);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static qsizetype itemsCount(QQmlListProperty<SystemTrayItem>* property);
|
ObjectModel<SystemTrayItem> mItems {this};
|
||||||
static SystemTrayItem* itemAt(QQmlListProperty<SystemTrayItem>* property, qsizetype index);
|
|
||||||
|
|
||||||
QList<SystemTrayItem*> mItems;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
///! Accessor for SystemTrayItem menus.
|
///! Accessor for SystemTrayItem menus.
|
||||||
|
|
|
@ -51,16 +51,19 @@ endfunction()
|
||||||
# -----
|
# -----
|
||||||
|
|
||||||
qt_add_library(quickshell-wayland STATIC)
|
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
|
# required to make sure the constructor is linked
|
||||||
add_library(quickshell-wayland-init OBJECT init.cpp)
|
add_library(quickshell-wayland-init OBJECT init.cpp)
|
||||||
|
|
||||||
|
set(WAYLAND_MODULES)
|
||||||
|
|
||||||
if (WAYLAND_WLR_LAYERSHELL)
|
if (WAYLAND_WLR_LAYERSHELL)
|
||||||
target_sources(quickshell-wayland PRIVATE wlr_layershell.cpp)
|
target_sources(quickshell-wayland PRIVATE wlr_layershell.cpp)
|
||||||
add_subdirectory(wlr_layershell)
|
add_subdirectory(wlr_layershell)
|
||||||
target_compile_definitions(quickshell-wayland PRIVATE QS_WAYLAND_WLR_LAYERSHELL)
|
target_compile_definitions(quickshell-wayland PRIVATE QS_WAYLAND_WLR_LAYERSHELL)
|
||||||
target_compile_definitions(quickshell-wayland-init PRIVATE QS_WAYLAND_WLR_LAYERSHELL)
|
target_compile_definitions(quickshell-wayland-init PRIVATE QS_WAYLAND_WLR_LAYERSHELL)
|
||||||
|
|
||||||
|
list(APPEND WAYLAND_MODULES Quickshell.Wayland._WlrLayerShell)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (WAYLAND_SESSION_LOCK)
|
if (WAYLAND_SESSION_LOCK)
|
||||||
|
@ -68,6 +71,11 @@ if (WAYLAND_SESSION_LOCK)
|
||||||
add_subdirectory(session_lock)
|
add_subdirectory(session_lock)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (WAYLAND_TOPLEVEL_MANAGEMENT)
|
||||||
|
add_subdirectory(toplevel_management)
|
||||||
|
list(APPEND WAYLAND_MODULES Quickshell.Wayland._ToplevelManagement)
|
||||||
|
endif()
|
||||||
|
|
||||||
if (HYPRLAND)
|
if (HYPRLAND)
|
||||||
add_subdirectory(hyprland)
|
add_subdirectory(hyprland)
|
||||||
endif()
|
endif()
|
||||||
|
@ -75,6 +83,12 @@ endif()
|
||||||
target_link_libraries(quickshell-wayland PRIVATE ${QT_DEPS})
|
target_link_libraries(quickshell-wayland PRIVATE ${QT_DEPS})
|
||||||
target_link_libraries(quickshell-wayland-init 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-wayland)
|
||||||
qs_pch(quickshell-waylandplugin)
|
qs_pch(quickshell-waylandplugin)
|
||||||
qs_pch(quickshell-wayland-init)
|
qs_pch(quickshell-wayland-init)
|
||||||
|
|
|
@ -1,15 +1,29 @@
|
||||||
qt_add_library(quickshell-hyprland STATIC)
|
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)
|
if (HYPRLAND_FOCUS_GRAB)
|
||||||
add_subdirectory(focus_grab)
|
add_subdirectory(focus_grab)
|
||||||
|
list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._FocusGrab)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (HYPRLAND_GLOBAL_SHORTCUTS)
|
if (HYPRLAND_GLOBAL_SHORTCUTS)
|
||||||
add_subdirectory(global_shortcuts)
|
add_subdirectory(global_shortcuts)
|
||||||
|
list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._GlobalShortcuts)
|
||||||
endif()
|
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-hyprland)
|
||||||
qs_pch(quickshell-hyprlandplugin)
|
qs_pch(quickshell-hyprlandplugin)
|
||||||
|
|
|
@ -9,21 +9,14 @@ qt_add_qml_module(quickshell-hyprland-focus-grab
|
||||||
VERSION 0.1
|
VERSION 0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(quickshell-hyprland-focus-grab-init OBJECT init.cpp)
|
|
||||||
|
|
||||||
wl_proto(quickshell-hyprland-focus-grab
|
wl_proto(quickshell-hyprland-focus-grab
|
||||||
hyprland-focus-grab-v1
|
hyprland-focus-grab-v1
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml"
|
"${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 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-grab)
|
||||||
qs_pch(quickshell-hyprland-focus-grabplugin)
|
qs_pch(quickshell-hyprland-focus-grabplugin)
|
||||||
qs_pch(quickshell-hyprland-focus-grab-init)
|
|
||||||
|
|
||||||
target_link_libraries(quickshell PRIVATE
|
target_link_libraries(quickshell PRIVATE quickshell-hyprland-focus-grabplugin)
|
||||||
quickshell-hyprland-focus-grabplugin
|
|
||||||
quickshell-hyprland-focus-grab-init
|
|
||||||
)
|
|
||||||
|
|
|
@ -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
|
VERSION 0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(quickshell-hyprland-global-shortcuts-init OBJECT init.cpp)
|
|
||||||
|
|
||||||
wl_proto(quickshell-hyprland-global-shortcuts
|
wl_proto(quickshell-hyprland-global-shortcuts
|
||||||
hyprland-global-shortcuts-v1
|
hyprland-global-shortcuts-v1
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/hyprland-global-shortcuts-v1.xml"
|
"${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 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-shortcuts)
|
||||||
qs_pch(quickshell-hyprland-global-shortcutsplugin)
|
qs_pch(quickshell-hyprland-global-shortcutsplugin)
|
||||||
qs_pch(quickshell-hyprland-global-shortcuts-init)
|
|
||||||
|
|
||||||
target_link_libraries(quickshell PRIVATE
|
target_link_libraries(quickshell PRIVATE quickshell-hyprland-global-shortcutsplugin)
|
||||||
quickshell-hyprland-global-shortcutsplugin
|
|
||||||
quickshell-hyprland-global-shortcuts-init
|
|
||||||
)
|
|
||||||
|
|
|
@ -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"
|
name = "Quickshell.Hyprland"
|
||||||
description = "Hyprland specific Quickshell types"
|
description = "Hyprland specific Quickshell types"
|
||||||
headers = [
|
headers = [
|
||||||
|
"ipc/connection.hpp",
|
||||||
|
"ipc/monitor.hpp",
|
||||||
|
"ipc/workspace.hpp",
|
||||||
|
"ipc/qml.hpp",
|
||||||
"focus_grab/qml.hpp",
|
"focus_grab/qml.hpp",
|
||||||
"global_shortcuts/qml.hpp",
|
"global_shortcuts/qml.hpp",
|
||||||
]
|
]
|
||||||
|
|
|
@ -34,13 +34,6 @@ class WaylandPlugin: public QuickshellPlugin {
|
||||||
// will not be registered. This can be worked around with a module import which makes
|
// 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.
|
// the QML_ELMENT module import the old register-type style module.
|
||||||
|
|
||||||
qmlRegisterModuleImport(
|
|
||||||
"Quickshell.Wayland",
|
|
||||||
QQmlModuleImportModuleAny,
|
|
||||||
"Quickshell.Wayland._WlrLayerShell",
|
|
||||||
QQmlModuleImportLatest
|
|
||||||
);
|
|
||||||
|
|
||||||
qmlRegisterModuleImport(
|
qmlRegisterModuleImport(
|
||||||
"Quickshell",
|
"Quickshell",
|
||||||
QQmlModuleImportModuleAny,
|
QQmlModuleImportModuleAny,
|
||||||
|
|
|
@ -4,5 +4,6 @@ headers = [
|
||||||
"wlr_layershell/window.hpp",
|
"wlr_layershell/window.hpp",
|
||||||
"wlr_layershell.hpp",
|
"wlr_layershell.hpp",
|
||||||
"session_lock.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>
|
|
@ -7,7 +7,6 @@
|
||||||
#include <private/qwaylandsurface_p.h>
|
#include <private/qwaylandsurface_p.h>
|
||||||
#include <private/qwaylandwindow_p.h>
|
#include <private/qwaylandwindow_p.h>
|
||||||
#include <qlogging.h>
|
#include <qlogging.h>
|
||||||
#include <qpoint.h>
|
|
||||||
#include <qrect.h>
|
#include <qrect.h>
|
||||||
#include <qsize.h>
|
#include <qsize.h>
|
||||||
#include <qtversionchecks.h>
|
#include <qtversionchecks.h>
|
||||||
|
@ -18,6 +17,10 @@
|
||||||
#include "shell_integration.hpp"
|
#include "shell_integration.hpp"
|
||||||
#include "window.hpp"
|
#include "window.hpp"
|
||||||
|
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
|
||||||
|
#include <qpoint.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
[[nodiscard]] QtWayland::zwlr_layer_shell_v1::layer toWaylandLayer(const WlrLayer::Enum& layer) noexcept;
|
[[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;
|
[[nodiscard]] QtWayland::zwlr_layer_surface_v1::anchor toWaylandAnchors(const Anchors& anchors) noexcept;
|
||||||
|
@ -72,7 +75,10 @@ QSWaylandLayerSurface::QSWaylandLayerSurface(
|
||||||
}
|
}
|
||||||
|
|
||||||
QSWaylandLayerSurface::~QSWaylandLayerSurface() {
|
QSWaylandLayerSurface::~QSWaylandLayerSurface() {
|
||||||
this->ext->surface = nullptr;
|
if (this->ext != nullptr) {
|
||||||
|
this->ext->surface = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
this->destroy();
|
this->destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,6 +112,7 @@ void QSWaylandLayerSurface::applyConfigure() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void QSWaylandLayerSurface::setWindowGeometry(const QRect& geometry) {
|
void QSWaylandLayerSurface::setWindowGeometry(const QRect& geometry) {
|
||||||
|
if (this->ext == nullptr) return;
|
||||||
auto size = constrainedSize(this->ext->mAnchors, geometry.size());
|
auto size = constrainedSize(this->ext->mAnchors, geometry.size());
|
||||||
this->set_size(size.width(), size.height());
|
this->set_size(size.width(), size.height());
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,12 @@
|
||||||
#include "shell_integration.hpp"
|
#include "shell_integration.hpp"
|
||||||
#include "surface.hpp"
|
#include "surface.hpp"
|
||||||
|
|
||||||
|
LayershellWindowExtension::~LayershellWindowExtension() {
|
||||||
|
if (this->surface != nullptr) {
|
||||||
|
this->surface->ext = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LayershellWindowExtension* LayershellWindowExtension::get(QWindow* window) {
|
LayershellWindowExtension* LayershellWindowExtension::get(QWindow* window) {
|
||||||
auto v = window->property("layershell_ext");
|
auto v = window->property("layershell_ext");
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qscreen.h>
|
#include <qscreen.h>
|
||||||
|
#include <qtclasshelpermacros.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <qtypes.h>
|
#include <qtypes.h>
|
||||||
#include <qwindow.h>
|
#include <qwindow.h>
|
||||||
|
@ -56,6 +57,8 @@ class LayershellWindowExtension: public QObject {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
LayershellWindowExtension(QObject* parent = nullptr): QObject(parent) {}
|
LayershellWindowExtension(QObject* parent = nullptr): QObject(parent) {}
|
||||||
|
~LayershellWindowExtension() override;
|
||||||
|
Q_DISABLE_COPY_MOVE(LayershellWindowExtension);
|
||||||
|
|
||||||
// returns the layershell extension if attached, otherwise nullptr
|
// returns the layershell extension if attached, otherwise nullptr
|
||||||
static LayershellWindowExtension* get(QWindow* window);
|
static LayershellWindowExtension* get(QWindow* window);
|
||||||
|
|
Loading…
Reference in a new issue